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 | notes | ||||||
| 
 | 
 | ||||||
|  | # This is downloaded in the Makefile | ||||||
|  | controllers/testdata/crds/* | ||||||
|  | 
 | ||||||
| # Binaries for programs and plugins | # Binaries for programs and plugins | ||||||
| *.exe | *.exe | ||||||
| *.exe~ | *.exe~ | ||||||
|  |  | ||||||
|  | @ -12,6 +12,7 @@ RUN go mod download | ||||||
| # Copy the go source | # Copy the go source | ||||||
| COPY main.go main.go | COPY main.go main.go | ||||||
| COPY api/ api/ | COPY api/ api/ | ||||||
|  | COPY pkg/ pkg/ | ||||||
| COPY controllers/ controllers/ | COPY controllers/ controllers/ | ||||||
| 
 | 
 | ||||||
| # Build | # 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)
 | # Produce CRDs that work back to Kubernetes 1.11 (no version conversion)
 | ||||||
| CRD_OPTIONS ?= "crd:trivialVersions=true" | 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)
 | # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
 | ||||||
| ifeq (,$(shell go env GOBIN)) | ifeq (,$(shell go env GOBIN)) | ||||||
| GOBIN=$(shell go env GOPATH)/bin | GOBIN=$(shell go env GOPATH)/bin | ||||||
|  | @ -11,10 +15,25 @@ else | ||||||
| GOBIN=$(shell go env GOBIN) | GOBIN=$(shell go env GOBIN) | ||||||
| endif | endif | ||||||
| 
 | 
 | ||||||
|  | TEST_CRDS:=controllers/testdata/crds | ||||||
|  | 
 | ||||||
| all: manager | 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
 | # Run tests
 | ||||||
| test: generate fmt vet manifests | test: test_deps generate fmt vet manifests | ||||||
| 	go test ./... -coverprofile cover.out | 	go test ./... -coverprofile cover.out | ||||||
| 
 | 
 | ||||||
| # Build manager binary
 | # Build manager binary
 | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								PROJECT
								
								
								
								
							
							
						
						
									
										6
									
								
								PROJECT
								
								
								
								
							|  | @ -1,3 +1,7 @@ | ||||||
| domain: fluxcd.io | 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" | 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 | apiVersion: apps/v1 | ||||||
| kind: Deployment | kind: Deployment | ||||||
| metadata: | metadata: | ||||||
|   name: controller-manager |   name: image-automation-controller | ||||||
|   labels: |   labels: | ||||||
|     control-plane: controller-manager |     control-plane: controller-manager | ||||||
| spec: | 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 | go 1.13 | ||||||
| 
 | 
 | ||||||
| require ( | require ( | ||||||
| 	k8s.io/apimachinery v0.17.2 | 	// If you bump this, change TOOLKIT_VERSION in the Makefile to match | ||||||
| 	k8s.io/client-go v0.17.2 | 	github.com/fluxcd/source-controller v0.0.6 | ||||||
| 	sigs.k8s.io/controller-runtime v0.5.0 | 	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" | 	_ "k8s.io/client-go/plugin/pkg/client/auth/gcp" | ||||||
| 	ctrl "sigs.k8s.io/controller-runtime" | 	ctrl "sigs.k8s.io/controller-runtime" | ||||||
| 	"sigs.k8s.io/controller-runtime/pkg/log/zap" | 	"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
 | 	// +kubebuilder:scaffold:imports
 | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -36,6 +41,9 @@ var ( | ||||||
| func init() { | func init() { | ||||||
| 	_ = clientgoscheme.AddToScheme(scheme) | 	_ = clientgoscheme.AddToScheme(scheme) | ||||||
| 
 | 
 | ||||||
|  | 	_ = imagev1alpha1_auto.AddToScheme(scheme) | ||||||
|  | 	_ = imagev1alpha1_reflect.AddToScheme(scheme) | ||||||
|  | 	_ = sourcev1alpha1.AddToScheme(scheme) | ||||||
| 	// +kubebuilder:scaffold:scheme
 | 	// +kubebuilder:scaffold:scheme
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -62,6 +70,14 @@ func main() { | ||||||
| 		os.Exit(1) | 		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
 | 	// +kubebuilder:scaffold:builder
 | ||||||
| 
 | 
 | ||||||
| 	setupLog.Info("starting manager") | 	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