Skip to content

Commit 908f8b7

Browse files
committed
Supply values to the commit message template
This commit: - passes a value including the update result to the commit message template - gives the template result a method for enumerating the objects regardless of file This means you can access the images updated either by file (`.Files`), by object (`.Objects()`), or just as a list (`.Images()`). The additional test case shows how to use these. Signed-off-by: Michael Bridgen <[email protected]>
1 parent 8daa649 commit 908f8b7

File tree

4 files changed

+267
-4
lines changed

4 files changed

+267
-4
lines changed

controllers/imageupdateautomation_controller.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ const defaultMessageTemplate = `Update from image update automation`
6969
const repoRefKey = ".spec.gitRepository"
7070
const imagePolicyKey = ".spec.update.imagePolicy"
7171

72+
type TemplateValues struct {
73+
AutomationObject types.NamespacedName
74+
Updated update.Result
75+
}
76+
7277
// ImageUpdateAutomationReconciler reconciles a ImageUpdateAutomation object
7378
type ImageUpdateAutomationReconciler struct {
7479
client.Client
@@ -85,6 +90,7 @@ type ImageUpdateAutomationReconciler struct {
8590
func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
8691
log := logr.FromContext(ctx)
8792
now := time.Now()
93+
var templateValues TemplateValues
8894

8995
var auto imagev1.ImageUpdateAutomation
9096
if err := r.Get(ctx, req.NamespacedName, &auto); err != nil {
@@ -96,6 +102,8 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
96102
return ctrl.Result{}, nil
97103
}
98104

105+
templateValues.AutomationObject = req.NamespacedName
106+
99107
// Record readiness metric when exiting; if there's any points at
100108
// which the readiness is updated _without also exiting_, they
101109
// should also record the readiness.
@@ -178,8 +186,10 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
178186
return failWithError(err)
179187
}
180188

181-
if _, err := updateAccordingToSetters(ctx, tmp, policies.Items); err != nil {
189+
if result, err := updateAccordingToSetters(ctx, tmp, policies.Items); err != nil {
182190
return failWithError(err)
191+
} else {
192+
templateValues.Updated = result
183193
}
184194
default:
185195
log.Info("no update strategy given in the spec")
@@ -197,7 +207,7 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
197207
// The status message depends on what happens next. Since there's
198208
// more than one way to succeed, there's some if..else below, and
199209
// early returns only on failure.
200-
if rev, err := commitAll(ctx, repo, &auto.Spec.Commit); err != nil {
210+
if rev, err := commitAll(ctx, repo, &auto.Spec.Commit, templateValues); err != nil {
201211
if err == errNoChanges {
202212
r.event(ctx, auto, events.EventSeverityInfo, "no updates made")
203213
log.V(debug).Info("no changes made in working directory; no commit")
@@ -348,7 +358,7 @@ func cloneInto(ctx context.Context, access repoAccess, branch, path, impl string
348358

349359
var errNoChanges error = errors.New("no changes made to working directory")
350360

351-
func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec) (string, error) {
361+
func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateValues) (string, error) {
352362
working, err := repo.Worktree()
353363
if err != nil {
354364
return "", err
@@ -370,7 +380,7 @@ func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.Comm
370380
return "", err
371381
}
372382
buf := &strings.Builder{}
373-
if err := tmpl.Execute(buf, "no data! yet"); err != nil {
383+
if err := tmpl.Execute(buf, values); err != nil {
374384
return "", err
375385
}
376386

controllers/update_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,157 @@ var _ = Describe("ImageUpdateAutomation", func() {
111111
Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed())
112112
})
113113

114+
Context("commit message template", func() {
115+
116+
var (
117+
localRepo *git.Repository
118+
commitMessage string
119+
)
120+
121+
const (
122+
commitTemplate = `Commit summary
123+
124+
Automation: {{ .AutomationObject }}
125+
126+
Files:
127+
{{ range $filename, $_ := .Updated.Files -}}
128+
- {{ $filename }}
129+
{{ end -}}
130+
131+
Objects:
132+
{{ range $resource, $_ := .Updated.Objects -}}
133+
- {{ $resource.Kind }} {{ $resource.Name }}
134+
{{ end -}}
135+
136+
Images:
137+
{{ range .Updated.Images -}}
138+
- {{.}}
139+
{{ end -}}
140+
`
141+
commitMessageFmt = `Commit summary
142+
143+
Automation: %s/update-test
144+
145+
Files:
146+
- deploy.yaml
147+
Objects:
148+
- Deployment test
149+
Images:
150+
- helloworld:v1.0.0
151+
`
152+
)
153+
154+
BeforeEach(func() {
155+
commitMessage = fmt.Sprintf(commitMessageFmt, namespace.Name)
156+
157+
Expect(initGitRepo(gitServer, "testdata/appconfig", branch, repositoryPath)).To(Succeed())
158+
repoURL := gitServer.HTTPAddressWithCredentials() + repositoryPath
159+
var err error
160+
localRepo, err = git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{
161+
URL: repoURL,
162+
RemoteName: "origin",
163+
ReferenceName: plumbing.NewBranchReferenceName(branch),
164+
})
165+
Expect(err).ToNot(HaveOccurred())
166+
167+
gitRepoKey := types.NamespacedName{
168+
Name: "image-auto-" + randStringRunes(5),
169+
Namespace: namespace.Name,
170+
}
171+
gitRepo := &sourcev1.GitRepository{
172+
ObjectMeta: metav1.ObjectMeta{
173+
Name: gitRepoKey.Name,
174+
Namespace: namespace.Name,
175+
},
176+
Spec: sourcev1.GitRepositorySpec{
177+
URL: repoURL,
178+
Interval: metav1.Duration{Duration: time.Minute},
179+
},
180+
}
181+
Expect(k8sClient.Create(context.Background(), gitRepo)).To(Succeed())
182+
policyKey := types.NamespacedName{
183+
Name: "policy-" + randStringRunes(5),
184+
Namespace: namespace.Name,
185+
}
186+
// NB not testing the image reflector controller; this
187+
// will make a "fully formed" ImagePolicy object.
188+
policy := &imagev1_reflect.ImagePolicy{
189+
ObjectMeta: metav1.ObjectMeta{
190+
Name: policyKey.Name,
191+
Namespace: policyKey.Namespace,
192+
},
193+
Spec: imagev1_reflect.ImagePolicySpec{
194+
ImageRepositoryRef: meta.LocalObjectReference{
195+
Name: "not-expected-to-exist",
196+
},
197+
Policy: imagev1_reflect.ImagePolicyChoice{
198+
SemVer: &imagev1_reflect.SemVerPolicy{
199+
Range: "1.x",
200+
},
201+
},
202+
},
203+
Status: imagev1_reflect.ImagePolicyStatus{
204+
LatestImage: "helloworld:v1.0.0",
205+
},
206+
}
207+
Expect(k8sClient.Create(context.Background(), policy)).To(Succeed())
208+
Expect(k8sClient.Status().Update(context.Background(), policy)).To(Succeed())
209+
210+
// Insert a setter reference into the deployment file,
211+
// before creating the automation object itself.
212+
commitInRepo(repoURL, branch, "Install setter marker", func(tmp string) {
213+
replaceMarker(tmp, policyKey)
214+
})
215+
216+
// pull the head commit we just pushed, so it's not
217+
// considered a new commit when checking for a commit
218+
// made by automation.
219+
waitForNewHead(localRepo, branch)
220+
221+
// now create the automation object, and let it (one
222+
// hopes!) make a commit itself.
223+
updateKey := types.NamespacedName{
224+
Namespace: namespace.Name,
225+
Name: "update-test",
226+
}
227+
updateBySetters := &imagev1.ImageUpdateAutomation{
228+
ObjectMeta: metav1.ObjectMeta{
229+
Name: updateKey.Name,
230+
Namespace: updateKey.Namespace,
231+
},
232+
Spec: imagev1.ImageUpdateAutomationSpec{
233+
Interval: metav1.Duration{Duration: 2 * time.Hour}, // this is to ensure any subsequent run should be outside the scope of the testing
234+
Checkout: imagev1.GitCheckoutSpec{
235+
GitRepositoryRef: meta.LocalObjectReference{
236+
Name: gitRepoKey.Name,
237+
},
238+
Branch: branch,
239+
},
240+
Update: &imagev1.UpdateStrategy{
241+
Strategy: imagev1.UpdateStrategySetters,
242+
},
243+
Commit: imagev1.CommitSpec{
244+
MessageTemplate: commitTemplate,
245+
},
246+
},
247+
}
248+
Expect(k8sClient.Create(context.Background(), updateBySetters)).To(Succeed())
249+
// wait for a new commit to be made by the controller
250+
waitForNewHead(localRepo, branch)
251+
})
252+
253+
AfterEach(func() {
254+
Expect(k8sClient.Delete(context.Background(), namespace)).To(Succeed())
255+
})
256+
257+
It("formats the commit message as in the template", func() {
258+
head, _ := localRepo.Head()
259+
commit, err := localRepo.CommitObject(head.Hash())
260+
Expect(err).ToNot(HaveOccurred())
261+
Expect(commit.Message).To(Equal(commitMessage))
262+
})
263+
})
264+
114265
endToEnd := func(impl, proto string) func() {
115266
return func() {
116267
var (

pkg/update/result.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,33 @@ type Result struct {
1616
type FileResult struct {
1717
Objects map[yaml.ResourceIdentifier][]name.Reference
1818
}
19+
20+
// Images returns all the images that were involved in at least one
21+
// update.
22+
func (r Result) Images() []name.Reference {
23+
seen := make(map[name.Reference]struct{})
24+
var result []name.Reference
25+
for _, file := range r.Files {
26+
for _, images := range file.Objects {
27+
for _, ref := range images {
28+
if _, ok := seen[ref]; !ok {
29+
seen[ref] = struct{}{}
30+
result = append(result, ref)
31+
}
32+
}
33+
}
34+
}
35+
return result
36+
}
37+
38+
// Objects returns a map of all the objects against the images updated
39+
// within, regardless of which file they appear in.
40+
func (r Result) Objects() map[yaml.ResourceIdentifier][]name.Reference {
41+
result := make(map[yaml.ResourceIdentifier][]name.Reference)
42+
for _, file := range r.Files {
43+
for res, refs := range file.Objects {
44+
result[res] = append(result[res], refs...)
45+
}
46+
}
47+
return result
48+
}

pkg/update/result_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package update
2+
3+
import (
4+
"github.com/google/go-containerregistry/pkg/name"
5+
. "github.com/onsi/ginkgo"
6+
. "github.com/onsi/gomega"
7+
"sigs.k8s.io/kustomize/kyaml/yaml"
8+
)
9+
10+
func mustRef(ref string) name.Reference {
11+
r, err := name.ParseReference(ref)
12+
if err != nil {
13+
panic(err)
14+
}
15+
return r
16+
}
17+
18+
var _ = Describe("update results", func() {
19+
20+
var result Result
21+
objectNames := []yaml.ResourceIdentifier{
22+
yaml.ResourceIdentifier{
23+
NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"},
24+
},
25+
yaml.ResourceIdentifier{
26+
NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"},
27+
},
28+
}
29+
30+
BeforeEach(func() {
31+
result = Result{
32+
Files: map[string]FileResult{
33+
"foo.yaml": {
34+
Objects: map[yaml.ResourceIdentifier][]name.Reference{
35+
objectNames[0]: {
36+
mustRef("image:v1.0"),
37+
mustRef("other:v2.0"),
38+
},
39+
},
40+
},
41+
"bar.yaml": {
42+
Objects: map[yaml.ResourceIdentifier][]name.Reference{
43+
objectNames[1]: {
44+
mustRef("image:v1.0"),
45+
mustRef("other:v2.0"),
46+
},
47+
},
48+
},
49+
},
50+
}
51+
})
52+
53+
It("deduplicates images", func() {
54+
Expect(result.Images()).To(Equal([]name.Reference{
55+
mustRef("image:v1.0"),
56+
mustRef("other:v2.0"),
57+
}))
58+
})
59+
60+
It("collects images by object", func() {
61+
Expect(result.Objects()).To(Equal(map[yaml.ResourceIdentifier][]name.Reference{
62+
objectNames[0]: {
63+
mustRef("image:v1.0"),
64+
mustRef("other:v2.0"),
65+
},
66+
objectNames[1]: {
67+
mustRef("image:v1.0"),
68+
mustRef("other:v2.0"),
69+
},
70+
}))
71+
})
72+
})

0 commit comments

Comments
 (0)