From 84fe3fbe729f668db5e5d6876c8d4f57e436f378 Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Thu, 23 Jul 2020 08:48:16 +0100 Subject: [PATCH] Test controller to the point of committing changes This adds a test for the controller, checking that it will commit and push a change, with the given commit message. I think checking it made the right change will need a bit of rejigging tests elsewhere, so I can compare directory contents (as with pkg/update tests). --- .gitignore | 3 + Makefile | 21 ++- .../imageupdateautomation_controller.go | 25 +-- controllers/suite_test.go | 32 +++- controllers/update_test.go | 162 ++++++++++++++++-- go.mod | 1 + 6 files changed, 213 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 2f0c3c7d..5d13d6ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ notes +# This is downloaded in the Makefile +controllers/testdata/crds/* + # Binaries for programs and plugins *.exe *.exe~ diff --git a/Makefile b/Makefile index 06410fa1..ba8a36cf 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,10 @@ IMG ?= squaremo/image-automation-controller # Produce CRDs that work back to Kubernetes 1.11 (no version conversion) CRD_OPTIONS ?= "crd:trivialVersions=true" +# Version of the Toolkit from which to get CRDs. Change this if you +# bump the go module version. +TOOLKIT_VERSION:=v0.0.6 + # Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) ifeq (,$(shell go env GOBIN)) GOBIN=$(shell go env GOPATH)/bin @@ -11,10 +15,25 @@ else GOBIN=$(shell go env GOBIN) endif +TEST_CRDS:=controllers/testdata/crds + all: manager +# Running the tests requires the source.fluxcd.io CRDs +test_deps: ${TEST_CRDS}/imagepolicies.yaml ${TEST_CRDS}/gitrepositories.yaml + +${TEST_CRDS}/gitrepositories.yaml: + mkdir -p ${TEST_CRDS} + curl -s https://raw.githubusercontent.com/fluxcd/source-controller/${TOOLKIT_VERSION}/config/crd/bases/source.fluxcd.io_gitrepositories.yaml \ + -o ${TEST_CRDS}/gitrepositories.yaml + +${TEST_CRDS}/imagepolicies.yaml: + mkdir -p ${TEST_CRDS} + curl -s https://raw.githubusercontent.com/squaremo/image-reflector-controller/master/config/crd/bases/image.fluxcd.io_imagepolicies.yaml \ + -o ${TEST_CRDS}/imagepolicies.yaml + # Run tests -test: generate fmt vet manifests +test: test_deps generate fmt vet manifests go test ./... -coverprofile cover.out # Build manager binary diff --git a/controllers/imageupdateautomation_controller.go b/controllers/imageupdateautomation_controller.go index 228b11e0..09910353 100644 --- a/controllers/imageupdateautomation_controller.go +++ b/controllers/imageupdateautomation_controller.go @@ -26,6 +26,7 @@ import ( "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" @@ -137,12 +138,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(req ctrl.Request) (ctrl.Resu log.V(debug).Info("made updates to working dir", "working", tmp) - if err = commitAllAndPush(ctx, repo, access, &auto.Spec.Commit); err != nil { + 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 } @@ -203,17 +207,17 @@ func cloneInto(ctx context.Context, access repoAccess, path string) (*gogit.Repo var errNoChanges = errors.New("no changes in working directory") -func commitAllAndPush(ctx context.Context, repo *gogit.Repository, access repoAccess, commit *imagev1alpha1.CommitSpec) error { +func commitAllAndPush(ctx context.Context, repo *gogit.Repository, access repoAccess, commit *imagev1alpha1.CommitSpec) (string, error) { working, err := repo.Worktree() if err != nil { - return err + return "", err } status, err := working.Status() if err != nil { - return err + return "", err } else if status.IsClean() { - return errNoChanges + return "", errNoChanges } msgTmpl := commit.MessageTemplate @@ -222,14 +226,15 @@ func commitAllAndPush(ctx context.Context, repo *gogit.Repository, access repoAc } tmpl, err := template.New("commit message").Parse(msgTmpl) if err != nil { - return err + return "", err } buf := &strings.Builder{} if err := tmpl.Execute(buf, "no data! yet"); err != nil { - return err + return "", err } - if _, err = working.Commit(buf.String(), &gogit.CommitOptions{ + var rev plumbing.Hash + if rev, err = working.Commit(buf.String(), &gogit.CommitOptions{ All: true, Author: &object.Signature{ Name: commit.AuthorName, @@ -237,10 +242,10 @@ func commitAllAndPush(ctx context.Context, repo *gogit.Repository, access repoAc When: time.Now(), }, }); err != nil { - return err + return "", err } - return repo.PushContext(ctx, &gogit.PushOptions{ + return rev.String(), repo.PushContext(ctx, &gogit.PushOptions{ Auth: access.auth, }) } diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 6e8bdce5..0d6915c0 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -24,13 +24,16 @@ import ( . "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 ) @@ -39,6 +42,7 @@ import ( var cfg *rest.Config var k8sClient client.Client +var k8sManager ctrl.Manager var testEnv *envtest.Environment func TestAPIs(t *testing.T) { @@ -54,7 +58,10 @@ var _ = BeforeSuite(func(done Done) { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{ + filepath.Join("..", "config", "crd", "bases"), + filepath.Join("testdata", "crds"), + }, } var err error @@ -62,13 +69,30 @@ var _ = BeforeSuite(func(done Done) { Expect(err).ToNot(HaveOccurred()) Expect(cfg).ToNot(BeNil()) - err = imagev1alpha1.AddToScheme(scheme.Scheme) - Expect(err).NotTo(HaveOccurred()) + 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 - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.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) diff --git a/controllers/update_test.go b/controllers/update_test.go index 887b9cf8..cd39e94e 100644 --- a/controllers/update_test.go +++ b/controllers/update_test.go @@ -19,15 +19,11 @@ package controllers import ( "context" "io/ioutil" + "math/rand" "os" "path/filepath" "time" - . "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" "github.com/fluxcd/source-controller/pkg/testserver" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-git/v5" @@ -35,44 +31,178 @@ import ( //"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" - //imagev1alpha1 "github.com/squaremo/image-automation-controller/api/v1alpha1" + . "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" + imagev1alpha1_reflect "github.com/squaremo/image-reflector-controller/api/v1alpha1" ) -const repositoryPath = "/config.git" +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 ( - namespace *corev1.Namespace - gitServer *testserver.GitServer + repositoryPath string + repoURL string + namespace *corev1.Namespace + gitServer *testserver.GitServer + gitRepoKey types.NamespacedName ) - const namespaceName = "image-update-test" - + // Start the git server BeforeEach(func() { + repositoryPath = "/config-" + randStringRunes(5) + ".git" + namespace = &corev1.Namespace{} - namespace.Name = namespaceName + 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(gitServer.StartHTTP()).To(Succeed()) - defer gitServer.StopHTTP() - Expect(initGitRepo(gitServer, "testdata/appconfig")).To(Succeed()) + 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)) + }) }) }) // Initialise a git server with a repo including the files in dir. -func initGitRepo(gitServer *testserver.GitServer, fixture string) error { +func initGitRepo(gitServer *testserver.GitServer, fixture, repositoryPath string) error { fs := memfs.New() repo, err := git.Init(memory.NewStorage(), fs) if err != nil { diff --git a/go.mod b/go.mod index 1d00a0fc..3dfe48d1 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/squaremo/image-automation-controller go 1.13 require ( + // 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