From 985d879e31026040d21d34d6764fd0707cec9040 Mon Sep 17 00:00:00 2001 From: Sunny Date: Thu, 21 Apr 2022 18:59:05 +0530 Subject: [PATCH 1/5] Move ginkgo suite to legacy & add testenv Signed-off-by: Sunny --- controllers/legacy_suite_test.go | 119 +++++++++++++++++++++++++++++++ controllers/suite_test.go | 117 ++++++++++++------------------ 2 files changed, 163 insertions(+), 73 deletions(-) create mode 100644 controllers/legacy_suite_test.go diff --git a/controllers/legacy_suite_test.go b/controllers/legacy_suite_test.go new file mode 100644 index 00000000..bd3fbd33 --- /dev/null +++ b/controllers/legacy_suite_test.go @@ -0,0 +1,119 @@ +/* +Copyright 2020 The Flux authors + +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" + "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" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" + sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" + + imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1" + // +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 imageAutoReconciler *ImageUpdateAutomationReconciler +var ginkgoTestEnv *envtest.Environment +var stopManager context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecsWithDefaultAndCustomReporters(t, + "Controller Suite", + []Reporter{printer.NewlineReporter{}}) +} + +var _ = BeforeSuite(func(done Done) { + ctrl.SetLogger( + zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)), + ) + + By("bootstrapping test environment") + ginkgoTestEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{ + filepath.Join("..", "config", "crd", "bases"), + filepath.Join("testdata", "crds"), + }, + } + + mgrContext, cancel := context.WithCancel(ctx) + + var err error + cfg, err = ginkgoTestEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed()) + Expect(imagev1_reflect.AddToScheme(scheme.Scheme)).To(Succeed()) + + Expect(imagev1.AddToScheme(scheme.Scheme)).To(Succeed()) + // +kubebuilder:scaffold:scheme + + k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + }) + Expect(err).ToNot(HaveOccurred()) + + controllerName := "image-automation-controller" + imageAutoReconciler = &ImageUpdateAutomationReconciler{ + Client: k8sManager.GetClient(), + Scheme: scheme.Scheme, + EventRecorder: k8sManager.GetEventRecorderFor(controllerName), + } + Expect(imageAutoReconciler.SetupWithManager(k8sManager, ImageUpdateAutomationReconcilerOptions{})).To(Succeed()) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(mgrContext) + Expect(err).ToNot(HaveOccurred()) + }() + stopManager = cancel + + // Specifically an uncached client. Use .Get if you + // want to see what the reconcilers see. + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).ToNot(HaveOccurred()) + Expect(k8sClient).ToNot(BeNil()) + + close(done) +}, 60) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + stopManager() + err := ginkgoTestEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) diff --git a/controllers/suite_test.go b/controllers/suite_test.go index de32ee46..35e66600 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,102 +17,73 @@ limitations under the License. package controllers import ( - "context" + "fmt" + "math/rand" + "os" "path/filepath" "testing" + "time" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" "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" - "sigs.k8s.io/controller-runtime/pkg/log/zap" imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" + "github.com/fluxcd/pkg/runtime/testenv" sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1" // +kubebuilder:scaffold:imports ) -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +// These tests make use of plain Go using Gomega for assertions. +// At the beginning of every (sub)test Gomega can be initialized +// using gomega.NewWithT. +// Refer to http://onsi.github.io/gomega/ to learn more about +// Gomega. -var cfg *rest.Config -var k8sClient client.Client -var k8sManager ctrl.Manager -var imageAutoReconciler *ImageUpdateAutomationReconciler -var testEnv *envtest.Environment -var ctx context.Context -var cancel context.CancelFunc - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) +var ( + testEnv *testenv.Environment + ctx = ctrl.SetupSignalHandler() +) - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) +func init() { + rand.Seed(time.Now().UnixNano()) } -var _ = BeforeSuite(func(done Done) { - ctrl.SetLogger( - zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)), - ) - ctx, cancel = context.WithCancel(context.TODO()) - - 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(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(imagev1_reflect.AddToScheme(scheme.Scheme)).To(Succeed()) +func TestMain(m *testing.M) { + utilruntime.Must(imagev1_reflect.AddToScheme(scheme.Scheme)) + utilruntime.Must(sourcev1.AddToScheme(scheme.Scheme)) + utilruntime.Must(imagev1.AddToScheme(scheme.Scheme)) - Expect(imagev1.AddToScheme(scheme.Scheme)).To(Succeed()) - // +kubebuilder:scaffold:scheme - - k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, - }) - Expect(err).ToNot(HaveOccurred()) + testEnv = testenv.New(testenv.WithCRDPath( + filepath.Join("..", "config", "crd", "bases"), + filepath.Join("testdata", "crds"), + )) controllerName := "image-automation-controller" - imageAutoReconciler = &ImageUpdateAutomationReconciler{ - Client: k8sManager.GetClient(), + if err := (&ImageUpdateAutomationReconciler{ + Client: testEnv, Scheme: scheme.Scheme, - EventRecorder: k8sManager.GetEventRecorderFor(controllerName), + EventRecorder: testEnv.GetEventRecorderFor(controllerName), + }).SetupWithManager(testEnv, ImageUpdateAutomationReconcilerOptions{}); err != nil { + panic(fmt.Sprintf("Failed to start ImageUpdateAutomationReconciler: %v", err)) } - Expect(imageAutoReconciler.SetupWithManager(k8sManager, ImageUpdateAutomationReconcilerOptions{})).To(Succeed()) go func() { - defer GinkgoRecover() - err = k8sManager.Start(ctx) - Expect(err).ToNot(HaveOccurred()) + fmt.Println("Starting the test environment") + if err := testEnv.Start(ctx); err != nil { + panic(fmt.Sprintf("Failed to start the test environment manager: %v", err)) + } }() + <-testEnv.Manager.Elected() + + code := m.Run() - // Specifically an uncached client. Use .Get if you - // want to see what the reconcilers see. - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).ToNot(HaveOccurred()) - Expect(k8sClient).ToNot(BeNil()) - - close(done) -}, 60) - -var _ = AfterSuite(func() { - cancel() - By("tearing down the test environment") - err := testEnv.Stop() - Expect(err).ToNot(HaveOccurred()) -}) + fmt.Println("Stopping the test environment") + if err := testEnv.Stop(); err != nil { + panic(fmt.Sprintf("Failed to stop the test environment: %v", err)) + } + + os.Exit(code) +} From ffbc825dcdb93021b3a4902bf68917bb89df0175 Mon Sep 17 00:00:00 2001 From: Sunny Date: Sat, 25 Sep 2021 03:27:26 +0530 Subject: [PATCH 2/5] update pkg/test and pkg/update with testenv Signed-off-by: Sunny --- pkg/test/files.go | 24 ++--- pkg/test/files_test.go | 76 ++++++++------- pkg/update/filereader_test.go | 57 ++++++------ pkg/update/result_test.go | 104 +++++++++++---------- pkg/update/update_test.go | 168 +++++++++++++--------------------- 5 files changed, 200 insertions(+), 229 deletions(-) diff --git a/pkg/test/files.go b/pkg/test/files.go index 152604e1..a3d7d7c0 100644 --- a/pkg/test/files.go +++ b/pkg/test/files.go @@ -26,21 +26,21 @@ import ( // 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()) - Expect(expectedRoot).To(BeADirectory()) +func ExpectMatchingDirectories(g *WithT, actualRoot, expectedRoot string) { + g.Expect(actualRoot).To(BeADirectory()) + g.Expect(expectedRoot).To(BeADirectory()) actualonly, expectedonly, different := DiffDirectories(actualRoot, expectedRoot) - Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot) - Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot) + g.Expect(actualonly).To(BeEmpty(), "Expect no files in %s but not in %s", actualRoot, expectedRoot) + g.Expect(expectedonly).To(BeEmpty(), "Expect no files in %s but not in %s", expectedRoot, actualRoot) // these are enumerated, so that the output is the actual difference for _, diff := range different { - diff.FailedExpectation() + diff.FailedExpectation(g) } } type Diff interface { Path() string - FailedExpectation() + FailedExpectation(g *WithT) } type contentdiff struct { @@ -52,8 +52,8 @@ func (d contentdiff) Path() string { } // Run an expectation that will fail, giving an appropriate error -func (d contentdiff) FailedExpectation() { - Expect(d.actual).To(Equal(d.expected)) +func (d contentdiff) FailedExpectation(g *WithT) { + g.Expect(d.actual).To(Equal(d.expected)) } type dirfile struct { @@ -65,11 +65,11 @@ func (d dirfile) Path() string { return d.path } -func (d dirfile) FailedExpectation() { +func (d dirfile) FailedExpectation(g *WithT) { if d.expectedRegularFile { - Expect(d.path).To(BeARegularFile()) + g.Expect(d.path).To(BeARegularFile()) } else { - Expect(d.path).To(BeADirectory()) + g.Expect(d.path).To(BeADirectory()) } } diff --git a/pkg/test/files_test.go b/pkg/test/files_test.go index 1e270bb4..46ce50b2 100644 --- a/pkg/test/files_test.go +++ b/pkg/test/files_test.go @@ -19,41 +19,51 @@ package test import ( "testing" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" ) -func TestFiles(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Files comparison helper") +func TestExpectMatchingDirectories(t *testing.T) { + tests := []struct { + name string + actualRoot string + expectedRoot string + }{ + { + name: "same directory", + actualRoot: "testdata/base", + expectedRoot: "testdata/base", + }, + { + name: "different equivalent directories", + actualRoot: "testdata/base", + expectedRoot: "testdata/equiv", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + g := NewWithT(t) + ExpectMatchingDirectories(g, tt.actualRoot, tt.expectedRoot) + }) + } } -var _ = Describe("when no differences", 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") - }) -}) - -var _ = Describe("with differences", func() { - It("finds files in expected from a/ but not in actual b/", func() { - aonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") - Expect(aonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) - }) - - It("finds files in actual a/ that weren't expected from b/", func() { - bonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") // change in order - Expect(bonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) - }) - - It("finds files that are different in a and b", func() { - _, _, diffs := DiffDirectories("testdata/diff/a", "testdata/diff/b") - var diffpaths []string - for _, d := range diffs { - diffpaths = append(diffpaths, d.Path()) - } - Expect(diffpaths).To(Equal([]string{"/different/content.yaml", "/dirfile"})) - }) -}) +func TestDiffDirectories(t *testing.T) { + g := NewWithT(t) + + // Finds files in expected from a/ but not in actual b/. + aonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") + g.Expect(aonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) + + // Finds files in actual a/ that weren't expected from b/. + bonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") // change in order + g.Expect(bonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) + + // Finds files that are different in a and b. + _, _, diffs := DiffDirectories("testdata/diff/a", "testdata/diff/b") + var diffpaths []string + for _, d := range diffs { + diffpaths = append(diffpaths, d.Path()) + } + g.Expect(diffpaths).To(Equal([]string{"/different/content.yaml", "/dirfile"})) +} diff --git a/pkg/update/filereader_test.go b/pkg/update/filereader_test.go index e99b7c9f..8df6c588 100644 --- a/pkg/update/filereader_test.go +++ b/pkg/update/filereader_test.go @@ -17,36 +17,37 @@ limitations under the License. package update import ( + "testing" + "github.com/go-logr/logr" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "sigs.k8s.io/kustomize/kyaml/kio/kioutil" ) -var _ = Describe("load YAMLs with ScreeningLocalReader", func() { - It("loads only the YAMLs containing the token", func() { - r := ScreeningLocalReader{ - Path: "testdata/setters/original", - Token: "$imagepolicy", - Trace: logr.Discard(), - } - nodes, err := r.Read() - Expect(err).ToNot(HaveOccurred()) - // the test fixture has three files that contain the marker: - // - otherns.yaml - // - marked.yaml - // - kustomization.yaml - Expect(len(nodes)).To(Equal(3)) - filesSeen := map[string]struct{}{} - for i := range nodes { - path, _, err := kioutil.GetFileAnnotations(nodes[i]) - Expect(err).ToNot(HaveOccurred()) - filesSeen[path] = struct{}{} - } - Expect(filesSeen).To(Equal(map[string]struct{}{ - "marked.yaml": struct{}{}, - "kustomization.yaml": struct{}{}, - "otherns.yaml": struct{}{}, - })) - }) -}) +func TestScreeningLocalReader(t *testing.T) { + g := NewWithT(t) + r := ScreeningLocalReader{ + Path: "testdata/setters/original", + Token: "$imagepolicy", + Trace: logr.Discard(), + } + nodes, err := r.Read() + g.Expect(err).ToNot(HaveOccurred()) + // the test fixture has three files that contain the marker: + // - otherns.yaml + // - marked.yaml + // - kustomization.yaml + g.Expect(len(nodes)).To(Equal(3)) + filesSeen := map[string]struct{}{} + for i := range nodes { + path, _, err := kioutil.GetFileAnnotations(nodes[i]) + g.Expect(err).ToNot(HaveOccurred()) + filesSeen[path] = struct{}{} + } + g.Expect(filesSeen).To(Equal(map[string]struct{}{ + "marked.yaml": {}, + "kustomization.yaml": {}, + "otherns.yaml": {}, + })) + +} diff --git a/pkg/update/result_test.go b/pkg/update/result_test.go index e96205d9..0292470d 100644 --- a/pkg/update/result_test.go +++ b/pkg/update/result_test.go @@ -1,8 +1,9 @@ package update import ( + "testing" + "github.com/google/go-containerregistry/pkg/name" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/kustomize/kyaml/yaml" @@ -18,79 +19,76 @@ func mustRef(ref string) imageRef { return imageRef{r, types.NamespacedName{}} } -var _ = Describe("image ref", func() { - It("gives each component of an image ref", func() { +func TestMustRef(t *testing.T) { + g := NewWithT(t) + + t.Run("gives each component of an image ref", func(t *testing.T) { ref := mustRef("helloworld:v1.0.1") - Expect(ref.String()).To(Equal("helloworld:v1.0.1")) - Expect(ref.Identifier()).To(Equal("v1.0.1")) - Expect(ref.Repository()).To(Equal("library/helloworld")) - Expect(ref.Registry()).To(Equal("index.docker.io")) - Expect(ref.Name()).To(Equal("index.docker.io/library/helloworld:v1.0.1")) + g.Expect(ref.String()).To(Equal("helloworld:v1.0.1")) + g.Expect(ref.Identifier()).To(Equal("v1.0.1")) + g.Expect(ref.Repository()).To(Equal("library/helloworld")) + g.Expect(ref.Registry()).To(Equal("index.docker.io")) + g.Expect(ref.Name()).To(Equal("index.docker.io/library/helloworld:v1.0.1")) }) - It("deals with hostnames and digests", func() { + t.Run("deals with hostnames and digests", func(t *testing.T) { image := "localhost:5000/org/helloworld@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be" ref := mustRef(image) - Expect(ref.String()).To(Equal(image)) - Expect(ref.Identifier()).To(Equal("sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be")) - Expect(ref.Repository()).To(Equal("org/helloworld")) - Expect(ref.Registry()).To(Equal("localhost:5000")) - Expect(ref.Name()).To(Equal(image)) + g.Expect(ref.String()).To(Equal(image)) + g.Expect(ref.Identifier()).To(Equal("sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be")) + g.Expect(ref.Repository()).To(Equal("org/helloworld")) + g.Expect(ref.Registry()).To(Equal("localhost:5000")) + g.Expect(ref.Name()).To(Equal(image)) }) -}) +} -var _ = Describe("update results", func() { +func TestUpdateResults(t *testing.T) { + g := NewWithT(t) var result Result objectNames := []ObjectIdentifier{ - ObjectIdentifier{yaml.ResourceIdentifier{ + {yaml.ResourceIdentifier{ NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"}, }}, - ObjectIdentifier{yaml.ResourceIdentifier{ + {yaml.ResourceIdentifier{ NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"}, }}, } - BeforeEach(func() { - result = Result{ - Files: map[string]FileResult{ - "foo.yaml": { - Objects: map[ObjectIdentifier][]ImageRef{ - objectNames[0]: { - mustRef("image:v1.0"), - mustRef("other:v2.0"), - }, + result = Result{ + Files: map[string]FileResult{ + "foo.yaml": { + Objects: map[ObjectIdentifier][]ImageRef{ + objectNames[0]: { + mustRef("image:v1.0"), + mustRef("other:v2.0"), }, }, - "bar.yaml": { - Objects: map[ObjectIdentifier][]ImageRef{ - objectNames[1]: { - mustRef("image:v1.0"), - mustRef("other:v2.0"), - }, + }, + "bar.yaml": { + Objects: map[ObjectIdentifier][]ImageRef{ + objectNames[1]: { + mustRef("image:v1.0"), + mustRef("other:v2.0"), }, }, }, - } - }) + }, + } - It("deduplicates images", func() { - Expect(result.Images()).To(Equal([]ImageRef{ + g.Expect(result.Images()).To(Equal([]ImageRef{ + mustRef("image:v1.0"), + mustRef("other:v2.0"), + })) + + g.Expect(result.Objects()).To(Equal(map[ObjectIdentifier][]ImageRef{ + objectNames[0]: { mustRef("image:v1.0"), mustRef("other:v2.0"), - })) - }) - - It("collects images by object", func() { - Expect(result.Objects()).To(Equal(map[ObjectIdentifier][]ImageRef{ - objectNames[0]: { - mustRef("image:v1.0"), - mustRef("other:v2.0"), - }, - objectNames[1]: { - mustRef("image:v1.0"), - mustRef("other:v2.0"), - }, - })) - }) -}) + }, + objectNames[1]: { + mustRef("image:v1.0"), + mustRef("other:v2.0"), + }, + })) +} diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index 5be39f3c..486d47c3 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -17,12 +17,12 @@ limitations under the License. package update import ( + "io/ioutil" "os" "testing" "github.com/go-logr/logr" "github.com/google/go-containerregistry/pkg/name" - . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -32,116 +32,78 @@ import ( imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" ) -func TestUpdate(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Update suite") -} - -var _ = Describe("Update image via kyaml setters2", func() { +func TestUpdateWithSetters(t *testing.T) { + g := NewWithT(t) - var ( - policies = []imagev1_reflect.ImagePolicy{ - { - ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} - Namespace: "automation-ns", - Name: "policy", - }, - Status: imagev1_reflect.ImagePolicyStatus{ - LatestImage: "index.repo.fake/updated:v1.0.1", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} - Namespace: "automation-ns", - Name: "unchanged", - }, - Status: imagev1_reflect.ImagePolicyStatus{ - LatestImage: "image:v1.0.0", - }, - }, - } - ) - - It("updates the image marked with the image policy (setter) ref", func() { - tmp, err := os.MkdirTemp("", "gotest") - Expect(err).ToNot(HaveOccurred()) - defer os.RemoveAll(tmp) - - policies := []imagev1_reflect.ImagePolicy{ - { - ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} - Namespace: "automation-ns", - Name: "policy", - }, - Status: imagev1_reflect.ImagePolicyStatus{ - LatestImage: "index.repo.fake/updated:v1.0.1", - }, + policies := []imagev1_reflect.ImagePolicy{ + { + ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} + Namespace: "automation-ns", + Name: "policy", }, - { - ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} - Namespace: "automation-ns", - Name: "unchanged", - }, - Status: imagev1_reflect.ImagePolicyStatus{ - LatestImage: "image:v1.0.0", - }, + Status: imagev1_reflect.ImagePolicyStatus{ + LatestImage: "index.repo.fake/updated:v1.0.1", }, - } - - _, err = UpdateWithSetters(logr.Discard(), "testdata/setters/original", tmp, policies) - Expect(err).ToNot(HaveOccurred()) - test.ExpectMatchingDirectories(tmp, "testdata/setters/expected") - }) - - It("gives the result of the updates", func() { - tmp, err := os.MkdirTemp("", "gotest") - Expect(err).ToNot(HaveOccurred()) - defer os.RemoveAll(tmp) - - result, err := UpdateWithSetters(logr.Discard(), "testdata/setters/original", tmp, policies) - Expect(err).ToNot(HaveOccurred()) - - kustomizeResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ - TypeMeta: yaml.TypeMeta{ - APIVersion: "kustomize.config.k8s.io/v1beta1", - Kind: "Kustomization", - }, - }} - markedResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ - TypeMeta: yaml.TypeMeta{ - APIVersion: "batch/v1beta1", - Kind: "CronJob", + }, + { + ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected} + Namespace: "automation-ns", + Name: "unchanged", }, - NameMeta: yaml.NameMeta{ - Namespace: "bar", - Name: "foo", + Status: imagev1_reflect.ImagePolicyStatus{ + LatestImage: "image:v1.0.0", }, - }} - r, _ := name.ParseReference("index.repo.fake/updated:v1.0.1") - expectedImageRef := imageRef{r, types.NamespacedName{ - Name: "policy", - Namespace: "automation-ns", - }} - - expectedResult := Result{ - Files: map[string]FileResult{ - "kustomization.yaml": { - Objects: map[ObjectIdentifier][]ImageRef{ - kustomizeResourceID: { - expectedImageRef, - }, + }, + } + + tmp, err := ioutil.TempDir("", "gotest") + g.Expect(err).ToNot(HaveOccurred()) + defer os.RemoveAll(tmp) + + result, err := UpdateWithSetters(logr.Discard(), "testdata/setters/original", tmp, policies) + g.Expect(err).ToNot(HaveOccurred()) + test.ExpectMatchingDirectories(g, tmp, "testdata/setters/expected") + + kustomizeResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ + TypeMeta: yaml.TypeMeta{ + APIVersion: "kustomize.config.k8s.io/v1beta1", + Kind: "Kustomization", + }, + }} + markedResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ + TypeMeta: yaml.TypeMeta{ + APIVersion: "batch/v1beta1", + Kind: "CronJob", + }, + NameMeta: yaml.NameMeta{ + Namespace: "bar", + Name: "foo", + }, + }} + r, _ := name.ParseReference("index.repo.fake/updated:v1.0.1") + expectedImageRef := imageRef{r, types.NamespacedName{ + Name: "policy", + Namespace: "automation-ns", + }} + + expectedResult := Result{ + Files: map[string]FileResult{ + "kustomization.yaml": { + Objects: map[ObjectIdentifier][]ImageRef{ + kustomizeResourceID: { + expectedImageRef, }, }, - "marked.yaml": { - Objects: map[ObjectIdentifier][]ImageRef{ - markedResourceID: { - expectedImageRef, - }, + }, + "marked.yaml": { + Objects: map[ObjectIdentifier][]ImageRef{ + markedResourceID: { + expectedImageRef, }, }, }, - } + }, + } - Expect(result).To(Equal(expectedResult)) - }) -}) + g.Expect(result).To(Equal(expectedResult)) +} From 5affa3a34b0b2dc0957202859501446f1f39575c Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Mon, 18 Oct 2021 14:35:45 +0100 Subject: [PATCH 3/5] Correct directory diffing test and algorithm Two steps: 1. TestDiffDirectories did not check if the expected only return value was correct; the intention was there to do so (judging by the comment "change in order"), but my eye for detail failed me. 2. Reversing the directory comparison in the test revealed bugs in the comparison code -- in general, it should skip any directory that is not a directory in the comparator. To make this easier, the code now keeps track of the expected files it saw. That means the check for whether an actual file has an expected counterpart only has two cases, yes or no (rather that trying to account for whether it's a directory and so on). If a directory was skipped while scanning the expected files, it won't be in the actual files anyway. Signed-off-by: Michael Bridgen --- pkg/test/files.go | 33 +++++++++++++++++++-------------- pkg/test/files_test.go | 12 ++++++------ 2 files changed, 25 insertions(+), 20 deletions(-) diff --git a/pkg/test/files.go b/pkg/test/files.go index a3d7d7c0..891f4b63 100644 --- a/pkg/test/files.go +++ b/pkg/test/files.go @@ -82,16 +82,21 @@ func (d dirfile) FailedExpectation(g *WithT) { // `foo.yaml~`). It panics if it encounters any error apart from a // file not found. func DiffDirectories(actual, expected string) (actualonly []string, expectedonly []string, different []Diff) { + seen := make(map[string]struct{}) + filepath.Walk(expected, func(expectedPath string, expectedInfo os.FileInfo, err error) error { if err != nil { panic(err) } + + relPath := expectedPath[len(expected):] + seen[relPath] = struct{}{} + // ignore emacs backups if strings.HasSuffix(expectedPath, "~") { return nil } - relPath := expectedPath[len(expected):] - actualPath := filepath.Join(actual, relPath) + // ignore dotfiles if strings.HasPrefix(filepath.Base(expectedPath), ".") { if expectedInfo.IsDir() { @@ -100,24 +105,30 @@ func DiffDirectories(actual, expected string) (actualonly []string, expectedonly return nil } + actualPath := filepath.Join(actual, relPath) actualInfo, err := os.Stat(actualPath) switch { case err == nil: break case os.IsNotExist(err): expectedonly = append(expectedonly, relPath) + if expectedInfo.IsDir() { + return filepath.SkipDir + } return nil default: panic(err) } // file exists in both places - switch { case actualInfo.IsDir() && expectedInfo.IsDir(): return nil // i.e., keep recursing case actualInfo.IsDir() || expectedInfo.IsDir(): different = append(different, dirfile{path: relPath, abspath: actualPath, expectedRegularFile: actualInfo.IsDir()}) + if expectedInfo.IsDir() { + return filepath.SkipDir + } return nil } @@ -151,18 +162,12 @@ func DiffDirectories(actual, expected string) (actualonly []string, expectedonly if actualInfo.IsDir() && strings.HasPrefix(filepath.Base(actualPath), ".") { return filepath.SkipDir } - // since I've already compared any file that exists in - // expected or both, I'm only concerned with files that appear - // in actual but not in expected. - expectedPath := filepath.Join(expected, relPath) - _, err = os.Stat(expectedPath) - switch { - case err == nil: - break - case os.IsNotExist(err): + + if _, ok := seen[relPath]; !ok { actualonly = append(actualonly, relPath) - default: - panic(err) + if actualInfo.IsDir() { + return filepath.SkipDir + } } return nil }) diff --git a/pkg/test/files_test.go b/pkg/test/files_test.go index 46ce50b2..b5e5f37d 100644 --- a/pkg/test/files_test.go +++ b/pkg/test/files_test.go @@ -51,13 +51,13 @@ func TestExpectMatchingDirectories(t *testing.T) { func TestDiffDirectories(t *testing.T) { g := NewWithT(t) - // Finds files in expected from a/ but not in actual b/. - aonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") - g.Expect(aonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) - // Finds files in actual a/ that weren't expected from b/. - bonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") // change in order - g.Expect(bonly).To(Equal([]string{"/only", "/only/here.yaml", "/onlyhere.yaml"})) + actualonly, _, _ := DiffDirectories("testdata/diff/a", "testdata/diff/b") + g.Expect(actualonly).To(Equal([]string{"/only", "/onlyhere.yaml"})) + + // Finds files in expected from a/ but not in actual b/. + _, expectedonly, _ := DiffDirectories("testdata/diff/b", "testdata/diff/a") // NB change in order + g.Expect(expectedonly).To(Equal([]string{"/only", "/onlyhere.yaml"})) // Finds files that are different in a and b. _, _, diffs := DiffDirectories("testdata/diff/a", "testdata/diff/b") From 37022d0c88826a4d85daa9589c857b02434f3f1f Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 25 Apr 2022 19:44:15 +0530 Subject: [PATCH 4/5] Refactor update_test.go to use testenv Signed-off-by: Sunny --- controllers/update_test.go | 2070 +++++++++++++++++------------------- 1 file changed, 954 insertions(+), 1116 deletions(-) diff --git a/controllers/update_test.go b/controllers/update_test.go index b0ad87c1..0264f0d1 100644 --- a/controllers/update_test.go +++ b/controllers/update_test.go @@ -28,24 +28,25 @@ import ( "path" "path/filepath" "strings" + "testing" "time" securejoin "github.com/cyphar/filepath-securejoin" + "github.com/go-logr/logr" git2go "github.com/libgit2/git2go/v33" libgit2 "github.com/libgit2/git2go/v33" + . "github.com/onsi/gomega" "github.com/otiai10/copy" "golang.org/x/crypto/openpgp" "golang.org/x/crypto/openpgp/armor" - - "github.com/go-logr/logr" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" apimeta "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" "github.com/fluxcd/pkg/apis/acl" @@ -59,76 +60,11 @@ import ( "github.com/fluxcd/image-automation-controller/pkg/update" ) -const timeout = 10 * time.Second - -var ( - // Copied from - // https://github.com/fluxcd/source-controller/blob/master/controllers/suite_test.go - letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") - - gitServer *gittestserver.GitServer - - repositoryPath string -) - -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 ( - branch string - namespace *corev1.Namespace - username, password string - authorName = "Flux B Ot" - authorEmail = "fluxbot@example.com" - ) - - // Start the git server - BeforeEach(func() { - branch = randStringRunes(8) - 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 = gittestserver.NewTempGitServer() - Expect(err).NotTo(HaveOccurred()) - username = randStringRunes(5) - password = randStringRunes(5) - // using authentication makes using the server more fiddly in - // general, but is required for testing SSH. - gitServer.Auth(username, password) - gitServer.AutoCreate() - Expect(gitServer.StartHTTP()).To(Succeed()) - gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys")) - Expect(gitServer.ListenSSH()).To(Succeed()) - }) - - AfterEach(func() { - gitServer.StopHTTP() - os.RemoveAll(gitServer.Root()) - }) - - It("Initialises git OK", func() { - Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) - }) - - Context("commit spec", func() { - - var ( - localRepo *git2go.Repository - commitMessage string - ) - - const ( - commitTemplate = `Commit summary +const ( + timeout = 10 * time.Second + testAuthorName = "Flux B Ot" + testAuthorEmail = "fluxbot@example.com" + testCommitTemplate = `Commit summary Automation: {{ .AutomationObject }} @@ -151,7 +87,7 @@ Images: - {{.}} ({{.Policy.Name}}) {{ end -}} ` - commitMessageFmt = `Commit summary + testCommitMessageFmt = `Commit summary Automation: %s/update-test @@ -162,1135 +98,716 @@ Objects: Images: - helloworld:v1.0.0 (%s) ` - ) - - BeforeEach(func() { - Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) - repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath - var err error - localRepo, err = clone(repoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) - - gitRepoKey := types.NamespacedName{ - Name: "image-auto-" + randStringRunes(5), - Namespace: namespace.Name, - } - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, - Namespace: namespace.Name, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - }, - } - Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) - 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 := &imagev1_reflect.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: imagev1_reflect.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: "not-expected-to-exist", - }, - Policy: imagev1_reflect.ImagePolicyChoice{ - SemVer: &imagev1_reflect.SemVerPolicy{ - Range: "1.x", - }, - }, - }, - } - Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) - policy.Status.LatestImage = "helloworld:v1.0.0" - Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed()) +) - // Format the expected message given the generated values - commitMessage = fmt.Sprintf(commitMessageFmt, namespace.Name, policyKey.Name) +var ( + // Copied from + // https://github.com/fluxcd/source-controller/blob/master/controllers/suite_test.go + letterRunes = []rune("abcdefghijklmnopqrstuvwxyz1234567890") - // Insert a setter reference into the deployment file, - // before creating the automation object itself. - commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) - }) + gitServer *gittestserver.GitServer - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) + repositoryPath string +) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - waitForNewHead(localRepo, branch, preChangeCommitId) +func randStringRunes(n int) string { + b := make([]rune, n) + for i := range b { + b[i] = letterRunes[rand.Intn(len(letterRunes))] + } + return string(b) +} - // now create the automation object, and let it (one - // hopes!) make a commit itself. - updateKey := types.NamespacedName{ - Namespace: namespace.Name, - Name: "update-test", - } - updateBySetters := &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - MessageTemplate: commitTemplate, - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - }, - }, - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - }, - }, +func TestImageAutomationReconciler_commitMessage(t *testing.T) { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + } + fixture := "testdata/appconfig" + latest := "helloworld:v1.0.0" + + testWithRepoAndImagePolicy( + NewWithT(t), testEnv, fixture, policySpec, latest, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) { + commitMessage := fmt.Sprintf(testCommitMessageFmt, s.namespace, s.imagePolicyName) + + // Update the setter marker in the repo. + policyKey := types.NamespacedName{ + Name: s.imagePolicyName, + Namespace: s.namespace, } + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) + }) - // pull the head commit we just pushed, so it's not + // Pull the head commit we just pushed, so it's not // considered a new commit when checking for a commit // made by automation. - preChangeCommitId = commitIdFromBranch(localRepo, branch) - Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) - // wait for a new commit to be made by the controller - waitForNewHead(localRepo, branch, preChangeCommitId) - }) + preChangeCommitId := commitIdFromBranch(localRepo, s.branch) - AfterEach(func() { - imageAutoReconciler.NoCrossNamespaceRef = false - Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) - }) + // Pull the head commit that was just pushed, so it's not considered a new + // commit when checking for a commit made by automation. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - It("formats the commit message as in the template", func() { - head, _ := headCommit(localRepo) - commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(commit.Message()).To(Equal(commitMessage)) - }) + preChangeCommitId = commitIdFromBranch(localRepo, s.branch) + + // Create the automation object and let it make a commit itself. + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - It("has the commit author as given", func() { head, _ := headCommit(localRepo) commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(commit.Message()).To(Equal(commitMessage)) signature := commit.Author() - Expect(signature).NotTo(BeNil()) - Expect(signature.Name).To(Equal(authorName)) - Expect(signature.Email).To(Equal(authorEmail)) - }) - }) - - Context("ref cross-ns GitRepository", func() { - var ( - localRepo *git2go.Repository - commitMessage string - updateBySetters *imagev1.ImageUpdateAutomation - ) - - const ( - authorName = "Flux B Ot" - authorEmail = "fluxbot@example.com" - commitTemplate = `Commit summary - -Automation: {{ .AutomationObject }} - -Files: -{{ range $filename, $_ := .Updated.Files -}} -- {{ $filename }} -{{ end -}} - -Objects: -{{ range $resource, $_ := .Updated.Objects -}} -{{ if eq $resource.Kind "Deployment" -}} -- {{ $resource.Kind | lower }} {{ $resource.Name | lower }} -{{ else -}} -- {{ $resource.Kind }} {{ $resource.Name }} -{{ end -}} -{{ end -}} - -Images: -{{ range .Updated.Images -}} -- {{.}} ({{.Policy.Name}}) -{{ end -}} -` - commitMessageFmt = `Commit summary - -Automation: %s/update-test + g.Expect(signature).NotTo(BeNil()) + g.Expect(signature.Name).To(Equal(testAuthorName)) + g.Expect(signature.Email).To(Equal(testAuthorEmail)) + }, + ) +} -Files: -- deploy.yaml -Objects: -- deployment test -Images: -- helloworld:v1.0.0 (%s) -` - ) - - BeforeEach(func() { - Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) - repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath - var err error - localRepo, err = clone(repoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) - - // A different namespace for the GitRepository. - gitRepoNamespace := &corev1.Namespace{} - gitRepoNamespace.Name = "cross-ns-git-repo" + randStringRunes(5) - Expect(k8sClient.Create(context.Background(), gitRepoNamespace)).To(Succeed()) - - gitRepoKey := types.NamespacedName{ - Name: "image-auto-" + randStringRunes(5), - Namespace: gitRepoNamespace.Name, - } - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - }, - } - Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) +func TestImageAutomationReconciler_crossNamespaceRef(t *testing.T) { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + } + fixture := "testdata/appconfig" + latest := "helloworld:v1.0.0" + + // Test successful cross namespace reference when NoCrossNamespaceRef=false. + args := newRepoAndPolicyArgs() + args.gitRepoNamespace = "cross-ns-git-repo" + randStringRunes(5) + testWithCustomRepoAndImagePolicy( + NewWithT(t), testEnv, fixture, policySpec, latest, args, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) { + commitMessage := fmt.Sprintf(testCommitMessageFmt, s.namespace, s.imagePolicyName) + + // Update the setter marker in the repo. policyKey := types.NamespacedName{ - Name: "policy-" + randStringRunes(5), - Namespace: namespace.Name, + Name: s.imagePolicyName, + Namespace: s.namespace, } - // NB not testing the image reflector controller; this - // will make a "fully formed" ImagePolicy object. - policy := &imagev1_reflect.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: imagev1_reflect.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: "not-expected-to-exist", - }, - Policy: imagev1_reflect.ImagePolicyChoice{ - SemVer: &imagev1_reflect.SemVerPolicy{ - Range: "1.x", - }, - }, - }, - } - Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) - policy.Status.LatestImage = "helloworld:v1.0.0" - Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed()) - - // Format the expected message given the generated values - commitMessage = fmt.Sprintf(commitMessageFmt, namespace.Name, policyKey.Name) - - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) - - // Insert a setter reference into the deployment file, - // before creating the automation object itself. - commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) }) - // pull the head commit we just pushed, so it's not + // Pull the head commit we just pushed, so it's not // considered a new commit when checking for a commit // made by automation. - waitForNewHead(localRepo, branch, preChangeCommitId) + preChangeCommitId := commitIdFromBranch(localRepo, s.branch) - // now create the automation object, and let it (one - // hopes!) make a commit itself. - updateKey := types.NamespacedName{ - Namespace: namespace.Name, - Name: "update-test", - } - updateBySetters = &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - MessageTemplate: commitTemplate, - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - }, - }, - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - }, - }, - } + // Pull the head commit that was just pushed, so it's not considered a new + // commit when checking for a commit made by automation. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId = commitIdFromBranch(localRepo, branch) - Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) - // wait for a new commit to be made by the controller - waitForNewHead(localRepo, branch, preChangeCommitId) - }) + preChangeCommitId = commitIdFromBranch(localRepo, s.branch) - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) - }) + // Create the automation object and let it make a commit itself. + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) - It("formats the commit message as in the template", func() { - head, _ := headCommit(localRepo) - commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(commit.Message()).To(Equal(commitMessage)) - }) + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - It("has the commit author as given", func() { head, _ := headCommit(localRepo) commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(commit.Message()).To(Equal(commitMessage)) signature := commit.Author() - Expect(signature).NotTo(BeNil()) - Expect(signature.Name).To(Equal(authorName)) - Expect(signature.Email).To(Equal(authorEmail)) - }) - - It("fails to reconcile if cross-namespace flag is set", func() { - imageAutoReconciler.NoCrossNamespaceRef = true - - // trigger reconcile - var updatePatch imagev1.ImageUpdateAutomation - Expect(k8sClient.Get(context.TODO(), client.ObjectKeyFromObject(updateBySetters), &updatePatch)).To(Succeed()) - updatePatch.Spec.Interval = metav1.Duration{Duration: 5 * time.Minute} - Expect(k8sClient.Patch(context.Background(), &updatePatch, client.Merge)).To(Succeed()) - - resultAuto := &imagev1.ImageUpdateAutomation{} - var readyCondition *metav1.Condition - - Eventually(func() bool { - _ = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(updateBySetters), resultAuto) - readyCondition = apimeta.FindStatusCondition(resultAuto.Status.Conditions, meta.ReadyCondition) - return apimeta.IsStatusConditionFalse(resultAuto.Status.Conditions, meta.ReadyCondition) - }, timeout, time.Second).Should(BeTrue()) - - Expect(readyCondition).ToNot(BeNil()) - Expect(readyCondition.Reason).To(Equal(acl.AccessDeniedReason)) - }) - }) - - Context("update path", func() { + g.Expect(signature).NotTo(BeNil()) + g.Expect(signature.Name).To(Equal(testAuthorName)) + g.Expect(signature.Email).To(Equal(testAuthorEmail)) + }, + ) - var localRepo *git2go.Repository - const commitTemplate = `Commit summary + // Test cross namespace reference failure when NoCrossNamespaceRef=true. + builder := fakeclient.NewClientBuilder().WithScheme(testEnv.Scheme()) + r := &ImageUpdateAutomationReconciler{ + Client: builder.Build(), + Scheme: scheme.Scheme, + EventRecorder: testEnv.GetEventRecorderFor("image-automation-controller"), + NoCrossNamespaceRef: true, + } + args = newRepoAndPolicyArgs() + args.gitRepoNamespace = "cross-ns-git-repo" + randStringRunes(5) + testWithCustomRepoAndImagePolicy( + NewWithT(t), r.Client, fixture, policySpec, latest, args, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) { + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } + err := createImageUpdateAutomation(r.Client, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) -{{ range $resource, $_ := .Updated.Objects -}} -- {{ $resource.Name }} -{{ end -}} -` + imageUpdateKey := types.NamespacedName{ + Name: "update-test", + Namespace: s.namespace, + } + _, err = r.Reconcile(context.TODO(), ctrl.Request{NamespacedName: imageUpdateKey}) + g.Expect(err).To(BeNil()) - BeforeEach(func() { - Expect(initGitRepo(gitServer, "testdata/pathconfig", branch, repositoryPath)).To(Succeed()) - repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath - var err error - localRepo, err = clone(repoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) + var imageUpdate imagev1.ImageUpdateAutomation + _ = r.Client.Get(context.TODO(), imageUpdateKey, &imageUpdate) + ready := apimeta.FindStatusCondition(imageUpdate.Status.Conditions, meta.ReadyCondition) + g.Expect(ready.Reason).To(Equal(acl.AccessDeniedReason)) + }, + ) +} - gitRepoKey := types.NamespacedName{ - Name: "image-auto-" + randStringRunes(5), - Namespace: namespace.Name, - } - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, - Namespace: namespace.Name, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - }, - } - Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) +func TestImageAutomationReconciler_updatePath(t *testing.T) { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + } + fixture := "testdata/pathconfig" + latest := "helloworld:v1.0.0" + testWithRepoAndImagePolicy( + NewWithT(t), testEnv, fixture, policySpec, latest, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) { + // Update the setter marker in the repo. 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 := &imagev1_reflect.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: imagev1_reflect.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: "not-expected-to-exist", - }, - Policy: imagev1_reflect.ImagePolicyChoice{ - SemVer: &imagev1_reflect.SemVerPolicy{ - Range: "1.x", - }, - }, - }, + Name: s.imagePolicyName, + Namespace: s.namespace, } - Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) - policy.Status.LatestImage = "helloworld:v1.0.0" - Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed()) // pull the head commit we just pushed, so it's not // considered a new commit when checking for a commit // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) + preChangeCommitId := commitIdFromBranch(localRepo, s.branch) - // Insert a setter reference into the deployment file, - // before creating the automation object itself. - commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(path.Join(tmp, "yes"), policyKey)).To(Succeed()) + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(path.Join(tmp, "yes"), policyKey)).To(Succeed()) }) - commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(path.Join(tmp, "no"), policyKey)).To(Succeed()) + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(path.Join(tmp, "no"), policyKey)).To(Succeed()) }) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - waitForNewHead(localRepo, branch, preChangeCommitId) + // Pull the head commit that was just pushed, so it's not considered a new + // commit when checking for a commit made by automation. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - // now create the automation object, and let it (one - // hopes!) make a commit itself. - updateKey := types.NamespacedName{ - Namespace: namespace.Name, - Name: "update-test", - } - updateBySetters := &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - Path: "./yes", - }, - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - MessageTemplate: commitTemplate, - }, - }, - }, - } + preChangeCommitId = commitIdFromBranch(localRepo, s.branch) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId = commitIdFromBranch(localRepo, branch) - Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) - // wait for a new commit to be made by the controller - waitForNewHead(localRepo, branch, preChangeCommitId) - }) + // Create the automation object and let it make a commit itself. + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + Path: "./yes", + } + err := createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) - }) + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - It("updates only the deployment in the specified path", func() { head, _ := headCommit(localRepo) commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(commit.Message()).To(Not(ContainSubstring("update-no"))) - Expect(commit.Message()).To(ContainSubstring("update-yes")) - }) - }) - - Context("commit signing", func() { - - var ( - localRepo *git2go.Repository - pgpEntity *openpgp.Entity - ) - - BeforeEach(func() { - Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) - repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath - var err error - localRepo, err = clone(repoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(commit.Message()).ToNot(ContainSubstring("update-no")) + g.Expect(commit.Message()).To(ContainSubstring("update-yes")) + }, + ) +} - gitRepoKey := types.NamespacedName{ - Name: "image-auto-" + randStringRunes(5), - Namespace: namespace.Name, - } - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, - Namespace: namespace.Name, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - }, - } - Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) +func TestImageAutomationReconciler_signedCommit(t *testing.T) { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + } + fixture := "testdata/appconfig" + latest := "helloworld:v1.0.0" + + testWithRepoAndImagePolicy( + NewWithT(t), testEnv, fixture, policySpec, latest, + func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) { + signingKeySecretName := "signing-key-secret-" + randStringRunes(5) + // Update the setter marker in the repo. policyKey := types.NamespacedName{ - Name: "policy-" + randStringRunes(5), - Namespace: namespace.Name, + Name: s.imagePolicyName, + Namespace: s.namespace, } - // NB not testing the image reflector controller; this - // will make a "fully formed" ImagePolicy object. - policy := &imagev1_reflect.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: imagev1_reflect.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: "not-expected-to-exist", - }, - Policy: imagev1_reflect.ImagePolicyChoice{ - SemVer: &imagev1_reflect.SemVerPolicy{ - Range: "1.x", - }, - }, - }, - } - Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) - policy.Status.LatestImage = "helloworld:v1.0.0" - Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed()) - - // Insert a setter reference into the deployment file, - // before creating the automation object itself. - commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) + commitInRepo(g, repoURL, s.branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) }) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) + preChangeCommitId := commitIdFromBranch(localRepo, s.branch) - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - waitForNewHead(localRepo, branch, preChangeCommitId) - - // generate keypair for signing - pgpEntity, err = openpgp.NewEntity("", "", "", nil) - Expect(err).ToNot(HaveOccurred()) - - // configure OpenPGP armor encoder - b := bytes.NewBuffer(nil) - w, err := armor.Encode(b, openpgp.PrivateKeyType, nil) - Expect(err).ToNot(HaveOccurred()) - - // serialize private key - err = pgpEntity.SerializePrivate(w, nil) - Expect(err).ToNot(HaveOccurred()) - err = w.Close() - Expect(err).ToNot(HaveOccurred()) - - // create the secret containing signing key - sec := &corev1.Secret{ - Data: map[string][]byte{ - "git.asc": b.Bytes(), - }, - } - sec.Name = "signing-key-secret-" + randStringRunes(5) - sec.Namespace = namespace.Name - Expect(k8sClient.Create(context.Background(), sec)).To(Succeed()) + // Pull the head commit that was just pushed, so it's not considered a new + // commit when checking for a commit made by automation. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - // now create the automation object, and let it (one - // hopes!) make a commit itself. - updateKey := types.NamespacedName{ - Namespace: namespace.Name, - Name: "update-test", - } - updateBySetters := &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - SigningKey: &imagev1.SigningKey{ - SecretRef: meta.LocalObjectReference{Name: sec.Name}, - }, - }, - }, - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - }, - }, - } + pgpEntity, err := createSigningKeyPair(testEnv, signingKeySecretName, s.namespace) + g.Expect(err).ToNot(HaveOccurred(), "failed to create signing key pair") - preChangeCommitId = commitIdFromBranch(localRepo, branch) - Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) - // wait for a new commit to be made by the controller - waitForNewHead(localRepo, branch, preChangeCommitId) - }) + preChangeCommitId = commitIdFromBranch(localRepo, s.branch) - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) - }) + // Create the automation object and let it make a commit itself. + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } + err = createImageUpdateAutomation(testEnv, "update-test", s.namespace, s.gitRepoName, s.gitRepoNamespace, s.branch, "", testCommitTemplate, signingKeySecretName, updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, s.branch, preChangeCommitId) - It("signs the commit with the generated GPG key", func() { head, _ := headCommit(localRepo) + g.Expect(err).ToNot(HaveOccurred()) commit, err := localRepo.LookupCommit(head.Id()) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) - // verify commit commitSig, commitContent, err := commit.ExtractSignature() - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) kr := openpgp.EntityList([]*openpgp.Entity{pgpEntity}) signature := strings.NewReader(commitSig) content := strings.NewReader(commitContent) _, err = openpgp.CheckArmoredDetachedSignature(kr, content, signature) - Expect(err).ToNot(HaveOccurred()) - }) - }) + g.Expect(err).ToNot(HaveOccurred()) + }, + ) +} - endToEnd := func(impl, proto string) func() { - return func() { - var ( - // for cloning locally - cloneLocalRepoURL string - // for the controller - repoURL string - localRepo *git2go.Repository - policy *imagev1_reflect.ImagePolicy - policyKey types.NamespacedName - gitRepoKey types.NamespacedName - commitMessage string - ) - - const latestImage = "helloworld:1.0.1" - - BeforeEach(func() { - cloneLocalRepoURL = gitServer.HTTPAddressWithCredentials() + repositoryPath - if proto == "http" { - repoURL = cloneLocalRepoURL // NB not testing auth for git over HTTP - } else if proto == "ssh" { - sshURL := gitServer.SSHAddress() - // this is expected to use 127.0.0.1, but host key - // checking usually wants a hostname, so use - // "localhost". - sshURL = strings.Replace(sshURL, "127.0.0.1", "localhost", 1) - repoURL = sshURL + repositoryPath - go func() { - defer GinkgoRecover() - gitServer.StartSSH() - }() - } else { - Fail("proto not set to http or ssh") - } +func TestImageAutomationReconciler_e2e(t *testing.T) { + gitImpls := []string{sourcev1.GoGitImplementation, sourcev1.LibGit2Implementation} + protos := []string{"http", "ssh"} - commitMessage = "Commit a difference " + randStringRunes(5) + testFunc := func(t *testing.T, proto string, impl string) { + g := NewWithT(t) - Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) + const latestImage = "helloworld:1.0.1" - var err error - localRepo, err = clone(cloneLocalRepoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) + namespace := "image-auto-test-" + randStringRunes(5) + branch := randStringRunes(8) + repositoryPath := "/config-" + randStringRunes(6) + ".git" + gitRepoName := "image-auto-" + randStringRunes(5) + gitSecretName := "git-secret-" + randStringRunes(5) + imagePolicyName := "policy-" + randStringRunes(5) + updateStrategy := &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + } - gitRepoKey = types.NamespacedName{ - Name: "image-auto-" + randStringRunes(5), - Namespace: namespace.Name, - } + // Create a test namespace. + nsCleanup, err := createNamespace(testEnv, namespace) + g.Expect(err).ToNot(HaveOccurred(), "failed to create test namespace") + defer func() { + g.Expect(nsCleanup()).To(Succeed()) + }() + + // Create git server. + gitServer, err := setupGitTestServer() + g.Expect(err).ToNot(HaveOccurred(), "failed to create test git server") + defer os.RemoveAll(gitServer.Root()) + defer gitServer.StopHTTP() + + cloneLocalRepoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath + repoURL, err := getRepoURL(gitServer, repositoryPath, proto) + g.Expect(err).ToNot(HaveOccurred()) + + // Start the ssh server if needed. + if proto == "ssh" { + // NOTE: Check how this is done in source-controller. + go func() { + gitServer.StartSSH() + }() + defer func() { + g.Expect(gitServer.StopSSH()).To(Succeed()) + }() + } - gitRepo := &sourcev1.GitRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: gitRepoKey.Name, - Namespace: namespace.Name, - }, - Spec: sourcev1.GitRepositorySpec{ - URL: repoURL, - Interval: metav1.Duration{Duration: time.Minute}, - GitImplementation: impl, - }, - } + commitMessage := "Commit a difference " + randStringRunes(5) + + // Initialize a git repo. + g.Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed()) + + // Create GitRepository resource for the above repo. + if proto == "ssh" { + // SSH requires an identity (private key) and known_hosts file + // in a secret. + err = createSSHIdentitySecret(testEnv, gitSecretName, namespace, repoURL) + g.Expect(err).ToNot(HaveOccurred()) + err = createGitRepository(testEnv, gitRepoName, namespace, impl, repoURL, gitSecretName) + g.Expect(err).ToNot(HaveOccurred()) + } else { + err = createGitRepository(testEnv, gitRepoName, namespace, impl, repoURL, "") + g.Expect(err).ToNot(HaveOccurred()) + } - // If using SSH, we need to provide an identity (private - // key) and known_hosts file in a secret. - if proto == "ssh" { - url, err := url.Parse(repoURL) - Expect(err).ToNot(HaveOccurred()) - knownhosts, err := ssh.ScanHostKey(url.Host, 5*time.Second) - Expect(err).ToNot(HaveOccurred()) - keygen := ssh.NewRSAGenerator(2048) - pair, err := keygen.Generate() - Expect(err).ToNot(HaveOccurred()) - - sec := &corev1.Secret{ - StringData: map[string]string{ - "known_hosts": string(knownhosts), - "identity": string(pair.PrivateKey), - "identity.pub": string(pair.PublicKey), - }, - } - sec.Name = "git-secret-" + randStringRunes(5) - sec.Namespace = namespace.Name - Expect(k8sClient.Create(context.Background(), sec)).To(Succeed()) - gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: sec.Name} - } + // Create an image policy. + policyKey := types.NamespacedName{ + Name: imagePolicyName, + Namespace: namespace, + } - Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed()) + // Create ImagePolicy and ImageUpdateAutomation resource for each of the + // test cases and cleanup at the end. - 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 = &imagev1_reflect.ImagePolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: policyKey.Name, - Namespace: policyKey.Namespace, - }, - Spec: imagev1_reflect.ImagePolicySpec{ - ImageRepositoryRef: meta.NamespacedObjectReference{ - Name: "not-expected-to-exist", - }, - Policy: imagev1_reflect.ImagePolicyChoice{ - SemVer: &imagev1_reflect.SemVerPolicy{ - Range: "1.x", - }, - }, - }, - } - Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) - policy.Status.LatestImage = latestImage - Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed()) + t.Run("PushSpec", func(t *testing.T) { + // Clone the repo locally. + localRepo, err := clone(cloneLocalRepoURL, "origin", branch) + g.Expect(err).ToNot(HaveOccurred(), "failed to clone git repo") - }) + // NB not testing the image reflector controller; this + // will make a "fully formed" ImagePolicy object. + err = createImagePolicyWithLatestImage(testEnv, imagePolicyName, namespace, "not-expected-to-exist", "1.x", latestImage) + g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource") - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) - Expect(k8sClient.Delete(context.Background(), policy)).To(Succeed()) - Expect(gitServer.StopSSH()).To(Succeed()) - }) + defer func() { + g.Expect(deleteImagePolicy(testEnv, imagePolicyName, namespace)).ToNot(HaveOccurred()) + }() - Context("with PushSpec", func() { - - var ( - update *imagev1.ImageUpdateAutomation - pushBranch string - ) - - BeforeEach(func() { - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) - commitInRepo(cloneLocalRepoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) - }) - waitForNewHead(localRepo, branch, preChangeCommitId) - - pushBranch = "pr-" + randStringRunes(5) - - update = &imagev1.ImageUpdateAutomation{ - Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - }, - Interval: metav1.Duration{Duration: 2 * time.Hour}, - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - MessageTemplate: commitMessage, - }, - Push: &imagev1.PushSpec{ - Branch: pushBranch, - }, - }, - }, - } - update.Name = "update-" + randStringRunes(5) - update.Namespace = namespace.Name - - Expect(k8sClient.Create(context.Background(), update)).To(Succeed()) - }) + imageUpdateAutomationName := "update-" + randStringRunes(5) + pushBranch := "pr-" + randStringRunes(5) - It("creates and pushes the push branch", func() { - initialHead, err := headFromBranch(localRepo, branch) - Expect(err).ToNot(HaveOccurred()) - defer initialHead.Free() - - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) - - waitForNewHead(localRepo, pushBranch, preChangeCommitId) - - head, err := getRemoteHead(localRepo, pushBranch) - Expect(err).NotTo(HaveOccurred()) - commit, err := localRepo.LookupCommit(head) - Expect(err).ToNot(HaveOccurred()) - defer commit.Free() - Expect(commit.Message()).To(Equal(commitMessage)) - - // previous commits should still exist in the tree. - // regression check to ensure previous commits were not squashed. - oldCommit, err := localRepo.LookupCommit(initialHead.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(oldCommit).ToNot(BeNil()) + t.Run("update with PushSpec", func(t *testing.T) { + preChangeCommitId := commitIdFromBranch(localRepo, branch) + commitInRepo(g, cloneLocalRepoURL, branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) }) + // Pull the head commit we just pushed, so it's not + // considered a new commit when checking for a commit + // made by automation. + waitForNewHead(g, localRepo, branch, preChangeCommitId) + + // Now create the automation object, and let it (one + // hopes!) make a commit itself. + err = createImageUpdateAutomation(testEnv, imageUpdateAutomationName, namespace, gitRepoName, namespace, branch, pushBranch, commitMessage, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + + initialHead, err := headFromBranch(localRepo, branch) + g.Expect(err).ToNot(HaveOccurred()) + defer initialHead.Free() + + preChangeCommitId = commitIdFromBranch(localRepo, branch) + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, pushBranch, preChangeCommitId) + + head, err := getRemoteHead(localRepo, pushBranch) + g.Expect(err).NotTo(HaveOccurred()) + commit, err := localRepo.LookupCommit(head) + g.Expect(err).ToNot(HaveOccurred()) + defer commit.Free() + g.Expect(commit.Message()).To(Equal(commitMessage)) + + // previous commits should still exist in the tree. + // regression check to ensure previous commits were not squashed. + oldCommit, err := localRepo.LookupCommit(initialHead.Id()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(oldCommit).ToNot(BeNil()) + }) - It("pushes another commit to the existing push branch", func() { - initialHead, err := headFromBranch(localRepo, branch) - Expect(err).ToNot(HaveOccurred()) - defer initialHead.Free() - - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - preChangeCommitId := commitIdFromBranch(localRepo, branch) - - // observe the first commit - waitForNewHead(localRepo, pushBranch, preChangeCommitId) - head, err := getRemoteHead(localRepo, pushBranch) - headHash := head.String() - Expect(err).NotTo(HaveOccurred()) - - // update the policy and expect another commit in the push branch - policy.Status.LatestImage = "helloworld:v1.3.0" - Expect(k8sClient.Status().Update(context.TODO(), policy)).To(Succeed()) - - preChangeCommitId = commitIdFromBranch(localRepo, branch) - waitForNewHead(localRepo, pushBranch, preChangeCommitId) - - head, err = getRemoteHead(localRepo, pushBranch) - Expect(err).NotTo(HaveOccurred()) - Expect(head.String()).NotTo(Equal(headHash)) - - // previous commits should still exist in the tree. - // regression check to ensure previous commits were not squashed. - oldCommit, err := localRepo.LookupCommit(initialHead.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(oldCommit).ToNot(BeNil()) - }) + t.Run("push branch gets updated", func(t *testing.T) { + initialHead, err := headFromBranch(localRepo, branch) + g.Expect(err).ToNot(HaveOccurred()) + defer initialHead.Free() - It("still pushes to the push branch after it's merged", func() { - initialHead, err := headFromBranch(localRepo, branch) - Expect(err).ToNot(HaveOccurred()) - defer initialHead.Free() - - preChangeCommitId := commitIdFromBranch(localRepo, branch) - - // observe the first commit - waitForNewHead(localRepo, pushBranch, preChangeCommitId) - head, err := getRemoteHead(localRepo, pushBranch) - Expect(err).NotTo(HaveOccurred()) - headHash := head.String() - - // merge the push branch into checkout branch, and push the merge commit - // upstream. - // waitForNewHead() leaves the repo at the head of the branch given, i.e., the - // push branch), so we have to check out the "main" branch first. - r, err := rebase(localRepo, pushBranch, branch) - Expect(err).ToNot(HaveOccurred()) - err = r.Finish() - Expect(err).ToNot(HaveOccurred()) - defer r.Free() - - // update the policy and expect another commit in the push branch - preChangeCommitId = commitIdFromBranch(localRepo, branch) - policy.Status.LatestImage = "helloworld:v1.3.0" - Expect(k8sClient.Status().Update(context.TODO(), policy)).To(Succeed()) - waitForNewHead(localRepo, pushBranch, preChangeCommitId) - - head, err = getRemoteHead(localRepo, pushBranch) - Expect(err).NotTo(HaveOccurred()) - Expect(head.String()).NotTo(Equal(headHash)) - - // previous commits should still exist in the tree. - // regression check to ensure previous commits were not squashed. - oldCommit, err := localRepo.LookupCommit(initialHead.Id()) - Expect(err).ToNot(HaveOccurred()) - Expect(oldCommit).ToNot(BeNil()) - }) + // Get the head hash before update. + head, err := getRemoteHead(localRepo, pushBranch) + g.Expect(err).NotTo(HaveOccurred()) + headHash := head.String() - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), update)).To(Succeed()) - }) + preChangeCommitId := commitIdFromBranch(localRepo, branch) - }) + // Update the policy and expect another commit in the push + // branch. + err = updateImagePolicyWithLatestImage(testEnv, imagePolicyName, namespace, "helloworld:v1.3.0") + g.Expect(err).ToNot(HaveOccurred()) - Context("with Setters", func() { - - var ( - updateKey types.NamespacedName - updateBySetters *imagev1.ImageUpdateAutomation - ) - - BeforeEach(func() { - preChangeCommitId := commitIdFromBranch(localRepo, branch) - // Insert a setter reference into the deployment file, - // before creating the automation object itself. - commitInRepo(cloneLocalRepoURL, branch, "Install setter marker", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) - }) - - // pull the head commit we just pushed, so it's not - // considered a new commit when checking for a commit - // made by automation. - waitForNewHead(localRepo, branch, preChangeCommitId) - - // now create the automation object, and let it (one - // hopes!) make a commit itself. - updateKey = types.NamespacedName{ - Namespace: gitRepoKey.Namespace, - Name: "update-" + randStringRunes(5), - } - updateBySetters = &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: updateKey.Name, - Namespace: updateKey.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, - Namespace: gitRepoKey.Namespace, - }, - Update: &imagev1.UpdateStrategy{ - Strategy: imagev1.UpdateStrategySetters, - }, - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - Commit: imagev1.CommitSpec{ - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - MessageTemplate: commitMessage, - }, - }, - }, - } - preChangeCommitId = commitIdFromBranch(localRepo, branch) - Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) - // wait for a new commit to be made by the controller - waitForNewHead(localRepo, branch, preChangeCommitId) - }) + waitForNewHead(g, localRepo, pushBranch, preChangeCommitId) - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), updateBySetters)).To(Succeed()) - }) + head, err = getRemoteHead(localRepo, pushBranch) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(head.String()).NotTo(Equal(headHash)) - It("updates to the most recent image", func() { - // having passed the BeforeEach, we should see a commit - commit, err := headCommit(localRepo) - Expect(err).ToNot(HaveOccurred()) + // previous commits should still exist in the tree. + // regression check to ensure previous commits were not squashed. + oldCommit, err := localRepo.LookupCommit(initialHead.Id()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(oldCommit).ToNot(BeNil()) + }) - defer commit.Free() - Expect(commit.Message()).To(Equal(commitMessage)) + t.Run("still pushes to the push branch after it's merged", func(t *testing.T) { + initialHead, err := headFromBranch(localRepo, branch) + g.Expect(err).ToNot(HaveOccurred()) + defer initialHead.Free() + + // Get the head hash before. + head, err := getRemoteHead(localRepo, pushBranch) + g.Expect(err).NotTo(HaveOccurred()) + headHash := head.String() + + // Merge the push branch into checkout branch, and push the merge commit + // upstream. + // waitForNewHead() leaves the repo at the head of the branch given, i.e., the + // push branch), so we have to check out the "main" branch first. + r, err := rebase(g, localRepo, pushBranch, branch) + g.Expect(err).ToNot(HaveOccurred()) + err = r.Finish() + g.Expect(err).ToNot(HaveOccurred()) + defer r.Free() + + preChangeCommitId := commitIdFromBranch(localRepo, branch) + + // Update the policy and expect another commit in the push + // branch. + err = updateImagePolicyWithLatestImage(testEnv, imagePolicyName, namespace, "helloworld:v1.3.1") + g.Expect(err).ToNot(HaveOccurred()) + + waitForNewHead(g, localRepo, pushBranch, preChangeCommitId) + + head, err = getRemoteHead(localRepo, pushBranch) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(head.String()).NotTo(Equal(headHash)) + + // previous commits should still exist in the tree. + // regression check to ensure previous commits were not squashed. + oldCommit, err := localRepo.LookupCommit(initialHead.Id()) + g.Expect(err).ToNot(HaveOccurred()) + g.Expect(oldCommit).ToNot(BeNil()) + }) - var newObj imagev1.ImageUpdateAutomation - Expect(k8sClient.Get(context.Background(), updateKey, &newObj)).To(Succeed()) - Expect(newObj.Status.LastPushCommit).To(Equal(commit.Id().String())) - Expect(newObj.Status.LastPushTime).ToNot(BeNil()) + // Cleanup the image update automation used above. + g.Expect(deleteImageUpdateAutomation(testEnv, imageUpdateAutomationName, namespace)).To(Succeed()) + }) - compareRepoWithExpected(cloneLocalRepoURL, branch, "testdata/appconfig-setters-expected", func(tmp string) { - Expect(replaceMarker(tmp, policyKey)).To(Succeed()) - }) - }) + t.Run("with update strategy setters", func(t *testing.T) { + // Clone the repo locally. + // NOTE: A new localRepo is created here instead of reusing the one + // in the previous case due to a bug in some of the git operations + // test helper. When switching branches, the localRepo seems to get + // stuck in one particular branch. As a workaround, create a + // separate localRepo. + localRepo, err := clone(cloneLocalRepoURL, "origin", branch) + g.Expect(err).ToNot(HaveOccurred(), "failed to clone git repo") - It("stops updating when suspended", func() { - // suspend it, and check that reconciliation does not run - var updatePatch imagev1.ImageUpdateAutomation - Expect(k8sClient.Get(context.TODO(), updateKey, &updatePatch)).To(Succeed()) - updatePatch.Spec.Suspend = true - Expect(k8sClient.Patch(context.Background(), &updatePatch, client.Merge)).To(Succeed()) - // wait for the suspension to reach the cache - var newUpdate imagev1.ImageUpdateAutomation - Eventually(func() bool { - if err := imageAutoReconciler.Get(context.Background(), updateKey, &newUpdate); err != nil { - return false - } - return newUpdate.Spec.Suspend - }, timeout, time.Second).Should(BeTrue()) - // run the reconciliation explicitly, and make sure it - // doesn't do anything - result, err := imageAutoReconciler.Reconcile(logr.NewContext(context.TODO(), ctrl.Log), ctrl.Request{ - NamespacedName: updateKey, - }) - Expect(err).To(BeNil()) - // this ought to fail if suspend is not working, since the item would be requeued; - // but if not, additional checks lie below. - Expect(result).To(Equal(ctrl.Result{})) - - var checkUpdate imagev1.ImageUpdateAutomation - Expect(k8sClient.Get(context.Background(), updateKey, &checkUpdate)).To(Succeed()) - Expect(checkUpdate.Status.ObservedGeneration).NotTo(Equal(checkUpdate.ObjectMeta.Generation)) - }) + g.Expect(checkoutBranch(localRepo, branch)).ToNot(HaveOccurred()) + err = createImagePolicyWithLatestImage(testEnv, imagePolicyName, namespace, "not-expected-to-exist", "1.x", latestImage) + g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource") - It("runs when the reconcile request annotation is added", func() { - // the automation has run, and is not expected to run - // again for 2 hours. Make a commit to the git repo - // which needs to be undone by automation, then add - // the annotation and make sure it runs again. - Expect(k8sClient.Get(context.Background(), updateKey, updateBySetters)).To(Succeed()) - Expect(updateBySetters.Status.LastAutomationRunTime).ToNot(BeNil()) - }) + defer func() { + g.Expect(deleteImagePolicy(testEnv, imagePolicyName, namespace)).ToNot(HaveOccurred()) + }() + + preChangeCommitId := commitIdFromBranch(localRepo, branch) + // Insert a setter reference into the deployment file, + // before creating the automation object itself. + commitInRepo(g, cloneLocalRepoURL, branch, "Install setter marker", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) }) - } - } - Context("Using go-git", func() { - Context("with HTTP", func() { - Describe("runs end to end", endToEnd(sourcev1.GoGitImplementation, "http")) - }) - Context("with SSH", func() { - Describe("runs end to end", endToEnd(sourcev1.GoGitImplementation, "ssh")) - }) - }) + // Pull the head commit we just pushed, so it's not + // considered a new commit when checking for a commit + // made by automation. + waitForNewHead(g, localRepo, branch, preChangeCommitId) - Context("Using libgit2", func() { - Context("with HTTP", func() { - Describe("runs end to end", endToEnd(sourcev1.LibGit2Implementation, "http")) - }) - Context("with SSH", func() { - Describe("runs end to end", endToEnd(sourcev1.LibGit2Implementation, "ssh")) + preChangeCommitId = commitIdFromBranch(localRepo, branch) + + // Now create the automation object, and let it (one + // hopes!) make a commit itself. + updateKey := types.NamespacedName{ + Namespace: namespace, + Name: "update-" + randStringRunes(5), + } + err = createImageUpdateAutomation(testEnv, updateKey.Name, namespace, gitRepoName, namespace, branch, "", commitMessage, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + defer func() { + g.Expect(deleteImageUpdateAutomation(testEnv, updateKey.Name, namespace)).To(Succeed()) + }() + + // Wait for a new commit to be made by the controller. + waitForNewHead(g, localRepo, branch, preChangeCommitId) + + // Check if the repo head matches with the ImageUpdateAutomation + // last push commit status. + commit, err := headCommit(localRepo) + g.Expect(err).ToNot(HaveOccurred()) + defer commit.Free() + g.Expect(commit.Message()).To(Equal(commitMessage)) + + var newObj imagev1.ImageUpdateAutomation + g.Expect(testEnv.Get(context.Background(), updateKey, &newObj)).To(Succeed()) + g.Expect(newObj.Status.LastPushCommit).To(Equal(commit.Id().String())) + g.Expect(newObj.Status.LastPushTime).ToNot(BeNil()) + + compareRepoWithExpected(g, cloneLocalRepoURL, branch, "testdata/appconfig-setters-expected", func(tmp string) { + g.Expect(replaceMarker(tmp, policyKey)).To(Succeed()) + }) }) - }) - Context("defaulting", func() { - var key types.NamespacedName - var auto *imagev1.ImageUpdateAutomation + t.Run("no reconciliation when object is suspended", func(t *testing.T) { + err = createImagePolicyWithLatestImage(testEnv, imagePolicyName, namespace, "not-expected-to-exist", "1.x", latestImage) + g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource") + + defer func() { + g.Expect(deleteImagePolicy(testEnv, imagePolicyName, namespace)).ToNot(HaveOccurred()) + }() - BeforeEach(func() { - key = types.NamespacedName{ - Namespace: namespace.Name, + // Create the automation object. + updateKey := types.NamespacedName{ + Namespace: namespace, Name: "update-" + randStringRunes(5), } - auto = &imagev1.ImageUpdateAutomation{ - ObjectMeta: metav1.ObjectMeta{ - Name: key.Name, - Namespace: key.Namespace, - }, - Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.CrossNamespaceSourceReference{ - Kind: "GitRepository", - Name: "garbage", - Namespace: key.Namespace, - }, - Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing - GitSpec: &imagev1.GitSpec{ - Checkout: &imagev1.GitCheckoutSpec{ - Reference: sourcev1.GitRepositoryRef{ - Branch: branch, - }, - }, - // leave Update field out - Commit: imagev1.CommitSpec{ - Author: imagev1.CommitUser{ - Name: authorName, - Email: authorEmail, - }, - MessageTemplate: "nothing", - }, - }, - }, + err = createImageUpdateAutomation(testEnv, updateKey.Name, namespace, gitRepoName, namespace, branch, "", commitMessage, "", updateStrategy) + g.Expect(err).ToNot(HaveOccurred()) + defer func() { + g.Expect(deleteImageUpdateAutomation(testEnv, updateKey.Name, namespace)).To(Succeed()) + }() + + // Wait for the object to be available in the cache before + // attempting update. + g.Eventually(func() bool { + obj := &imagev1.ImageUpdateAutomation{} + if err := testEnv.Get(context.Background(), updateKey, obj); err != nil { + return false + } + return true + }, timeout, time.Second).Should(BeTrue()) + + // Suspend the automation object. + var updatePatch imagev1.ImageUpdateAutomation + g.Expect(testEnv.Get(context.TODO(), updateKey, &updatePatch)).To(Succeed()) + updatePatch.Spec.Suspend = true + g.Expect(testEnv.Patch(context.Background(), &updatePatch, client.Merge)).To(Succeed()) + + // Create a new image automation reconciler and run it + // explicitly. + imageAutoReconciler := &ImageUpdateAutomationReconciler{ + Client: testEnv, + Scheme: scheme.Scheme, } - Expect(k8sClient.Create(context.Background(), auto)).To(Succeed()) - }) - AfterEach(func() { - Expect(k8sClient.Delete(context.Background(), auto)).To(Succeed()) + // Wait for the suspension to reach the cache + var newUpdate imagev1.ImageUpdateAutomation + g.Eventually(func() bool { + if err := imageAutoReconciler.Get(context.Background(), updateKey, &newUpdate); err != nil { + return false + } + return newUpdate.Spec.Suspend + }, timeout, time.Second).Should(BeTrue()) + // Run the reconciliation explicitly, and make sure it + // doesn't do anything + result, err := imageAutoReconciler.Reconcile(logr.NewContext(context.TODO(), ctrl.Log), ctrl.Request{ + NamespacedName: updateKey, + }) + g.Expect(err).To(BeNil()) + // This ought to fail if suspend is not working, since the item would be requeued; + // but if not, additional checks lie below. + g.Expect(result).To(Equal(ctrl.Result{})) + + var checkUpdate imagev1.ImageUpdateAutomation + g.Expect(testEnv.Get(context.Background(), updateKey, &checkUpdate)).To(Succeed()) + g.Expect(checkUpdate.Status.ObservedGeneration).NotTo(Equal(checkUpdate.ObjectMeta.Generation)) }) + } - It("defaults .spec.update to {strategy: Setters}", func() { - var fetchedAuto imagev1.ImageUpdateAutomation - Expect(k8sClient.Get(context.Background(), key, &fetchedAuto)).To(Succeed()) - Expect(fetchedAuto.Spec.Update).To(Equal(&imagev1.UpdateStrategy{Strategy: imagev1.UpdateStrategySetters})) - }) - }) -}) + // Run the protocol based e2e tests against the git implementations. + for _, gitImpl := range gitImpls { + for _, proto := range protos { + t.Run(fmt.Sprintf("%s_%s", gitImpl, proto), func(t *testing.T) { + testFunc(t, proto, gitImpl) + }) + } + } +} + +func TestImageAutomationReconciler_defaulting(t *testing.T) { + g := NewWithT(t) + + branch := randStringRunes(8) + namespace := &corev1.Namespace{} + namespace.Name = "image-auto-test-" + randStringRunes(5) + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + // Create a test namespace. + g.Expect(testEnv.Create(ctx, namespace)).To(Succeed()) + defer func() { + g.Expect(testEnv.Delete(ctx, namespace)).To(Succeed()) + }() + + // Create an instance of ImageUpdateAutomation. + key := types.NamespacedName{ + Name: "update-" + randStringRunes(5), + Namespace: namespace.Name, + } + auto := &imagev1.ImageUpdateAutomation{ + ObjectMeta: metav1.ObjectMeta{ + Name: key.Name, + Namespace: key.Namespace, + }, + Spec: imagev1.ImageUpdateAutomationSpec{ + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: "garbage", + }, + Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing + GitSpec: &imagev1.GitSpec{ + Checkout: &imagev1.GitCheckoutSpec{ + Reference: sourcev1.GitRepositoryRef{ + Branch: branch, + }, + }, + // leave Update field out + Commit: imagev1.CommitSpec{ + Author: imagev1.CommitUser{ + Email: testAuthorEmail, + }, + MessageTemplate: "nothing", + }, + }, + }, + } + g.Expect(testEnv.Create(ctx, auto)).To(Succeed()) + defer func() { + g.Expect(testEnv.Delete(ctx, auto)).To(Succeed()) + }() + + // Should default .spec.update to {strategy: Setters}. + var fetchedAuto imagev1.ImageUpdateAutomation + g.Eventually(func() bool { + err := testEnv.Get(ctx, key, &fetchedAuto) + return err == nil + }, timeout, time.Second).Should(BeTrue()) + g.Expect(fetchedAuto.Spec.Update). + To(Equal(&imagev1.UpdateStrategy{Strategy: imagev1.UpdateStrategySetters})) +} func checkoutBranch(repo *git2go.Repository, branch string) error { sl, err := repo.StatusList(&git2go.StatusOptions{ @@ -1318,12 +835,6 @@ func checkoutBranch(repo *git2go.Repository, branch string) error { return repo.SetHead(fmt.Sprintf("refs/heads/%s", branch)) } -func expectCommittedAndPushed(conditions []metav1.Condition) { - rc := apimeta.FindStatusCondition(conditions, meta.ReadyCondition) - Expect(rc).ToNot(BeNil()) - Expect(rc.Message).To(ContainSubstring("committed and pushed")) -} - func replaceMarker(path string, policyKey types.NamespacedName) error { // NB this requires knowledge of what's in the git repo, so a little brittle deployment := filepath.Join(path, "deploy.yaml") @@ -1342,27 +853,29 @@ func setterRef(name types.NamespacedName) string { return fmt.Sprintf(`{"%s": "%s:%s"}`, update.SetterShortHand, name.Namespace, name.Name) } -func compareRepoWithExpected(repoURL, branch, fixture string, changeFixture func(tmp string)) { +func compareRepoWithExpected(g *WithT, repoURL, branch, fixture string, changeFixture func(tmp string)) { expected, err := os.MkdirTemp("", "gotest-imageauto-expected") - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) defer os.RemoveAll(expected) copy.Copy(fixture, expected) changeFixture(expected) repo, err := clone(repoURL, "origin", branch) - Expect(err).ToNot(HaveOccurred()) - actual := repo.Workdir() + g.Expect(err).ToNot(HaveOccurred()) + // NOTE: The workdir contains a trailing /. Clean it to not confuse the + // DiffDirectories(). + actual := filepath.Clean(repo.Workdir()) defer os.RemoveAll(actual) - Expect(err).ToNot(HaveOccurred()) - test.ExpectMatchingDirectories(actual, expected) + g.Expect(err).ToNot(HaveOccurred()) + test.ExpectMatchingDirectories(g, actual, expected) } -func commitInRepo(repoURL, branch, msg string, changeFiles func(path string)) { +func commitInRepo(g *WithT, repoURL, branch, msg string, changeFiles func(path string)) { originRemote := "origin" repo, err := clone(repoURL, originRemote, branch) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) changeFiles(repo.Workdir()) @@ -1372,7 +885,7 @@ func commitInRepo(repoURL, branch, msg string, changeFiles func(path string)) { When: time.Now(), } _, err = commitWorkDir(repo, branch, msg, sig) - Expect(err).ToNot(HaveOccurred()) + g.Expect(err).ToNot(HaveOccurred()) origin, err := repo.Remotes.Lookup(originRemote) if err != nil { @@ -1380,7 +893,7 @@ func commitInRepo(repoURL, branch, msg string, changeFiles func(path string)) { } defer origin.Free() - Expect(origin.Push([]string{branchRefName(branch)}, &libgit2.PushOptions{})).To(Succeed()) + g.Expect(origin.Push([]string{branchRefName(branch)}, &libgit2.PushOptions{})).To(Succeed()) } // Initialise a git server with a repo including the files in dir. @@ -1633,11 +1146,11 @@ func clone(repoURL, remoteName, branchName string) (*git2go.Repository, error) { return git2go.Clone(repoURL, dir, opts) } -func waitForNewHead(repo *git2go.Repository, branch, preChangeHash string) { +func waitForNewHead(g *WithT, repo *git2go.Repository, branch, preChangeHash string) { var commitToResetTo *git2go.Commit // Now try to fetch new commits from that remote branch - Eventually(func() bool { + g.Eventually(func() bool { origin, err := repo.Remotes.Lookup("origin") if err != nil { panic("origin not set") @@ -1675,7 +1188,7 @@ func waitForNewHead(repo *git2go.Repository, branch, preChangeHash string) { // New commits in the remote branch -- reset the working tree head // to that. Note this does not create a local branch tracking the // remote, so it is a detached head. - Expect(repo.ResetToCommit(commitToResetTo, libgit2.ResetHard, + g.Expect(repo.ResetToCommit(commitToResetTo, libgit2.ResetHard, &libgit2.CheckoutOptions{})).To(Succeed()) } } @@ -1720,12 +1233,12 @@ func getRemoteHead(repo *git2go.Repository, branchName string) (*git2go.Oid, err // This merges the push branch into HEAD, and pushes upstream. This is // to simulate e.g., a PR being merged. -func rebase(repo *git2go.Repository, sourceBranch, targetBranch string) (*git2go.Rebase, error) { +func rebase(g *WithT, repo *git2go.Repository, sourceBranch, targetBranch string) (*git2go.Rebase, error) { rebaseOpts, err := git2go.DefaultRebaseOptions() - Expect(err).NotTo(HaveOccurred()) + g.Expect(err).NotTo(HaveOccurred()) err = checkoutBranch(repo, sourceBranch) - Expect(err).NotTo(HaveOccurred()) + g.Expect(err).NotTo(HaveOccurred()) master, err := repo.LookupBranch(targetBranch, git2go.BranchLocal) if err != nil { @@ -1792,3 +1305,328 @@ func rebase(repo *git2go.Repository, sourceBranch, targetBranch string) (*git2go func operationsAreEqual(l, r *git2go.RebaseOperation) bool { return l.Exec == r.Exec && l.Type == r.Type && l.Id.String() == r.Id.String() } + +type repoAndPolicyArgs struct { + namespace, imagePolicyName, gitRepoName, branch, gitRepoNamespace string +} + +// newRepoAndPolicyArgs generates random namespace, git repo, branch and image +// policy names to be used in the test. The gitRepoNamespace is set the same +// as the overall namespace. For different git repo namespace, the caller may +// assign it as per the needs. +func newRepoAndPolicyArgs() repoAndPolicyArgs { + args := repoAndPolicyArgs{ + namespace: "image-auto-test-" + randStringRunes(5), + gitRepoName: "image-auto-test-" + randStringRunes(5), + branch: randStringRunes(8), + imagePolicyName: "policy-" + randStringRunes(5), + } + args.gitRepoNamespace = args.namespace + return args +} + +// testWithRepoAndImagePolicyTestFunc is the test closure function type passed +// to testWithRepoAndImagePolicy. +type testWithRepoAndImagePolicyTestFunc func(g *WithT, s repoAndPolicyArgs, repoURL string, localRepo *libgit2.Repository) + +// testWithRepoAndImagePolicy generates a repoAndPolicyArgs with all the +// resource in the same namespace and runs the given repo and image policy test. +func testWithRepoAndImagePolicy( + g *WithT, + kClient client.Client, + fixture string, + policySpec imagev1_reflect.ImagePolicySpec, + latest string, + testFunc testWithRepoAndImagePolicyTestFunc) { + // Generate unique repo and policy arguments. + args := newRepoAndPolicyArgs() + testWithCustomRepoAndImagePolicy(g, kClient, fixture, policySpec, latest, args, testFunc) +} + +// testWithRepoAndImagePolicy sets up a git server, a repository in the git +// server, a GitRepository object for the created git repo, and an ImagePolicy +// with the given policy spec based on a repoAndPolicyArgs. It calls testFunc +// to run the test in the created environment. +func testWithCustomRepoAndImagePolicy( + g *WithT, + kClient client.Client, + fixture string, + policySpec imagev1_reflect.ImagePolicySpec, + latest string, + args repoAndPolicyArgs, + testFunc testWithRepoAndImagePolicyTestFunc) { + repositoryPath := "/config-" + randStringRunes(6) + ".git" + + // Create test git server. + gitServer, err := setupGitTestServer() + g.Expect(err).ToNot(HaveOccurred(), "failed to create test git server") + defer os.RemoveAll(gitServer.Root()) + defer gitServer.StopHTTP() + + // Create test namespace. + nsCleanup, err := createNamespace(kClient, args.namespace) + g.Expect(err).ToNot(HaveOccurred(), "failed to create test namespace") + defer func() { + g.Expect(nsCleanup()).To(Succeed()) + }() + + // Create gitRepoNamespace if it's not the same as the overall test + // namespace. + if args.namespace != args.gitRepoNamespace { + gitNSCleanup, err := createNamespace(kClient, args.gitRepoNamespace) + g.Expect(err).ToNot(HaveOccurred(), "failed to create test git repo namespace") + defer func() { + g.Expect(gitNSCleanup()).To(Succeed()) + }() + } + + // Create a git repo. + g.Expect(initGitRepo(gitServer, fixture, args.branch, repositoryPath)).To(Succeed()) + + // Clone the repo. + repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath + localRepo, err := clone(repoURL, "origin", args.branch) + g.Expect(err).ToNot(HaveOccurred(), "failed to clone git repo") + + // Create GitRepository resource for the above repo. + err = createGitRepository(kClient, args.gitRepoName, args.gitRepoNamespace, "", repoURL, "") + g.Expect(err).ToNot(HaveOccurred(), "failed to create GitRepository resource") + + // Create ImagePolicy with populated latest image in the status. + err = createImagePolicyWithLatestImageForSpec(kClient, args.imagePolicyName, args.namespace, policySpec, latest) + g.Expect(err).ToNot(HaveOccurred(), "failed to create ImagePolicy resource") + + testFunc(g, args, repoURL, localRepo) +} + +// setupGitTestServer creates and returns a git test server. The caller must +// ensure it's stopped and cleaned up. +func setupGitTestServer() (*gittestserver.GitServer, error) { + gitServer, err := gittestserver.NewTempGitServer() + if err != nil { + return nil, err + } + username := randStringRunes(5) + password := randStringRunes(5) + // Using authentication makes using the server more fiddly in + // general, but is required for testing SSH. + gitServer.Auth(username, password) + gitServer.AutoCreate() + if err := gitServer.StartHTTP(); err != nil { + return nil, err + } + gitServer.KeyDir(filepath.Join(gitServer.Root(), "keys")) + if err := gitServer.ListenSSH(); err != nil { + return nil, err + } + return gitServer, nil +} + +// cleanup is used to return closures for cleaning up. +type cleanup func() error + +// createNamespace creates a namespace and returns a closure for deleting the +// namespace. +func createNamespace(kClient client.Client, name string) (cleanup, error) { + namespace := &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + } + if err := kClient.Create(context.Background(), namespace); err != nil { + return nil, err + } + cleanup := func() error { + return kClient.Delete(context.Background(), namespace) + } + return cleanup, nil +} + +func createGitRepository(kClient client.Client, name, namespace, impl, repoURL, secretRef string) error { + gitRepo := &sourcev1.GitRepository{ + Spec: sourcev1.GitRepositorySpec{ + URL: repoURL, + Interval: metav1.Duration{Duration: time.Minute}, + }, + } + gitRepo.Name = name + gitRepo.Namespace = namespace + if secretRef != "" { + gitRepo.Spec.SecretRef = &meta.LocalObjectReference{Name: secretRef} + } + if impl != "" { + gitRepo.Spec.GitImplementation = impl + } + return kClient.Create(context.Background(), gitRepo) +} + +func createImagePolicyWithLatestImage(kClient client.Client, name, namespace, repoRef, semverRange, latest string) error { + policySpec := imagev1_reflect.ImagePolicySpec{ + ImageRepositoryRef: meta.NamespacedObjectReference{ + Name: repoRef, + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: semverRange, + }, + }, + } + return createImagePolicyWithLatestImageForSpec(kClient, name, namespace, policySpec, latest) +} + +func createImagePolicyWithLatestImageForSpec(kClient client.Client, name, namespace string, policySpec imagev1_reflect.ImagePolicySpec, latest string) error { + policy := &imagev1_reflect.ImagePolicy{ + Spec: policySpec, + } + policy.Name = name + policy.Namespace = namespace + err := kClient.Create(context.Background(), policy) + if err != nil { + return err + } + policy.Status.LatestImage = latest + return kClient.Status().Update(context.Background(), policy) +} + +func updateImagePolicyWithLatestImage(kClient client.Client, name, namespace, latest string) error { + policy := &imagev1_reflect.ImagePolicy{} + key := types.NamespacedName{ + Name: name, + Namespace: namespace, + } + if err := kClient.Get(context.Background(), key, policy); err != nil { + return err + } + policy.Status.LatestImage = latest + return kClient.Status().Update(context.Background(), policy) +} + +func createImageUpdateAutomation(kClient client.Client, name, namespace, + gitRepo, gitRepoNamespace, checkoutBranch, pushBranch, commitTemplate, signingKeyRef string, + updateStrategy *imagev1.UpdateStrategy) error { + updateAutomation := &imagev1.ImageUpdateAutomation{ + Spec: imagev1.ImageUpdateAutomationSpec{ + Interval: metav1.Duration{Duration: 2 * time.Hour}, // This is to ensure any subsequent run should be outside the scope of the testing. + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: gitRepo, + Namespace: gitRepoNamespace, + }, + GitSpec: &imagev1.GitSpec{ + Checkout: &imagev1.GitCheckoutSpec{ + Reference: sourcev1.GitRepositoryRef{ + Branch: checkoutBranch, + }, + }, + Commit: imagev1.CommitSpec{ + MessageTemplate: commitTemplate, + Author: imagev1.CommitUser{ + Name: testAuthorName, + Email: testAuthorEmail, + }, + }, + }, + Update: updateStrategy, + }, + } + updateAutomation.Name = name + updateAutomation.Namespace = namespace + if pushBranch != "" { + updateAutomation.Spec.GitSpec.Push = &imagev1.PushSpec{ + Branch: pushBranch, + } + } + if signingKeyRef != "" { + updateAutomation.Spec.GitSpec.Commit.SigningKey = &imagev1.SigningKey{ + SecretRef: meta.LocalObjectReference{Name: signingKeyRef}, + } + } + return kClient.Create(context.Background(), updateAutomation) +} + +func deleteImageUpdateAutomation(kClient client.Client, name, namespace string) error { + update := &imagev1.ImageUpdateAutomation{} + update.Name = name + update.Namespace = namespace + return kClient.Delete(context.Background(), update) +} + +func deleteImagePolicy(kClient client.Client, name, namespace string) error { + imagePolicy := &imagev1_reflect.ImagePolicy{} + imagePolicy.Name = name + imagePolicy.Namespace = namespace + return kClient.Delete(context.Background(), imagePolicy) +} + +func createSigningKeyPair(kClient client.Client, name, namespace string) (*openpgp.Entity, error) { + pgpEntity, err := openpgp.NewEntity("", "", "", nil) + if err != nil { + return nil, err + } + // Configure OpenPGP armor encoder. + b := bytes.NewBuffer(nil) + w, err := armor.Encode(b, openpgp.PrivateKeyType, nil) + if err != nil { + return nil, err + } + // Serialize private key. + if err := pgpEntity.SerializePrivate(w, nil); err != nil { + return nil, err + } + if err = w.Close(); err != nil { + return nil, err + } + // Create the secret containing signing key. + sec := &corev1.Secret{ + Data: map[string][]byte{ + "git.asc": b.Bytes(), + }, + } + sec.Name = name + sec.Namespace = namespace + if err := kClient.Create(ctx, sec); err != nil { + return nil, err + } + return pgpEntity, nil +} + +func createSSHIdentitySecret(kClient client.Client, name, namespace, repoURL string) error { + url, err := url.Parse(repoURL) + if err != nil { + return err + } + knownhosts, err := ssh.ScanHostKey(url.Host, 5*time.Second) + if err != nil { + return err + } + keygen := ssh.NewRSAGenerator(2048) + pair, err := keygen.Generate() + if err != nil { + return err + } + sec := &corev1.Secret{ + StringData: map[string]string{ + "known_hosts": string(knownhosts), + "identity": string(pair.PrivateKey), + "identity.pub": string(pair.PublicKey), + }, + } + sec.Name = name + sec.Namespace = namespace + return kClient.Create(ctx, sec) +} + +func getRepoURL(gitServer *gittestserver.GitServer, repoPath, proto string) (string, error) { + if proto == "http" { + return gitServer.HTTPAddressWithCredentials() + repoPath, nil + } else if proto == "ssh" { + return getSSHRepoURL(gitServer.SSHAddress(), repoPath), nil + } + return "", fmt.Errorf("proto not set to http or ssh") +} + +func getSSHRepoURL(sshAddress, repoPath string) string { + // This is expected to use 127.0.0.1, but host key + // checking usually wants a hostname, so use + // "localhost". + sshURL := strings.Replace(sshAddress, "127.0.0.1", "localhost", 1) + return sshURL + repoPath +} From 0e87398e590d188b6eebf9e5f2d43c74ab0e055a Mon Sep 17 00:00:00 2001 From: Sunny Date: Mon, 25 Apr 2022 19:45:52 +0530 Subject: [PATCH 5/5] Remove legacy_suite_test.go and ginkgo deps Signed-off-by: Sunny --- controllers/legacy_suite_test.go | 119 ------------------------------- go.mod | 3 - go.sum | 5 -- 3 files changed, 127 deletions(-) delete mode 100644 controllers/legacy_suite_test.go diff --git a/controllers/legacy_suite_test.go b/controllers/legacy_suite_test.go deleted file mode 100644 index bd3fbd33..00000000 --- a/controllers/legacy_suite_test.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright 2020 The Flux authors - -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" - "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" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - imagev1_reflect "github.com/fluxcd/image-reflector-controller/api/v1beta1" - sourcev1 "github.com/fluxcd/source-controller/api/v1beta2" - - imagev1 "github.com/fluxcd/image-automation-controller/api/v1beta1" - // +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 imageAutoReconciler *ImageUpdateAutomationReconciler -var ginkgoTestEnv *envtest.Environment -var stopManager context.CancelFunc - -func TestAPIs(t *testing.T) { - RegisterFailHandler(Fail) - - RunSpecsWithDefaultAndCustomReporters(t, - "Controller Suite", - []Reporter{printer.NewlineReporter{}}) -} - -var _ = BeforeSuite(func(done Done) { - ctrl.SetLogger( - zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)), - ) - - By("bootstrapping test environment") - ginkgoTestEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{ - filepath.Join("..", "config", "crd", "bases"), - filepath.Join("testdata", "crds"), - }, - } - - mgrContext, cancel := context.WithCancel(ctx) - - var err error - cfg, err = ginkgoTestEnv.Start() - Expect(err).ToNot(HaveOccurred()) - Expect(cfg).ToNot(BeNil()) - - Expect(sourcev1.AddToScheme(scheme.Scheme)).To(Succeed()) - Expect(imagev1_reflect.AddToScheme(scheme.Scheme)).To(Succeed()) - - Expect(imagev1.AddToScheme(scheme.Scheme)).To(Succeed()) - // +kubebuilder:scaffold:scheme - - k8sManager, err = ctrl.NewManager(cfg, ctrl.Options{ - Scheme: scheme.Scheme, - }) - Expect(err).ToNot(HaveOccurred()) - - controllerName := "image-automation-controller" - imageAutoReconciler = &ImageUpdateAutomationReconciler{ - Client: k8sManager.GetClient(), - Scheme: scheme.Scheme, - EventRecorder: k8sManager.GetEventRecorderFor(controllerName), - } - Expect(imageAutoReconciler.SetupWithManager(k8sManager, ImageUpdateAutomationReconcilerOptions{})).To(Succeed()) - - go func() { - defer GinkgoRecover() - err = k8sManager.Start(mgrContext) - Expect(err).ToNot(HaveOccurred()) - }() - stopManager = cancel - - // Specifically an uncached client. Use .Get if you - // want to see what the reconcilers see. - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) - Expect(err).ToNot(HaveOccurred()) - Expect(k8sClient).ToNot(BeNil()) - - close(done) -}, 60) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - stopManager() - err := ginkgoTestEnv.Stop() - Expect(err).ToNot(HaveOccurred()) -}) diff --git a/go.mod b/go.mod index 8cfbd625..e72757e6 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/go-logr/logr v1.2.3 github.com/google/go-containerregistry v0.8.0 github.com/libgit2/git2go/v33 v33.0.9 - github.com/onsi/ginkgo v1.16.5 github.com/onsi/gomega v1.19.0 github.com/otiai10/copy v1.7.0 github.com/spf13/pflag v1.0.5 @@ -84,7 +83,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/nxadm/tail v1.4.8 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.12.1 // indirect @@ -112,7 +110,6 @@ require ( google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect diff --git a/go.sum b/go.sum index 212993e1..1f9c565c 100644 --- a/go.sum +++ b/go.sum @@ -437,7 +437,6 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.14 h1:gm3vOOXfiuw5i9p5N9xJvfjvuofpyvLA9Wr6QfK5Fng= github.com/go-openapi/swag v0.19.14/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus v0.0.0-20151105175453-c7fdd8b5cd55/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20180201030542-885f9cc04c9c/go.mod h1:/YcGZj5zSblfDWMMoOzV4fas9FZnQYTkDnsGvmh2Grw= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= @@ -761,7 +760,6 @@ github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/onsi/ginkgo v0.0.0-20151202141238-7f8ab55aaf3b/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -773,7 +771,6 @@ github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+ github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3 h1:e/3Cwtogj0HA+25nMP1jCMDIf8RtRYbGwGGuBIFztkc= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= @@ -1279,7 +1276,6 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201202213521-69691e467435/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1396,7 +1392,6 @@ golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82u golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=