Merge pull request #1 from squaremo/automation-type
Implement automation type and controller, at the local minima
This commit is contained in:
commit
a0e26cee85
|
|
@ -1,5 +1,8 @@
|
|||
notes
|
||||
|
||||
# This is downloaded in the Makefile
|
||||
controllers/testdata/crds/*
|
||||
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ RUN go mod download
|
|||
# Copy the go source
|
||||
COPY main.go main.go
|
||||
COPY api/ api/
|
||||
COPY pkg/ pkg/
|
||||
COPY controllers/ controllers/
|
||||
|
||||
# Build
|
||||
|
|
|
|||
21
Makefile
21
Makefile
|
|
@ -4,6 +4,10 @@ IMG ?= squaremo/image-automation-controller
|
|||
# Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
|
||||
CRD_OPTIONS ?= "crd:trivialVersions=true"
|
||||
|
||||
# Version of the Toolkit from which to get CRDs. Change this if you
|
||||
# bump the go module version.
|
||||
TOOLKIT_VERSION:=v0.0.6
|
||||
|
||||
# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
|
||||
ifeq (,$(shell go env GOBIN))
|
||||
GOBIN=$(shell go env GOPATH)/bin
|
||||
|
|
@ -11,10 +15,25 @@ else
|
|||
GOBIN=$(shell go env GOBIN)
|
||||
endif
|
||||
|
||||
TEST_CRDS:=controllers/testdata/crds
|
||||
|
||||
all: manager
|
||||
|
||||
# Running the tests requires the source.fluxcd.io CRDs
|
||||
test_deps: ${TEST_CRDS}/imagepolicies.yaml ${TEST_CRDS}/gitrepositories.yaml
|
||||
|
||||
${TEST_CRDS}/gitrepositories.yaml:
|
||||
mkdir -p ${TEST_CRDS}
|
||||
curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${TOOLKIT_VERSION}/config/crd/bases/source.fluxcd.io_gitrepositories.yaml \
|
||||
-o ${TEST_CRDS}/gitrepositories.yaml
|
||||
|
||||
${TEST_CRDS}/imagepolicies.yaml:
|
||||
mkdir -p ${TEST_CRDS}
|
||||
curl -s https://raw.githubusercontent.com/squaremo/image-reflector-controller/master/config/crd/bases/image.fluxcd.io_imagepolicies.yaml \
|
||||
-o ${TEST_CRDS}/imagepolicies.yaml
|
||||
|
||||
# Run tests
|
||||
test: generate fmt vet manifests
|
||||
test: test_deps generate fmt vet manifests
|
||||
go test ./... -coverprofile cover.out
|
||||
|
||||
# Build manager binary
|
||||
|
|
|
|||
6
PROJECT
6
PROJECT
|
|
@ -1,3 +1,7 @@
|
|||
domain: fluxcd.io
|
||||
repo: github.com/squaremo/image-automation
|
||||
repo: github.com/squaremo/image-automation-controller
|
||||
resources:
|
||||
- group: image
|
||||
kind: ImageUpdateAutomation
|
||||
version: v1alpha1
|
||||
version: "2"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Package v1alpha1 contains API Schema definitions for the image v1alpha1 API group
|
||||
// +kubebuilder:object:generate=true
|
||||
// +groupName=image.fluxcd.io
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
var (
|
||||
// GroupVersion is group version used to register these objects
|
||||
GroupVersion = schema.GroupVersion{Group: "image.fluxcd.io", Version: "v1alpha1"}
|
||||
|
||||
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
|
||||
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
|
||||
|
||||
// AddToScheme adds the types in this group-version to the given scheme.
|
||||
AddToScheme = SchemeBuilder.AddToScheme
|
||||
)
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
|
||||
// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized.
|
||||
|
||||
// ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
|
||||
type ImageUpdateAutomationSpec struct {
|
||||
// GitRepository refers to the resource carry access details to a
|
||||
// git repository to update files in.
|
||||
// +required
|
||||
GitRepository corev1.LocalObjectReference `json:"gitRepository"`
|
||||
// Update gives the specification for how to update the files in
|
||||
// the repository
|
||||
// +required
|
||||
Update UpdateStrategy `json:"update"`
|
||||
// Commit specifies how to commit to the git repo
|
||||
// +required
|
||||
Commit CommitSpec `json:"commit"`
|
||||
}
|
||||
|
||||
// UpdateStrategy is a union of the various strategies for updating
|
||||
// the git repository.
|
||||
type UpdateStrategy struct {
|
||||
// ImagePolicy if present means update all workloads using the
|
||||
// given policy's image, to the policy's latest image reference.
|
||||
// +optional
|
||||
ImagePolicy *corev1.LocalObjectReference `json:"imagePolicy,omitempty"`
|
||||
}
|
||||
|
||||
// CommitSpec specifies how to commit changes to the git repository
|
||||
type CommitSpec struct {
|
||||
// AuthorName gives the name to provide when making a commit
|
||||
// +required
|
||||
AuthorName string `json:"authorName"`
|
||||
// AuthorEmail gives the email to provide when making a commit
|
||||
// +required
|
||||
AuthorEmail string `json:"authorEmail"`
|
||||
// MessageTemplate provides a template for the commit message,
|
||||
// into which will be interpolated the details of the change made.
|
||||
// +optional
|
||||
MessageTemplate string `json:"messageTemplate,omitempty"`
|
||||
}
|
||||
|
||||
// ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation
|
||||
type ImageUpdateAutomationStatus struct {
|
||||
// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
|
||||
// Important: Run "make" to regenerate code after modifying this file
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
|
||||
// ImageUpdateAutomation is the Schema for the imageupdateautomations API
|
||||
type ImageUpdateAutomation struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec ImageUpdateAutomationSpec `json:"spec,omitempty"`
|
||||
Status ImageUpdateAutomationStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// +kubebuilder:object:root=true
|
||||
// +kubebuilder:subresource:status
|
||||
|
||||
// ImageUpdateAutomationList contains a list of ImageUpdateAutomation
|
||||
type ImageUpdateAutomationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []ImageUpdateAutomation `json:"items"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
SchemeBuilder.Register(&ImageUpdateAutomation{}, &ImageUpdateAutomationList{})
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
// +build !ignore_autogenerated
|
||||
|
||||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Code generated by controller-gen. DO NOT EDIT.
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import (
|
||||
"k8s.io/api/core/v1"
|
||||
runtime "k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *CommitSpec) DeepCopyInto(out *CommitSpec) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommitSpec.
|
||||
func (in *CommitSpec) DeepCopy() *CommitSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(CommitSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageUpdateAutomation) DeepCopyInto(out *ImageUpdateAutomation) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
out.Status = in.Status
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomation.
|
||||
func (in *ImageUpdateAutomation) DeepCopy() *ImageUpdateAutomation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImageUpdateAutomation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImageUpdateAutomation) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageUpdateAutomationList) DeepCopyInto(out *ImageUpdateAutomationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
in, out := &in.Items, &out.Items
|
||||
*out = make([]ImageUpdateAutomation, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationList.
|
||||
func (in *ImageUpdateAutomationList) DeepCopy() *ImageUpdateAutomationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImageUpdateAutomationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
|
||||
func (in *ImageUpdateAutomationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageUpdateAutomationSpec) DeepCopyInto(out *ImageUpdateAutomationSpec) {
|
||||
*out = *in
|
||||
out.GitRepository = in.GitRepository
|
||||
in.Update.DeepCopyInto(&out.Update)
|
||||
out.Commit = in.Commit
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationSpec.
|
||||
func (in *ImageUpdateAutomationSpec) DeepCopy() *ImageUpdateAutomationSpec {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImageUpdateAutomationSpec)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *ImageUpdateAutomationStatus) DeepCopyInto(out *ImageUpdateAutomationStatus) {
|
||||
*out = *in
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageUpdateAutomationStatus.
|
||||
func (in *ImageUpdateAutomationStatus) DeepCopy() *ImageUpdateAutomationStatus {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(ImageUpdateAutomationStatus)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) {
|
||||
*out = *in
|
||||
if in.ImagePolicy != nil {
|
||||
in, out := &in.ImagePolicy, &out.ImagePolicy
|
||||
*out = new(v1.LocalObjectReference)
|
||||
**out = **in
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UpdateStrategy.
|
||||
func (in *UpdateStrategy) DeepCopy() *UpdateStrategy {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(UpdateStrategy)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
|
||||
---
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
controller-gen.kubebuilder.io/version: v0.2.5
|
||||
creationTimestamp: null
|
||||
name: imageupdateautomations.image.fluxcd.io
|
||||
spec:
|
||||
group: image.fluxcd.io
|
||||
names:
|
||||
kind: ImageUpdateAutomation
|
||||
listKind: ImageUpdateAutomationList
|
||||
plural: imageupdateautomations
|
||||
singular: imageupdateautomation
|
||||
scope: Namespaced
|
||||
validation:
|
||||
openAPIV3Schema:
|
||||
description: ImageUpdateAutomation is the Schema for the imageupdateautomations
|
||||
API
|
||||
properties:
|
||||
apiVersion:
|
||||
description: 'APIVersion defines the versioned schema of this representation
|
||||
of an object. Servers should convert recognized schemas to the latest
|
||||
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
|
||||
type: string
|
||||
kind:
|
||||
description: 'Kind is a string value representing the REST resource this
|
||||
object represents. Servers may infer this from the endpoint the client
|
||||
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
|
||||
type: string
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
description: ImageUpdateAutomationSpec defines the desired state of ImageUpdateAutomation
|
||||
properties:
|
||||
commit:
|
||||
description: Commit specifies how to commit to the git repo
|
||||
properties:
|
||||
authorEmail:
|
||||
description: AuthorEmail gives the email to provide when making
|
||||
a commit
|
||||
type: string
|
||||
authorName:
|
||||
description: AuthorName gives the name to provide when making a
|
||||
commit
|
||||
type: string
|
||||
messageTemplate:
|
||||
description: MessageTemplate provides a template for the commit
|
||||
message, into which will be interpolated the details of the change
|
||||
made.
|
||||
type: string
|
||||
required:
|
||||
- authorEmail
|
||||
- authorName
|
||||
type: object
|
||||
gitRepository:
|
||||
description: GitRepository refers to the resource carry access details
|
||||
to a git repository to update files in.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
update:
|
||||
description: Update gives the specification for how to update the files
|
||||
in the repository
|
||||
properties:
|
||||
imagePolicy:
|
||||
description: ImagePolicy if present means update all workloads using
|
||||
the given policy's image, to the policy's latest image reference.
|
||||
properties:
|
||||
name:
|
||||
description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names
|
||||
TODO: Add other useful fields. apiVersion, kind, uid?'
|
||||
type: string
|
||||
type: object
|
||||
type: object
|
||||
required:
|
||||
- commit
|
||||
- gitRepository
|
||||
- update
|
||||
type: object
|
||||
status:
|
||||
description: ImageUpdateAutomationStatus defines the observed state of ImageUpdateAutomation
|
||||
type: object
|
||||
type: object
|
||||
version: v1alpha1
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
status:
|
||||
acceptedNames:
|
||||
kind: ""
|
||||
plural: ""
|
||||
conditions: []
|
||||
storedVersions: []
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
# This kustomization.yaml is not intended to be run by itself,
|
||||
# since it depends on service name and namespace that are out of this kustomize package.
|
||||
# It should be run by config/default
|
||||
resources:
|
||||
- bases/image.fluxcd.io_imageupdateautomations.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizeresource
|
||||
|
||||
patchesStrategicMerge:
|
||||
# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix.
|
||||
# patches here are for enabling the conversion webhook for each CRD
|
||||
#- patches/webhook_in_imageupdateautomations.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizewebhookpatch
|
||||
|
||||
# [CERTMANAGER] To enable webhook, uncomment all the sections with [CERTMANAGER] prefix.
|
||||
# patches here are for enabling the CA injection for each CRD
|
||||
#- patches/cainjection_in_imageupdateautomations.yaml
|
||||
# +kubebuilder:scaffold:crdkustomizecainjectionpatch
|
||||
|
||||
# the following config is for teaching kustomize how to do kustomization for CRDs.
|
||||
configurations:
|
||||
- kustomizeconfig.yaml
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# This file is for teaching kustomize how to substitute name and namespace reference in CRD
|
||||
nameReference:
|
||||
- kind: Service
|
||||
version: v1
|
||||
fieldSpecs:
|
||||
- kind: CustomResourceDefinition
|
||||
group: apiextensions.k8s.io
|
||||
path: spec/conversion/webhookClientConfig/service/name
|
||||
|
||||
namespace:
|
||||
- kind: CustomResourceDefinition
|
||||
group: apiextensions.k8s.io
|
||||
path: spec/conversion/webhookClientConfig/service/namespace
|
||||
create: false
|
||||
|
||||
varReference:
|
||||
- path: metadata/annotations
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
# The following patch adds a directive for certmanager to inject CA into the CRD
|
||||
# CRD conversion requires k8s 1.13 or later.
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
annotations:
|
||||
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
|
||||
name: imageupdateautomations.image.fluxcd.io
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# The following patch enables conversion webhook for CRD
|
||||
# CRD conversion requires k8s 1.13 or later.
|
||||
apiVersion: apiextensions.k8s.io/v1beta1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: imageupdateautomations.image.fluxcd.io
|
||||
spec:
|
||||
conversion:
|
||||
strategy: Webhook
|
||||
webhookClientConfig:
|
||||
# this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank,
|
||||
# but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager)
|
||||
caBundle: Cg==
|
||||
service:
|
||||
namespace: system
|
||||
name: webhook-service
|
||||
path: /convert
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: controller-manager
|
||||
name: image-automation-controller
|
||||
labels:
|
||||
control-plane: controller-manager
|
||||
spec:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# permissions for end users to edit imageupdateautomations.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: imageupdateautomation-editor-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations/status
|
||||
verbs:
|
||||
- get
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# permissions for end users to view imageupdateautomations.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: imageupdateautomation-viewer-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations/status
|
||||
verbs:
|
||||
- get
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: manager-role
|
||||
rules:
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations
|
||||
verbs:
|
||||
- create
|
||||
- delete
|
||||
- get
|
||||
- list
|
||||
- patch
|
||||
- update
|
||||
- watch
|
||||
- apiGroups:
|
||||
- image.fluxcd.io
|
||||
resources:
|
||||
- imageupdateautomations/status
|
||||
verbs:
|
||||
- get
|
||||
- patch
|
||||
- update
|
||||
- apiGroups:
|
||||
- source.fluxcd.io
|
||||
resources:
|
||||
- gitrepositories
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
- watch
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
apiVersion: image.fluxcd.io/v1alpha1
|
||||
kind: ImageUpdateAutomation
|
||||
metadata:
|
||||
name: imageupdateautomation-sample
|
||||
spec:
|
||||
# Add fields here
|
||||
foo: bar
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
gogit "github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/plumbing/transport"
|
||||
"github.com/go-logr/logr"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
sourcev1alpha1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
||||
"github.com/fluxcd/source-controller/pkg/git"
|
||||
imagev1alpha1 "github.com/squaremo/image-automation-controller/api/v1alpha1"
|
||||
"github.com/squaremo/image-automation-controller/pkg/update"
|
||||
imagev1alpha1_reflect "github.com/squaremo/image-reflector-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
// log level for debug info
|
||||
const debug = 1
|
||||
const originRemote = "origin"
|
||||
|
||||
const defaultMessageTemplate = `Update from image update automation`
|
||||
|
||||
// ImageUpdateAutomationReconciler reconciles a ImageUpdateAutomation object
|
||||
type ImageUpdateAutomationReconciler struct {
|
||||
client.Client
|
||||
Log logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imageupdateautomations,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=image.fluxcd.io,resources=imageupdateautomations/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=source.fluxcd.io,resources=gitrepositories,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=source.fluxcd.io,resources=gitrepositories,verbs=get;list;watch
|
||||
|
||||
func (r *ImageUpdateAutomationReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) {
|
||||
ctx := context.Background()
|
||||
log := r.Log.WithValues("imageupdateautomation", req.NamespacedName)
|
||||
|
||||
var auto imagev1alpha1.ImageUpdateAutomation
|
||||
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
|
||||
return ctrl.Result{}, client.IgnoreNotFound(err)
|
||||
}
|
||||
|
||||
// get the git repository object so it can be checked out
|
||||
var origin sourcev1alpha1.GitRepository
|
||||
originName := types.NamespacedName{
|
||||
Name: auto.Spec.GitRepository.Name,
|
||||
Namespace: auto.GetNamespace(),
|
||||
}
|
||||
if err := r.Get(ctx, originName, &origin); err != nil {
|
||||
// TODO status
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
log.Error(err, "referenced git repository does not exist")
|
||||
return ctrl.Result{}, nil // and assume we'll hear about it when it arrives
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.V(debug).Info("found git repository", "gitrepository", originName)
|
||||
|
||||
tmp, err := ioutil.TempDir("", fmt.Sprintf("%s-%s", originName.Namespace, originName.Name))
|
||||
if err != nil {
|
||||
// TODO status
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
//defer os.RemoveAll(tmp)
|
||||
|
||||
// FIXME use context with deadline for at least the following ops
|
||||
|
||||
access, err := r.getRepoAccess(ctx, &origin)
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
var repo *gogit.Repository
|
||||
if repo, err = cloneInto(ctx, access, tmp); err != nil {
|
||||
// TODO status
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
log.V(debug).Info("cloned git repository", "gitrepository", originName, "working", tmp)
|
||||
|
||||
updateStrat := auto.Spec.Update
|
||||
switch {
|
||||
case updateStrat.ImagePolicy != nil:
|
||||
var policy imagev1alpha1_reflect.ImagePolicy
|
||||
policyName := types.NamespacedName{
|
||||
Namespace: auto.GetNamespace(),
|
||||
Name: updateStrat.ImagePolicy.Name,
|
||||
}
|
||||
if err := r.Get(ctx, policyName, &policy); err != nil {
|
||||
if client.IgnoreNotFound(err) == nil {
|
||||
log.Info("referenced ImagePolicy not found")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
if err := updateAccordingToImagePolicy(ctx, tmp, &policy); err != nil {
|
||||
if err == errImagePolicyNotReady {
|
||||
log.Info("image policy does not have latest image ref", "imagepolicy", policyName)
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
default:
|
||||
log.Info("no update strategy given in the spec")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
log.V(debug).Info("made updates to working dir", "working", tmp)
|
||||
|
||||
var rev string
|
||||
if rev, err = commitAllAndPush(ctx, repo, access, &auto.Spec.Commit); err != nil {
|
||||
if err == errNoChanges {
|
||||
log.Info("no changes made in working directory; no commit")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
log.V(debug).Info("pushed commit to origin", "revision", rev)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
func (r *ImageUpdateAutomationReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&imagev1alpha1.ImageUpdateAutomation{}).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
// --- git ops
|
||||
|
||||
type repoAccess struct {
|
||||
auth transport.AuthMethod
|
||||
url string
|
||||
}
|
||||
|
||||
func (r *ImageUpdateAutomationReconciler) getRepoAccess(ctx context.Context, repository *sourcev1alpha1.GitRepository) (repoAccess, error) {
|
||||
var access repoAccess
|
||||
access.url = repository.Spec.URL
|
||||
authStrat := git.AuthSecretStrategyForURL(access.url)
|
||||
|
||||
if repository.Spec.SecretRef != nil && authStrat != nil {
|
||||
name := types.NamespacedName{
|
||||
Namespace: repository.GetNamespace(),
|
||||
Name: repository.Spec.SecretRef.Name,
|
||||
}
|
||||
|
||||
var secret corev1.Secret
|
||||
err := r.Client.Get(ctx, name, &secret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("auth secret error: %w", err)
|
||||
return access, err
|
||||
}
|
||||
|
||||
access.auth, err = authStrat.Method(secret)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("auth error: %w", err)
|
||||
return access, err
|
||||
}
|
||||
}
|
||||
return access, nil
|
||||
}
|
||||
|
||||
func cloneInto(ctx context.Context, access repoAccess, path string) (*gogit.Repository, error) {
|
||||
// For now, check out the default branch. Using `nil` will do this
|
||||
// for now; but, it's likely that eventually a *GitRepositoryRef
|
||||
// will come from the image-update-automation object or the
|
||||
// git-repository object.
|
||||
checkoutStrat := git.CheckoutStrategyForRef(nil)
|
||||
_, _, err := checkoutStrat.Checkout(ctx, path, access.url, access.auth)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return gogit.PlainOpen(path)
|
||||
}
|
||||
|
||||
var errNoChanges = errors.New("no changes in working directory")
|
||||
|
||||
func commitAllAndPush(ctx context.Context, repo *gogit.Repository, access repoAccess, commit *imagev1alpha1.CommitSpec) (string, error) {
|
||||
working, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
status, err := working.Status()
|
||||
if err != nil {
|
||||
return "", err
|
||||
} else if status.IsClean() {
|
||||
return "", errNoChanges
|
||||
}
|
||||
|
||||
msgTmpl := commit.MessageTemplate
|
||||
if msgTmpl == "" {
|
||||
msgTmpl = defaultMessageTemplate
|
||||
}
|
||||
tmpl, err := template.New("commit message").Parse(msgTmpl)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := &strings.Builder{}
|
||||
if err := tmpl.Execute(buf, "no data! yet"); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var rev plumbing.Hash
|
||||
if rev, err = working.Commit(buf.String(), &gogit.CommitOptions{
|
||||
All: true,
|
||||
Author: &object.Signature{
|
||||
Name: commit.AuthorName,
|
||||
Email: commit.AuthorEmail,
|
||||
When: time.Now(),
|
||||
},
|
||||
}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return rev.String(), repo.PushContext(ctx, &gogit.PushOptions{
|
||||
Auth: access.auth,
|
||||
})
|
||||
}
|
||||
|
||||
// --- updates
|
||||
|
||||
var errImagePolicyNotReady = errors.New("ImagePolicy resource is not ready")
|
||||
|
||||
// update the manifest files under path according to policy, by
|
||||
// replacing any mention of the policy's image repository with the
|
||||
// latest ref.
|
||||
func updateAccordingToImagePolicy(ctx context.Context, path string, policy *imagev1alpha1_reflect.ImagePolicy) error {
|
||||
// the function that does the update expects an original and a
|
||||
// replacement; but it only uses the repository part of the
|
||||
// original, and it compares canonical forms (with the defaults
|
||||
// filled in). Since the latest image will have the same
|
||||
// repository, I can just pass that as the original.
|
||||
latestRef := policy.Status.LatestImage
|
||||
if latestRef == "" {
|
||||
return errImagePolicyNotReady
|
||||
}
|
||||
return update.UpdateImageEverywhere(path, path, latestRef, latestRef)
|
||||
}
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest/printer"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
sourcev1alpha1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
||||
imagev1alpha1 "github.com/squaremo/image-automation-controller/api/v1alpha1"
|
||||
imagev1alpha1_reflect "github.com/squaremo/image-reflector-controller/api/v1alpha1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var k8sManager ctrl.Manager
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecsWithDefaultAndCustomReporters(t,
|
||||
"Controller Suite",
|
||||
[]Reporter{printer.NewlineReporter{}})
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func(done Done) {
|
||||
logf.SetLogger(zap.LoggerTo(GinkgoWriter, true))
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{
|
||||
filepath.Join("..", "config", "crd", "bases"),
|
||||
filepath.Join("testdata", "crds"),
|
||||
},
|
||||
}
|
||||
|
||||
var err error
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(cfg).ToNot(BeNil())
|
||||
|
||||
Expect(imagev1alpha1.AddToScheme(scheme.Scheme)).To(Succeed())
|
||||
Expect(sourcev1alpha1.AddToScheme(scheme.Scheme)).To(Succeed())
|
||||
Expect(imagev1alpha1_reflect.AddToScheme(scheme.Scheme)).To(Succeed())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{
|
||||
Scheme: scheme.Scheme,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
err = (&ImageUpdateAutomationReconciler{
|
||||
Client: k8sManager.GetClient(),
|
||||
Log: ctrl.Log.WithName("controllers").WithName("ImageUpdateAutomation"),
|
||||
Scheme: scheme.Scheme,
|
||||
}).SetupWithManager(k8sManager)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
go func() {
|
||||
err = k8sManager.Start(ctrl.SetupSignalHandler())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
}()
|
||||
|
||||
k8sClient = k8sManager.GetClient()
|
||||
Expect(k8sClient).ToNot(BeNil())
|
||||
|
||||
close(done)
|
||||
}, 60)
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: helll
|
||||
image: helloworld:1.0.1
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: test
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: helll
|
||||
image: helloworld:1.0.0
|
||||
|
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/fluxcd/source-controller/pkg/testserver"
|
||||
"github.com/go-git/go-billy/v5/memfs"
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/config"
|
||||
//"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
sourcev1alpha1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
||||
imagev1alpha1 "github.com/squaremo/image-automation-controller/api/v1alpha1"
|
||||
"github.com/squaremo/image-automation-controller/pkg/test"
|
||||
imagev1alpha1_reflect "github.com/squaremo/image-reflector-controller/api/v1alpha1"
|
||||
)
|
||||
|
||||
const timeout = 10 * time.Second
|
||||
|
||||
// Copied from
|
||||
// https://github.com/fluxcd/source-controller/blob/master/controllers/suite_test.go
|
||||
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
|
||||
|
||||
func randStringRunes(n int) string {
|
||||
b := make([]rune, n)
|
||||
for i := range b {
|
||||
b[i] = letterRunes[rand.Intn(len(letterRunes))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var _ = Describe("ImageUpdateAutomation", func() {
|
||||
var (
|
||||
repositoryPath string
|
||||
repoURL string
|
||||
namespace *corev1.Namespace
|
||||
gitServer *testserver.GitServer
|
||||
gitRepoKey types.NamespacedName
|
||||
)
|
||||
|
||||
// Start the git server
|
||||
BeforeEach(func() {
|
||||
repositoryPath = "/config-" + randStringRunes(5) + ".git"
|
||||
|
||||
namespace = &corev1.Namespace{}
|
||||
namespace.Name = "image-auto-test-" + randStringRunes(5)
|
||||
Expect(k8sClient.Create(context.Background(), namespace)).To(Succeed())
|
||||
|
||||
var err error
|
||||
gitServer, err = testserver.NewTempGitServer()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
gitServer.AutoCreate()
|
||||
Expect(gitServer.StartHTTP()).To(Succeed())
|
||||
|
||||
repoURL = gitServer.HTTPAddress() + repositoryPath
|
||||
|
||||
gitRepoKey = types.NamespacedName{
|
||||
Name: "image-auto-" + randStringRunes(5),
|
||||
Namespace: namespace.Name,
|
||||
}
|
||||
|
||||
gitRepo := &sourcev1alpha1.GitRepository{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: gitRepoKey.Name,
|
||||
Namespace: namespace.Name,
|
||||
},
|
||||
Spec: sourcev1alpha1.GitRepositorySpec{
|
||||
URL: repoURL,
|
||||
Interval: metav1.Duration{Duration: time.Minute},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
gitServer.StopHTTP()
|
||||
os.RemoveAll(gitServer.Root())
|
||||
Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed())
|
||||
})
|
||||
|
||||
It("Initialises git OK", func() {
|
||||
Expect(initGitRepo(gitServer, "testdata/appconfig", repositoryPath)).To(Succeed())
|
||||
})
|
||||
|
||||
Context("with ImagePolicy", func() {
|
||||
var (
|
||||
localRepo *git.Repository
|
||||
updateKey types.NamespacedName
|
||||
policy *imagev1alpha1_reflect.ImagePolicy
|
||||
updateByImagePolicy *imagev1alpha1.ImageUpdateAutomation
|
||||
commitMessage string
|
||||
)
|
||||
|
||||
const latestImage = "helloworld:1.0.1"
|
||||
|
||||
BeforeEach(func() {
|
||||
Expect(initGitRepo(gitServer, "testdata/appconfig", repositoryPath)).To(Succeed())
|
||||
|
||||
var err error
|
||||
localRepo, err = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
|
||||
URL: repoURL,
|
||||
RemoteName: "origin",
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
|
||||
policyKey := types.NamespacedName{
|
||||
Name: "policy-" + randStringRunes(5),
|
||||
Namespace: namespace.Name,
|
||||
}
|
||||
// NB not testing the image reflector controller; this
|
||||
// will make a "fully formed" ImagePolicy object.
|
||||
policy = &imagev1alpha1_reflect.ImagePolicy{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: policyKey.Name,
|
||||
Namespace: policyKey.Namespace,
|
||||
},
|
||||
Spec: imagev1alpha1_reflect.ImagePolicySpec{},
|
||||
Status: imagev1alpha1_reflect.ImagePolicyStatus{
|
||||
LatestImage: latestImage,
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), policy)).To(Succeed())
|
||||
Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed())
|
||||
|
||||
commitMessage = "Commit a difference " + randStringRunes(5)
|
||||
updateKey = types.NamespacedName{
|
||||
Namespace: gitRepoKey.Namespace,
|
||||
Name: "update-" + randStringRunes(5),
|
||||
}
|
||||
updateByImagePolicy = &imagev1alpha1.ImageUpdateAutomation{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: updateKey.Name,
|
||||
Namespace: updateKey.Namespace,
|
||||
},
|
||||
Spec: imagev1alpha1.ImageUpdateAutomationSpec{
|
||||
GitRepository: corev1.LocalObjectReference{
|
||||
Name: gitRepoKey.Name,
|
||||
},
|
||||
Update: imagev1alpha1.UpdateStrategy{
|
||||
ImagePolicy: &corev1.LocalObjectReference{
|
||||
Name: policyKey.Name,
|
||||
},
|
||||
},
|
||||
Commit: imagev1alpha1.CommitSpec{
|
||||
MessageTemplate: commitMessage,
|
||||
},
|
||||
},
|
||||
}
|
||||
Expect(k8sClient.Create(context.Background(), updateByImagePolicy)).To(Succeed())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
Expect(k8sClient.Delete(context.Background(), updateByImagePolicy)).To(Succeed())
|
||||
Expect(k8sClient.Delete(context.Background(), policy)).To(Succeed())
|
||||
})
|
||||
|
||||
It("updates to the most recent image", func() {
|
||||
head, _ := localRepo.Head()
|
||||
headHash := head.Hash().String()
|
||||
working, err := localRepo.Worktree()
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Eventually(func() bool {
|
||||
if working.Pull(&git.PullOptions{}); err != nil {
|
||||
return false
|
||||
}
|
||||
h, _ := localRepo.Head()
|
||||
return headHash != h.Hash().String()
|
||||
}, timeout, time.Second).Should(BeTrue())
|
||||
head, _ = localRepo.Head()
|
||||
commit, err := localRepo.CommitObject(head.Hash())
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(commit.Message).To(Equal(commitMessage))
|
||||
|
||||
tmp, err := ioutil.TempDir("", "gotest-imageauto")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tmp)
|
||||
|
||||
_, err = git.PlainClone(tmp, false, &git.CloneOptions{
|
||||
URL: repoURL,
|
||||
})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
test.ExpectMatchingDirectories(tmp, "testdata/appconfig-expected")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// Initialise a git server with a repo including the files in dir.
|
||||
func initGitRepo(gitServer *testserver.GitServer, fixture, repositoryPath string) error {
|
||||
fs := memfs.New()
|
||||
repo, err := git.Init(memory.NewStorage(), fs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = filepath.Walk(fixture, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return fs.MkdirAll(fs.Join(path[len(fixture):]), info.Mode())
|
||||
}
|
||||
|
||||
fileBytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ff, err := fs.Create(path[len(fixture):])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer ff.Close()
|
||||
|
||||
_, err = ff.Write(fileBytes)
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
working, err := repo.Worktree()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = working.Add(".")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = working.Commit("Initial revision from "+fixture, &git.CommitOptions{
|
||||
Author: &object.Signature{
|
||||
Name: "Testbot",
|
||||
Email: "test@example.com",
|
||||
When: time.Now(),
|
||||
},
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
remote, err := repo.CreateRemote(&config.RemoteConfig{
|
||||
Name: "origin",
|
||||
URLs: []string{gitServer.HTTPAddress() + repositoryPath},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return remote.Push(&git.PushOptions{
|
||||
RefSpecs: []config.RefSpec{"refs/heads/*:refs/heads/*"},
|
||||
})
|
||||
}
|
||||
20
go.mod
20
go.mod
|
|
@ -3,7 +3,21 @@ module github.com/squaremo/image-automation-controller
|
|||
go 1.13
|
||||
|
||||
require (
|
||||
k8s.io/apimachinery v0.17.2
|
||||
k8s.io/client-go v0.17.2
|
||||
sigs.k8s.io/controller-runtime v0.5.0
|
||||
// If you bump this, change TOOLKIT_VERSION in the Makefile to match
|
||||
github.com/fluxcd/source-controller v0.0.6
|
||||
github.com/go-git/go-billy/v5 v5.0.0
|
||||
github.com/go-git/go-git/v5 v5.1.0
|
||||
github.com/go-logr/logr v0.1.0
|
||||
github.com/google/go-containerregistry v0.1.1
|
||||
github.com/onsi/ginkgo v1.12.1
|
||||
github.com/onsi/gomega v1.10.1
|
||||
github.com/squaremo/image-reflector-controller v0.0.0-20200719062427-4f918bf22db6
|
||||
k8s.io/api v0.18.4
|
||||
k8s.io/apimachinery v0.18.4
|
||||
k8s.io/client-go v0.18.4
|
||||
sigs.k8s.io/controller-runtime v0.6.1
|
||||
sigs.k8s.io/kustomize/kyaml v0.4.1
|
||||
)
|
||||
|
||||
// https://github.com/sosedoff/gitkit/pull/21
|
||||
replace github.com/sosedoff/gitkit => github.com/hiddeco/gitkit v0.2.1-0.20200422093229-4355fec70348
|
||||
|
|
|
|||
16
main.go
16
main.go
|
|
@ -25,6 +25,11 @@ import (
|
|||
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
sourcev1alpha1 "github.com/fluxcd/source-controller/api/v1alpha1"
|
||||
imagev1alpha1_auto "github.com/squaremo/image-automation-controller/api/v1alpha1"
|
||||
"github.com/squaremo/image-automation-controller/controllers"
|
||||
imagev1alpha1_reflect "github.com/squaremo/image-reflector-controller/api/v1alpha1"
|
||||
// +kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
|
|
@ -36,6 +41,9 @@ var (
|
|||
func init() {
|
||||
_ = clientgoscheme.AddToScheme(scheme)
|
||||
|
||||
_ = imagev1alpha1_auto.AddToScheme(scheme)
|
||||
_ = imagev1alpha1_reflect.AddToScheme(scheme)
|
||||
_ = sourcev1alpha1.AddToScheme(scheme)
|
||||
// +kubebuilder:scaffold:scheme
|
||||
}
|
||||
|
||||
|
|
@ -62,6 +70,14 @@ func main() {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err = (&controllers.ImageUpdateAutomationReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Log: ctrl.Log.WithName("controllers").WithName("ImageUpdateAutomation"),
|
||||
Scheme: mgr.GetScheme(),
|
||||
}).SetupWithManager(mgr); err != nil {
|
||||
setupLog.Error(err, "unable to create controller", "controller", "ImageUpdateAutomation")
|
||||
os.Exit(1)
|
||||
}
|
||||
// +kubebuilder:scaffold:builder
|
||||
|
||||
setupLog.Info("starting manager")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// TODO rewrite this as just doing the diff, so I can test that it
|
||||
// fails at the right times too.
|
||||
func ExpectMatchingDirectories(actualRoot, expectedRoot string) {
|
||||
Expect(actualRoot).To(BeADirectory())
|
||||
filepath.Walk(expectedRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// ignore emacs backups
|
||||
if strings.HasSuffix(path, "~") {
|
||||
return nil
|
||||
}
|
||||
relPath := path[len(expectedRoot):]
|
||||
actualPath := filepath.Join(actualRoot, relPath)
|
||||
if info.IsDir() {
|
||||
if strings.HasPrefix(filepath.Base(path), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
Expect(actualPath).To(BeADirectory())
|
||||
return nil
|
||||
}
|
||||
Expect(actualPath).To(BeARegularFile())
|
||||
actualBytes, err := ioutil.ReadFile(actualPath)
|
||||
expectedBytes, err := ioutil.ReadFile(path)
|
||||
Expect(string(actualBytes)).To(Equal(string(expectedBytes)))
|
||||
return nil
|
||||
})
|
||||
filepath.Walk(actualRoot, func(path string, info os.FileInfo, err error) error {
|
||||
p := path[len(actualRoot):]
|
||||
// ignore emacs backups
|
||||
if strings.HasSuffix(p, "~") {
|
||||
return nil
|
||||
}
|
||||
if info.IsDir() && strings.HasPrefix(filepath.Base(p), ".") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
Expect(filepath.Join(expectedRoot, p)).To(BeAnExistingFile())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Copyright 2020 Michael Bridgen <mikeb@squaremobius.net>
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestFiles(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Files comparison helper")
|
||||
}
|
||||
|
||||
var _ = Describe("Test helper", func() {
|
||||
It("matches when given the same directory", func() {
|
||||
ExpectMatchingDirectories("testdata/base", "testdata/base")
|
||||
})
|
||||
It("matches when given equivalent directories", func() {
|
||||
ExpectMatchingDirectories("testdata/base", "testdata/equiv")
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1 @@
|
|||
foo: 1
|
||||
|
|
@ -0,0 +1 @@
|
|||
foo: 1
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: c
|
||||
image: helloworld:v1.0.0
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: c
|
||||
image: helloworld:v1.0.0
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: c
|
||||
image: used:v1.1.0
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: bar
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: c
|
||||
image: used:v1.0.0
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// update any mention of an image with the canonical name
|
||||
// canonicalName, with the latestRef. TODO: other kinds.
|
||||
func UpdateImageEverywhere(inpath, outpath, imageName, latestRef string) error {
|
||||
updateImages := makeUpdateImagesFilter(imageName, latestRef)
|
||||
|
||||
reader := &kio.LocalPackageReader{
|
||||
PackagePath: inpath,
|
||||
IncludeSubpackages: true,
|
||||
}
|
||||
writer := &kio.LocalPackageWriter{
|
||||
PackagePath: outpath,
|
||||
}
|
||||
|
||||
pipeline := kio.Pipeline{
|
||||
Inputs: []kio.Reader{reader},
|
||||
Outputs: []kio.Writer{writer},
|
||||
Filters: []kio.Filter{updateImages},
|
||||
}
|
||||
return pipeline.Execute()
|
||||
}
|
||||
|
||||
func makeUpdateImagesFilter(originalRepo, replacement string) kio.Filter {
|
||||
originalRef, err := name.ParseReference(originalRepo)
|
||||
if err != nil {
|
||||
return kio.FilterFunc(func([]*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
return nil, err
|
||||
})
|
||||
}
|
||||
|
||||
canonName := originalRef.Context().String()
|
||||
replacementNode := yaml.NewScalarRNode(replacement)
|
||||
|
||||
replaceContainerImage := func(container *yaml.RNode) error {
|
||||
if imageField := container.Field("image"); imageField != nil {
|
||||
ref, err := name.ParseReference(imageField.Value.YNode().Value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if ref.Context().String() == canonName {
|
||||
imageField.Value.SetYNode(replacementNode.YNode())
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
replaceImageInEachContainer := yaml.FilterFunc(func(containers *yaml.RNode) (*yaml.RNode, error) {
|
||||
return containers, containers.VisitElements(replaceContainerImage)
|
||||
})
|
||||
|
||||
return kio.FilterFunc(func(objs []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
for _, obj := range objs {
|
||||
if err := obj.PipeE(
|
||||
yaml.Lookup("spec", "template", "spec"),
|
||||
yaml.Tee(
|
||||
yaml.Lookup("initContainers"),
|
||||
replaceImageInEachContainer,
|
||||
),
|
||||
yaml.Tee(
|
||||
yaml.Lookup("containers"),
|
||||
replaceImageInEachContainer,
|
||||
),
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return objs, nil
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
package update
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/squaremo/image-automation-controller/pkg/test"
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "Update suite")
|
||||
}
|
||||
|
||||
var _ = Describe("Update image everywhere", func() {
|
||||
It("leaves a different image alone", func() {
|
||||
tmp, err := ioutil.TempDir("", "gotest")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tmp)
|
||||
Expect(UpdateImageEverywhere("testdata/leave/original", tmp, "notused", "notused:v1.0.1")).To(Succeed())
|
||||
test.ExpectMatchingDirectories("testdata/leave/expected", tmp)
|
||||
})
|
||||
|
||||
It("replaces the given image", func() {
|
||||
tmp, err := ioutil.TempDir("", "gotest")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
defer os.RemoveAll(tmp)
|
||||
Expect(UpdateImageEverywhere("testdata/replace/original", tmp, "used", "used:v1.1.0")).To(Succeed())
|
||||
test.ExpectMatchingDirectories("testdata/replace/expected", tmp)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue