Skip to content

Commit a582871

Browse files
committed
Make sure reconcile request annotation works
The convention among GOTK controllers is to use a "reconcile request" annotation to force a reconcilation, outside of spec or dependency changes. This is used by e.g., the incoming webhooks handler. The predicate `ChangePredicate`, already used by this controller, takes this into account by allowing events that either caused the generation to increment, _or_ changed the reconcile request annotation. This commit adds a test that the automation will indeed run when the annotation is set. This is a little delicate, because I have to rule out _other_ reasons it might run. To do so, the test makes a change to the git repo that will be overwritten by an automation run -- a commit will not trigger a Reconcile call since it's entirely outside Kubernetes. Signed-off-by: Michael Bridgen <michael@weave.works>
1 parent 8bc349d commit a582871

5 files changed

+112
-38
lines changed

api/v1alpha1/imageupdateautomation_types.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,8 @@ type ImageUpdateAutomationStatus struct {
102102
// +optional
103103
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
104104
// +optional
105-
Conditions []metav1.Condition `json:"conditions,omitempty"`
105+
Conditions []metav1.Condition `json:"conditions,omitempty"`
106+
meta.ReconcileRequestStatus `json:",inline"`
106107
}
107108

108109
const (

api/v1alpha1/zz_generated.deepcopy.go

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/image.toolkit.fluxcd.io_imageupdateautomations.yaml

+4
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,10 @@ spec:
191191
made).
192192
format: date-time
193193
type: string
194+
lastHandledReconcileAt:
195+
description: LastHandledReconcileAt holds the value of the most recent
196+
reconcile request value, so a change can be detected.
197+
type: string
194198
observedGeneration:
195199
format: int64
196200
type: integer

controllers/imageupdateautomation_controller.go

+9
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,15 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(req ctrl.Request) (ctrl.Resu
8787
return ctrl.Result{}, client.IgnoreNotFound(err)
8888
}
8989

90+
// whatever else happens, we've now "seen" the reconcile
91+
// annotation if it's there
92+
if token, ok := meta.ReconcileAnnotationValue(auto.GetAnnotations()); ok {
93+
auto.Status.SetLastHandledReconcileRequest(token)
94+
if err := r.Status().Update(ctx, &auto); err != nil {
95+
return ctrl.Result{Requeue: true}, err
96+
}
97+
}
98+
9099
if auto.Spec.Suspend {
91100
msg := "ImageUpdateAutomation is suspended, skipping automation run"
92101
imagev1.SetImageUpdateAutomationReadiness(

controllers/update_test.go

+96-37
Original file line numberDiff line numberDiff line change
@@ -188,29 +188,9 @@ var _ = Describe("ImageUpdateAutomation", func() {
188188
BeforeEach(func() {
189189
// Insert a setter reference into the deployment file,
190190
// before creating the automation object itself.
191-
tmp, err := ioutil.TempDir("", "gotest-imageauto-setters")
192-
Expect(err).ToNot(HaveOccurred())
193-
defer os.RemoveAll(tmp)
194-
repo, err := git.PlainClone(tmp, false, &git.CloneOptions{
195-
URL: repoURL,
196-
ReferenceName: plumbing.NewBranchReferenceName(defaultBranch),
197-
})
198-
Expect(err).ToNot(HaveOccurred())
199-
200-
replaceMarker(tmp, policyKey)
201-
worktree, err := repo.Worktree()
202-
Expect(err).ToNot(HaveOccurred())
203-
_, err = worktree.Add("deploy.yaml")
204-
Expect(err).ToNot(HaveOccurred())
205-
_, err = worktree.Commit("Install setter marker", &git.CommitOptions{
206-
Author: &object.Signature{
207-
Name: "Testbot",
208-
Email: "test@example.com",
209-
When: time.Now(),
210-
},
191+
commitInRepo(repoURL, "Install setter marker", func(tmp string) {
192+
replaceMarker(tmp, policyKey)
211193
})
212-
Expect(err).ToNot(HaveOccurred())
213-
Expect(repo.Push(&git.PushOptions{RemoteName: "origin"})).To(Succeed())
214194

215195
// pull the head commit we just pushed, so it's not
216196
// considered a new commit when checking for a commit
@@ -229,6 +209,7 @@ var _ = Describe("ImageUpdateAutomation", func() {
229209
Namespace: updateKey.Namespace,
230210
},
231211
Spec: imagev1.ImageUpdateAutomationSpec{
212+
RunInterval: &metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing
232213
Checkout: imagev1.GitCheckoutSpec{
233214
GitRepositoryRef: corev1.LocalObjectReference{
234215
Name: gitRepoKey.Name,
@@ -259,22 +240,9 @@ var _ = Describe("ImageUpdateAutomation", func() {
259240
Expect(err).ToNot(HaveOccurred())
260241
Expect(commit.Message).To(Equal(commitMessage))
261242

262-
tmp, err := ioutil.TempDir("", "gotest-imageauto")
263-
Expect(err).ToNot(HaveOccurred())
264-
defer os.RemoveAll(tmp)
265-
266-
expected, err := ioutil.TempDir("", "gotest-imageauto-expected")
267-
Expect(err).ToNot(HaveOccurred())
268-
defer os.RemoveAll(expected)
269-
copy.Copy("testdata/appconfig-setters-expected", expected)
270-
replaceMarker(expected, policyKey)
271-
272-
_, err = git.PlainClone(tmp, false, &git.CloneOptions{
273-
URL: repoURL,
274-
ReferenceName: plumbing.NewBranchReferenceName(defaultBranch),
243+
compareRepoWithExpected(repoURL, "testdata/appconfig-setters-expected", func(tmp string) {
244+
replaceMarker(tmp, policyKey)
275245
})
276-
Expect(err).ToNot(HaveOccurred())
277-
test.ExpectMatchingDirectories(tmp, expected)
278246
})
279247

280248
It("stops updating when suspended", func() {
@@ -292,6 +260,52 @@ var _ = Describe("ImageUpdateAutomation", func() {
292260
return ready != nil && ready.Status == metav1.ConditionFalse && ready.Reason == meta.SuspendedReason
293261
}, timeout, time.Second).Should(BeTrue())
294262
})
263+
264+
It("runs when the reconcile request annotation is added", func() {
265+
// the automation has run, and is not expected to run
266+
// again for 2 hours. Make a commit to the git repo
267+
// which needs to be undone by automation, then add
268+
// the annotation and make sure it runs again.
269+
Expect(k8sClient.Get(context.Background(), updateKey, updateBySetters)).To(Succeed())
270+
lastRun := updateBySetters.Status.LastAutomationRunTime
271+
Expect(lastRun).ToNot(BeNil())
272+
273+
commitInRepo(repoURL, "Revert image update", func(tmp string) {
274+
// revert the change made by copying the old version
275+
// of the file back over then restoring the setter
276+
// marker
277+
copy.Copy("testdata/appconfig/deploy.yaml", filepath.Join(tmp, "deploy.yaml"))
278+
replaceMarker(tmp, policyKey)
279+
})
280+
// check that it was reverted
281+
compareRepoWithExpected(repoURL, "testdata/appconfig", func(tmp string) {
282+
replaceMarker(tmp, policyKey)
283+
})
284+
285+
ts := time.Now().String()
286+
var updatePatch imagev1.ImageUpdateAutomation
287+
updatePatch.Name = updateKey.Name
288+
updatePatch.Namespace = updateKey.Namespace
289+
updatePatch.ObjectMeta.Annotations = map[string]string{
290+
meta.ReconcileRequestAnnotation: ts,
291+
}
292+
Expect(k8sClient.Patch(context.Background(), &updatePatch, client.Merge)).To(Succeed())
293+
294+
Eventually(func() bool {
295+
if err := k8sClient.Get(context.Background(), updateKey, updateBySetters); err != nil {
296+
return false
297+
}
298+
newLastRun := updateBySetters.Status.LastAutomationRunTime
299+
return newLastRun != nil && newLastRun.Time.After(lastRun.Time)
300+
}, timeout, time.Second).Should(BeTrue())
301+
// check that the annotation was recorded as seen
302+
Expect(updateBySetters.Status.LastHandledReconcileAt).To(Equal(ts))
303+
304+
// check that a new commit was made
305+
compareRepoWithExpected(repoURL, "testdata/appconfig-setters-expected", func(tmp string) {
306+
replaceMarker(tmp, policyKey)
307+
})
308+
})
295309
})
296310
})
297311
})
@@ -326,6 +340,51 @@ func waitForNewHead(repo *git.Repository) {
326340
}, timeout, time.Second).Should(BeTrue())
327341
}
328342

343+
func compareRepoWithExpected(repoURL, fixture string, changeFixture func(tmp string)) {
344+
expected, err := ioutil.TempDir("", "gotest-imageauto-expected")
345+
Expect(err).ToNot(HaveOccurred())
346+
defer os.RemoveAll(expected)
347+
copy.Copy(fixture, expected)
348+
changeFixture(expected)
349+
350+
tmp, err := ioutil.TempDir("", "gotest-imageauto")
351+
Expect(err).ToNot(HaveOccurred())
352+
defer os.RemoveAll(tmp)
353+
_, err = git.PlainClone(tmp, false, &git.CloneOptions{
354+
URL: repoURL,
355+
ReferenceName: plumbing.NewBranchReferenceName(defaultBranch),
356+
})
357+
Expect(err).ToNot(HaveOccurred())
358+
test.ExpectMatchingDirectories(tmp, expected)
359+
}
360+
361+
func commitInRepo(repoURL, msg string, changeFiles func(path string)) {
362+
tmp, err := ioutil.TempDir("", "gotest-imageauto")
363+
Expect(err).ToNot(HaveOccurred())
364+
defer os.RemoveAll(tmp)
365+
repo, err := git.PlainClone(tmp, false, &git.CloneOptions{
366+
URL: repoURL,
367+
ReferenceName: plumbing.NewBranchReferenceName(defaultBranch),
368+
})
369+
Expect(err).ToNot(HaveOccurred())
370+
371+
changeFiles(tmp)
372+
373+
worktree, err := repo.Worktree()
374+
Expect(err).ToNot(HaveOccurred())
375+
_, err = worktree.Add(".")
376+
Expect(err).ToNot(HaveOccurred())
377+
_, err = worktree.Commit(msg, &git.CommitOptions{
378+
Author: &object.Signature{
379+
Name: "Testbot",
380+
Email: "test@example.com",
381+
When: time.Now(),
382+
},
383+
})
384+
Expect(err).ToNot(HaveOccurred())
385+
Expect(repo.Push(&git.PushOptions{RemoteName: "origin"})).To(Succeed())
386+
}
387+
329388
// Initialise a git server with a repo including the files in dir.
330389
func initGitRepo(gitServer *gittestserver.GitServer, fixture, repositoryPath string) error {
331390
fs := memfs.New()

0 commit comments

Comments
 (0)