Merge pull request #1 from squaremo/automation-type

Implement automation type and controller, at the local minima
This commit is contained in:
Michael Bridgen 2020-07-23 18:26:21 +01:00 committed by GitHub
commit a0e26cee85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 2610 additions and 16 deletions

3
.gitignore vendored
View File

@ -1,5 +1,8 @@
notes
# This is downloaded in the Makefile
controllers/testdata/crds/*
# Binaries for programs and plugins
*.exe
*.exe~

View File

@ -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

View File

@ -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

View File

@ -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"

View File

@ -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
)

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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: []

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,7 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: controller-manager
name: image-automation-controller
labels:
control-plane: controller-manager
spec:

View File

@ -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

View File

@ -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

36
config/rbac/role.yaml Normal file
View File

@ -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

View File

@ -0,0 +1,7 @@
apiVersion: image.fluxcd.io/v1alpha1
kind: ImageUpdateAutomation
metadata:
name: imageupdateautomation-sample
spec:
# Add fields here
foo: bar

View File

@ -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)
}

105
controllers/suite_test.go Normal file
View File

@ -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())
})

View File

@ -0,0 +1,10 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
template:
spec:
containers:
- name: helll
image: helloworld:1.0.1

View File

@ -0,0 +1,10 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: test
spec:
template:
spec:
containers:
- name: helll
image: helloworld:1.0.0

279
controllers/update_test.go Normal file
View File

@ -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
View File

@ -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

1087
go.sum

File diff suppressed because it is too large Load Diff

16
main.go
View File

@ -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")

67
pkg/test/files.go Normal file
View File

@ -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
})
}

38
pkg/test/files_test.go Normal file
View File

@ -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")
})
})

1
pkg/test/testdata/base/foo.yaml vendored Normal file
View File

@ -0,0 +1 @@
foo: 1

1
pkg/test/testdata/equiv/foo.yaml vendored Normal file
View File

@ -0,0 +1 @@
foo: 1

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

76
pkg/update/update.go Normal file
View File

@ -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
})
}

35
pkg/update/update_test.go Normal file
View File

@ -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)
})
})