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 <sanskar.jaiswal@weave.works>
This commit is contained in:
Sanskar Jaiswal 2022-01-21 13:34:20 +05:30
parent 524b603a72
commit 3de51e7a1e
7 changed files with 325 additions and 106 deletions

View File

@ -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.

View File

@ -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)
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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{

View File

@ -119,6 +119,74 @@ string
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta1.CrossNamespaceSourceReference">CrossNamespaceSourceReference
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta1.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec</a>)
</p>
<p>CrossNamespaceSourceReference contains enough information to let you locate the
typed Kubernetes resource object at cluster level.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiVersion</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>API version of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>kind</code><br>
<em>
string
</em>
</td>
<td>
<p>Kind of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the referent.</p>
</td>
</tr>
<tr>
<td>
<code>namespace</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>Namespace of the referent, defaults to the namespace of the Kubernetes resource object that contains the reference.</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta1.GitCheckoutSpec">GitCheckoutSpec
</h3>
<p>
@ -262,8 +330,8 @@ ImageUpdateAutomationSpec
<td>
<code>sourceRef</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta1.SourceReference">
SourceReference
<a href="#image.toolkit.fluxcd.io/v1beta1.CrossNamespaceSourceReference">
CrossNamespaceSourceReference
</a>
</em>
</td>
@ -370,8 +438,8 @@ ImageUpdateAutomationStatus
<td>
<code>sourceRef</code><br>
<em>
<a href="#image.toolkit.fluxcd.io/v1beta1.SourceReference">
SourceReference
<a href="#image.toolkit.fluxcd.io/v1beta1.CrossNamespaceSourceReference">
CrossNamespaceSourceReference
</a>
</em>
</td>
@ -616,62 +684,6 @@ ImageUpdateAutomation.</p>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta1.SourceReference">SourceReference
</h3>
<p>
(<em>Appears on:</em>
<a href="#image.toolkit.fluxcd.io/v1beta1.ImageUpdateAutomationSpec">ImageUpdateAutomationSpec</a>)
</p>
<p>SourceReference contains enough information to let you locate the
typed, referenced source object.</p>
<div class="md-typeset__scrollwrap">
<div class="md-typeset__table">
<table>
<thead>
<tr>
<th>Field</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<code>apiVersion</code><br>
<em>
string
</em>
</td>
<td>
<em>(Optional)</em>
<p>API version of the referent</p>
</td>
</tr>
<tr>
<td>
<code>kind</code><br>
<em>
string
</em>
</td>
<td>
<p>Kind of the referent</p>
</td>
</tr>
<tr>
<td>
<code>name</code><br>
<em>
string
</em>
</td>
<td>
<p>Name of the referent</p>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h3 id="image.toolkit.fluxcd.io/v1beta1.UpdateStrategy">UpdateStrategy
</h3>
<p>