diff --git a/api/v1beta1/imageupdateautomation_types.go b/api/v1beta1/imageupdateautomation_types.go index 542f28f2..fe4a0408 100644 --- a/api/v1beta1/imageupdateautomation_types.go +++ b/api/v1beta1/imageupdateautomation_types.go @@ -29,7 +29,7 @@ type ImageUpdateAutomationSpec struct { // SourceRef refers to the resource giving access details // to a git repository. // +required - SourceRef SourceReference `json:"sourceRef"` + SourceRef CrossNamespaceSourceReference `json:"sourceRef"` // GitSpec contains all the git-specific definitions. This is // technically optional, but in practice mandatory until there are // other kinds of source allowed. diff --git a/api/v1beta1/reference.go b/api/v1beta1/reference.go index e699f98e..595225b6 100644 --- a/api/v1beta1/reference.go +++ b/api/v1beta1/reference.go @@ -1,5 +1,5 @@ /* -Copyright 2020 The Flux authors +Copyright 2020, 2021 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. @@ -16,20 +16,33 @@ limitations under the License. package v1beta1 -// SourceReference contains enough information to let you locate the -// typed, referenced source object. -type SourceReference struct { - // API version of the referent +import "fmt" + +// CrossNamespaceSourceReference contains enough information to let you locate the +// typed Kubernetes resource object at cluster level. +type CrossNamespaceSourceReference struct { + // API version of the referent. // +optional APIVersion string `json:"apiVersion,omitempty"` - // Kind of the referent + // Kind of the referent. // +kubebuilder:validation:Enum=GitRepository // +kubebuilder:default=GitRepository // +required Kind string `json:"kind"` - // Name of the referent + // Name of the referent. // +required Name string `json:"name"` + + // Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference. + // +optional + Namespace string `json:"namespace,omitempty"` +} + +func (s *CrossNamespaceSourceReference) String() string { + if s.Namespace != "" { + return fmt.Sprintf("%s/%s/%s", s.Kind, s.Namespace, s.Name) + } + return fmt.Sprintf("%s/%s", s.Kind, s.Name) } diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 93c73fe0..5757cdb9 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -62,6 +62,21 @@ func (in *CommitUser) DeepCopy() *CommitUser { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CrossNamespaceSourceReference) DeepCopyInto(out *CrossNamespaceSourceReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossNamespaceSourceReference. +func (in *CrossNamespaceSourceReference) DeepCopy() *CrossNamespaceSourceReference { + if in == nil { + return nil + } + out := new(CrossNamespaceSourceReference) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitCheckoutSpec) DeepCopyInto(out *GitCheckoutSpec) { *out = *in @@ -252,21 +267,6 @@ func (in *SigningKey) DeepCopy() *SigningKey { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SourceReference) DeepCopyInto(out *SourceReference) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SourceReference. -func (in *SourceReference) DeepCopy() *SourceReference { - if in == nil { - return nil - } - out := new(SourceReference) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *UpdateStrategy) DeepCopyInto(out *UpdateStrategy) { *out = *in diff --git a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml index 7e5c9f89..65148234 100644 --- a/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml +++ b/config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml @@ -645,16 +645,20 @@ spec: to a git repository. properties: apiVersion: - description: API version of the referent + description: API version of the referent. type: string kind: default: GitRepository - description: Kind of the referent + description: Kind of the referent. enum: - GitRepository type: string name: - description: Name of the referent + description: Name of the referent. + type: string + namespace: + description: Namespace of the referent, defaults to the namespace + of the Kubernetes resource object that contains the reference. type: string required: - kind diff --git a/controllers/imageupdateautomation_controller.go b/controllers/imageupdateautomation_controller.go index 2711ac82..db7b7fed 100644 --- a/controllers/imageupdateautomation_controller.go +++ b/controllers/imageupdateautomation_controller.go @@ -162,22 +162,27 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr if kind := auto.Spec.SourceRef.Kind; kind != sourcev1.GitRepositoryKind { return failWithError(fmt.Errorf("source kind %q not supported", kind)) } + gitSpec := auto.Spec.GitSpec if gitSpec == nil { return failWithError(fmt.Errorf("source kind %s neccessitates field .spec.git", sourcev1.GitRepositoryKind)) } var origin sourcev1.GitRepository + gitRepoNamespace := req.Namespace + if auto.Spec.SourceRef.Namespace != "" { + gitRepoNamespace = auto.Spec.SourceRef.Namespace + } originName := types.NamespacedName{ Name: auto.Spec.SourceRef.Name, - Namespace: auto.GetNamespace(), + Namespace: gitRepoNamespace, } debuglog.Info("fetching git repository", "gitrepository", originName) if err := r.Get(ctx, originName, &origin); err != nil { if client.IgnoreNotFound(err) == nil { imagev1.SetImageUpdateAutomationReadiness(&auto, metav1.ConditionFalse, imagev1.GitNotAvailableReason, "referenced git repository is missing") - log.Error(err, "referenced git repository does not exist") + log.Error(err, fmt.Sprintf("referenced git repository %s does not exist.", originName.String())) if err := r.patchStatus(ctx, req, auto.Status); err != nil { return ctrl.Result{Requeue: true}, err } diff --git a/controllers/update_test.go b/controllers/update_test.go index 9be17efb..4ecc4e38 100644 --- a/controllers/update_test.go +++ b/controllers/update_test.go @@ -238,9 +238,189 @@ Images: }, 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.SourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, + 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, + }, + }, + } + 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("formats the commit message as in the template", func() { + head, _ := localRepo.Head() + commit, err := localRepo.CommitObject(head.Hash()) + Expect(err).ToNot(HaveOccurred()) + Expect(commit.Message).To(Equal(commitMessage)) + }) + + It("has the commit author as given", func() { + head, _ := localRepo.Head() + commit, err := localRepo.CommitObject(head.Hash()) + Expect(err).ToNot(HaveOccurred()) + Expect(commit.Author).NotTo(BeNil()) + Expect(commit.Author.Name).To(Equal(authorName)) + Expect(commit.Author.Email).To(Equal(authorEmail)) + }) + }) + + Context("ref cross-ns GitRepository", func() { + var ( + localRepo *git.Repository + commitMessage string + ) + + 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 + +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 = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: repoURL, + RemoteName: "origin", + ReferenceName: plumbing.NewBranchReferenceName(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()) + 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) + + // 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()) + }) + + // 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 + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: gitRepoKey.Name, + Namespace: gitRepoKey.Namespace, }, GitSpec: &imagev1.GitSpec{ Checkout: &imagev1.GitCheckoutSpec{ @@ -380,9 +560,10 @@ Images: Strategy: imagev1.UpdateStrategySetters, Path: "./yes", }, - SourceRef: imagev1.SourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: gitRepoKey.Name, + Namespace: gitRepoKey.Namespace, }, GitSpec: &imagev1.GitSpec{ Checkout: &imagev1.GitCheckoutSpec{ @@ -524,9 +705,10 @@ Images: Namespace: updateKey.Namespace, }, Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.SourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, + 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{ @@ -720,9 +902,10 @@ Images: update = &imagev1.ImageUpdateAutomation{ Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.SourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: gitRepoKey.Name, + Namespace: gitRepoKey.Namespace, }, Update: &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, @@ -838,9 +1021,10 @@ Images: }, 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.SourceReference{ - Kind: "GitRepository", - Name: gitRepoKey.Name, + SourceRef: imagev1.CrossNamespaceSourceReference{ + Kind: "GitRepository", + Name: gitRepoKey.Name, + Namespace: gitRepoKey.Namespace, }, Update: &imagev1.UpdateStrategy{ Strategy: imagev1.UpdateStrategySetters, @@ -960,9 +1144,10 @@ Images: Namespace: key.Namespace, }, Spec: imagev1.ImageUpdateAutomationSpec{ - SourceRef: imagev1.SourceReference{ - Kind: "GitRepository", - Name: "garbage", + 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{ diff --git a/docs/api/image-automation.md b/docs/api/image-automation.md index ac038a54..6e896adb 100644 --- a/docs/api/image-automation.md +++ b/docs/api/image-automation.md @@ -119,6 +119,74 @@ string +

CrossNamespaceSourceReference +

+

+(Appears on: +ImageUpdateAutomationSpec) +

+

CrossNamespaceSourceReference contains enough information to let you locate the +typed Kubernetes resource object at cluster level.

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+ +string + +
+(Optional) +

API version of the referent.

+
+kind
+ +string + +
+

Kind of the referent.

+
+name
+ +string + +
+

Name of the referent.

+
+namespace
+ +string + +
+(Optional) +

Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference.

+
+
+

GitCheckoutSpec

@@ -262,8 +330,8 @@ ImageUpdateAutomationSpec sourceRef
- -SourceReference + +CrossNamespaceSourceReference @@ -370,8 +438,8 @@ ImageUpdateAutomationStatus sourceRef
- -SourceReference + +CrossNamespaceSourceReference @@ -616,62 +684,6 @@ ImageUpdateAutomation.

-

SourceReference -

-

-(Appears on: -ImageUpdateAutomationSpec) -

-

SourceReference contains enough information to let you locate the -typed, referenced source object.

-
-
- - - - - - - - - - - - - - - - - - - - - -
FieldDescription
-apiVersion
- -string - -
-(Optional) -

API version of the referent

-
-kind
- -string - -
-

Kind of the referent

-
-name
- -string - -
-

Name of the referent

-
-
-

UpdateStrategy