diff --git a/api/v1alpha1/imageupdateautomation_types.go b/api/v1alpha1/imageupdateautomation_types.go index ffc24f7d..108f500d 100644 --- a/api/v1alpha1/imageupdateautomation_types.go +++ b/api/v1alpha1/imageupdateautomation_types.go @@ -30,15 +30,18 @@ type ImageUpdateAutomationSpec struct { // ready to make changes. // +required Checkout GitCheckoutSpec `json:"checkout"` + // Interval gives an lower bound for how often the automation // run should be attempted. // +required Interval metav1.Duration `json:"interval"` + // Update gives the specification for how to update the files in // the repository. This can be left empty, to use the default // value. // +kubebuilder:default={"strategy":"Setters"} Update *UpdateStrategy `json:"update,omitempty"` + // Commit specifies how to commit to the git repository. // +required Commit CommitSpec `json:"commit"` @@ -87,6 +90,12 @@ type UpdateStrategy struct { // +required // +kubebuilder:default=Setters Strategy UpdateStrategyName `json:"strategy"` + + // Path to the directory containing the manifests to be updated. + // Defaults to 'None', which translates to the root path + // of the GitRepositoryRef. + // +optional + Path string `json:"path,omitempty"` } // CommitSpec specifies how to commit changes to the git repository diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml index 0dd9f04e..629548ea 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml @@ -111,6 +111,11 @@ spec: files in the repository. This can be left empty, to use the default value. properties: + path: + description: Path to the directory containing the manifests to + be updated. Defaults to 'None', which translates to the root + path of the GitRepositoryRef. + type: string strategy: default: Setters description: Strategy names the strategy to be used. diff --git a/controllers/imageupdateautomation_controller.go b/controllers/imageupdateautomation_controller.go index b0591332..903eaf77 100644 --- a/controllers/imageupdateautomation_controller.go +++ b/controllers/imageupdateautomation_controller.go @@ -30,6 +30,7 @@ import ( gogit "github.com/go-git/go-git/v5" libgit2 "github.com/libgit2/git2go/v31" + securejoin "github.com/cyphar/filepath-securejoin" "github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing/object" "github.com/go-logr/logr" @@ -196,7 +197,16 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr return failWithError(err) } - if result, err := updateAccordingToSetters(ctx, tmp, policies.Items); err != nil { + manifestsPath := tmp + if auto.Spec.Update.Path != "" { + if p, err := securejoin.SecureJoin(tmp, auto.Spec.Update.Path); err != nil { + return failWithError(err) + } else { + manifestsPath = p + } + } + + if result, err := updateAccordingToSetters(ctx, manifestsPath, policies.Items); err != nil { return failWithError(err) } else { templateValues.Updated = result diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 0344b0d0..dd12f2e7 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -90,6 +90,7 @@ var _ = BeforeSuite(func(done Done) { Expect(imageAutoReconciler.SetupWithManager(k8sManager)).To(Succeed()) go func() { + defer GinkgoRecover() err = k8sManager.Start(ctrl.SetupSignalHandler()) Expect(err).ToNot(HaveOccurred()) }() diff --git a/controllers/testdata/pathconfig/no/deploy.yaml b/controllers/testdata/pathconfig/no/deploy.yaml new file mode 100644 index 00000000..a64a5f5b --- /dev/null +++ b/controllers/testdata/pathconfig/no/deploy.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: update-no +spec: + template: + spec: + containers: + - name: hello + image: helloworld:1.0.0 # SETTER_SITE diff --git a/controllers/testdata/pathconfig/yes/deploy.yaml b/controllers/testdata/pathconfig/yes/deploy.yaml new file mode 100644 index 00000000..bdf3b26e --- /dev/null +++ b/controllers/testdata/pathconfig/yes/deploy.yaml @@ -0,0 +1,10 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: update-yes +spec: + template: + spec: + containers: + - name: hello + image: helloworld:1.0.0 # SETTER_SITE diff --git a/controllers/update_test.go b/controllers/update_test.go index 20c42e96..2bbc6054 100644 --- a/controllers/update_test.go +++ b/controllers/update_test.go @@ -24,6 +24,7 @@ import ( "math/rand" "net/url" "os" + "path" "path/filepath" "strings" "time" @@ -263,6 +264,130 @@ Images: }) }) + Context("update path", func() { + + var localRepo *git.Repository + const commitTemplate = `Commit summary + +{{ range $resource, $_ := .Updated.Objects -}} +- {{ $resource.Name }} +{{ end -}} +` + + BeforeEach(func() { + Expect(initGitRepo(gitServer, "testdata/pathconfig", branch, repositoryPath)).To(Succeed()) + repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath + var err error + localRepo, err = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: repoURL, + RemoteName: "origin", + ReferenceName: plumbing.NewBranchReferenceName(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.LocalObjectReference{ + Name: "not-expected-to-exist", + }, + Policy: imagev1_reflect.ImagePolicyChoice{ + SemVer: &imagev1_reflect.SemVerPolicy{ + Range: "1.x", + }, + }, + }, + Status: imagev1_reflect.ImagePolicyStatus{ + LatestImage: "helloworld:v1.0.0", + }, + } + Expect(k8sClient.Create(context.Background(), policy)).To(Succeed()) + 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) { + replaceMarker(path.Join(tmp, "yes"), policyKey) + }) + commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) { + replaceMarker(path.Join(tmp, "no"), policyKey) + }) + + // 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) + + // 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 + Checkout: imagev1.GitCheckoutSpec{ + GitRepositoryRef: meta.LocalObjectReference{ + Name: gitRepoKey.Name, + }, + Branch: branch, + }, + Update: &imagev1.UpdateStrategy{ + Strategy: imagev1.UpdateStrategySetters, + Path: "./yes", + }, + Commit: imagev1.CommitSpec{ + MessageTemplate: commitTemplate, + }, + }, + } + Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed()) + // wait for a new commit to be made by the controller + waitForNewHead(localRepo, branch) + }) + + AfterEach(func() { + Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed()) + }) + + It("updates only the deployment in the specified path", func() { + head, _ := localRepo.Head() + commit, err := localRepo.CommitObject(head.Hash()) + Expect(err).ToNot(HaveOccurred()) + Expect(commit.Message).To(Not(ContainSubstring("update-no"))) + Expect(commit.Message).To(ContainSubstring("update-yes")) + }) + }) + endToEnd := func(impl, proto string) func() { return func() { var ( @@ -619,7 +744,6 @@ Images: Expect(fetchedAuto.Spec.Update).To(Equal(&imagev1.UpdateStrategy{Strategy: imagev1.UpdateStrategySetters})) }) }) - }) func expectCommittedAndPushed(conditions []metav1.Condition) { diff --git a/docs/api/image-automation.md b/docs/api/image-automation.md index 2ca9543c..1446b195 100644 --- a/docs/api/image-automation.md +++ b/docs/api/image-automation.md @@ -88,8 +88,8 @@ into which will be interpolated the details of the change made.

gitRepositoryRef
- -Kubernetes core/v1.LocalObjectReference + +github.com/fluxcd/pkg/apis/meta.LocalObjectReference @@ -106,7 +106,9 @@ string -

Branch gives the branch to clone from the git repository.

+

Branch gives the branch to clone from the git repository. If +.spec.push is not supplied, commits will also be pushed to +this branch.

@@ -206,7 +208,23 @@ CommitSpec -

Commit specifies how to commit to the git repo

+

Commit specifies how to commit to the git repository.

+ + + + +push
+ + +PushSpec + + + + +(Optional) +

Push specifies how and where to push commits made by the +automation. If missing, commits are pushed (back) to +.spec.checkout.branch.

@@ -311,7 +329,23 @@ CommitSpec -

Commit specifies how to commit to the git repo

+

Commit specifies how to commit to the git repository.

+ + + + +push
+ + +PushSpec + + + + +(Optional) +

Push specifies how and where to push commits made by the +automation. If missing, commits are pushed (back) to +.spec.checkout.branch.

@@ -434,6 +468,40 @@ github.com/fluxcd/pkg/apis/meta.ReconcileRequestStatus +

PushSpec +

+

+(Appears on: +ImageUpdateAutomationSpec) +

+

PushSpec specifies how and where to push commits.

+
+
+ + + + + + + + + + + + + +
FieldDescription
+branch
+ +string + +
+

Branch specifies that commits should be pushed to the branch +named. The branch is created using .spec.checkout.branch as the +starting point, if it doesn’t already exist.

+
+
+

UpdateStrategy

@@ -466,6 +534,20 @@ UpdateStrategyName

Strategy names the strategy to be used.

+ + +path
+ +string + + + +(Optional) +

Path to the directory containing the manifests to be updated. +Defaults to ‘None’, which translates to the root path +of the GitRepositoryRef.

+ + diff --git a/docs/spec/v1alpha1/imageupdateautomations.md b/docs/spec/v1alpha1/imageupdateautomations.md index ec35390e..8fa7f8d5 100644 --- a/docs/spec/v1alpha1/imageupdateautomations.md +++ b/docs/spec/v1alpha1/imageupdateautomations.md @@ -108,6 +108,11 @@ type UpdateStrategy struct { // Strategy names the strategy to be used. // +required Strategy UpdateStrategyName `json:"strategy"` + // Path to the directory containing the manifests to be updated. + // Defaults to 'None', which translates to the root path + // of the GitRepositoryRef. + // +optional + Path string `json:"path,omitempty"` } ``` diff --git a/go.mod b/go.mod index 6b4315b1..3e31551b 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ replace github.com/fluxcd/image-automation-controller/api => ./api replace gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c require ( + github.com/cyphar/filepath-securejoin v0.2.2 github.com/fluxcd/image-automation-controller/api v0.6.1 github.com/fluxcd/image-reflector-controller/api v0.7.0 github.com/fluxcd/pkg/apis/meta v0.8.0 diff --git a/go.sum b/go.sum index e5f1564a..c6bac23d 100644 --- a/go.sum +++ b/go.sum @@ -252,6 +252,7 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:ma github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.2 h1:jCwT2GTP+PY5nBz3c/YL5PAIbusElVrPujOBSCj8xRg= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=