From 3de51e7a1e755541dbc97a01777c8dd247ccf922 Mon Sep 17 00:00:00 2001
From: Sanskar Jaiswal
Date: Fri, 21 Jan 2022 13:34:20 +0530
Subject: [PATCH] add support for cross-namespace sourceRef in
ImageUpdateAutomation
ImageUpdateAutomation objects can now refer to GitRepository objects in other
namespaces. Implemented by switching sourceRef from a SourceReference to a
dependency.CrossNamespaceDependencyReference.
Signed-off-by: Sanskar Jaiswal
---
api/v1beta1/imageupdateautomation_types.go | 2 +-
api/v1beta1/reference.go | 27 ++-
api/v1beta1/zz_generated.deepcopy.go | 30 +--
...lkit.fluxcd.io_imageupdateautomations.yaml | 10 +-
.../imageupdateautomation_controller.go | 9 +-
controllers/update_test.go | 221 ++++++++++++++++--
docs/api/image-automation.md | 132 ++++++-----
7 files changed, 325 insertions(+), 106 deletions(-)
diff --git a/api/v1beta1/imageupdateautomation_types.go b/api/v1beta1/imageupdateautomation_types.go
index 542f28f..fe4a040 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 e699f98..595225b 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 93c73fe..5757cdb 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 7e5c9f8..6514823 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 2711ac8..db7b7fe 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 9be17ef..4ecc4e3 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 ac038a5..6e896ad 100644
--- a/docs/api/image-automation.md
+++ b/docs/api/image-automation.md
@@ -119,6 +119,74 @@ string
+
+
+(Appears on:
+ImageUpdateAutomationSpec)
+
+CrossNamespaceSourceReference contains enough information to let you locate the
+typed Kubernetes resource object at cluster level.
+
@@ -262,8 +330,8 @@ ImageUpdateAutomationSpec
sourceRef
-
-SourceReference
+
+CrossNamespaceSourceReference
|
@@ -370,8 +438,8 @@ ImageUpdateAutomationStatus
sourceRef
-
-SourceReference
+
+CrossNamespaceSourceReference
|
@@ -616,62 +684,6 @@ ImageUpdateAutomation.
-
-
-(Appears on:
-ImageUpdateAutomationSpec)
-
-SourceReference contains enough information to let you locate the
-typed, referenced source object.
-