From df7d570ae505bfe94a1b981bc71d8510fdd8654c Mon Sep 17 00:00:00 2001 From: Michael Bridgen Date: Wed, 3 Mar 2021 11:00:54 +0000 Subject: [PATCH] Give details of template data in spec docs This explains the data available to the commit message template in the API guide. While writing it, I realised it could be made more convenient, so: - mask external types by embedding them - make the most useful parts of an image ref available using a wrapper struct and interface Signed-off-by: Michael Bridgen --- .../imageupdateautomation_controller.go | 8 +- docs/spec/v1alpha1/imageupdateautomations.md | 98 +++++++++++++++++++ pkg/update/result.go | 59 +++++++++-- pkg/update/result_test.go | 43 +++++--- pkg/update/setters.go | 10 +- pkg/update/update_test.go | 15 +-- 6 files changed, 198 insertions(+), 35 deletions(-) diff --git a/controllers/imageupdateautomation_controller.go b/controllers/imageupdateautomation_controller.go index ac6f0ea..2f0915a 100644 --- a/controllers/imageupdateautomation_controller.go +++ b/controllers/imageupdateautomation_controller.go @@ -69,7 +69,9 @@ const defaultMessageTemplate = `Update from image update automation` const repoRefKey = ".spec.gitRepository" const imagePolicyKey = ".spec.update.imagePolicy" -type TemplateValues struct { +// TemplateData is the type of the value given to the commit message +// template. +type TemplateData struct { AutomationObject types.NamespacedName Updated update.Result } @@ -90,7 +92,7 @@ type ImageUpdateAutomationReconciler struct { func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { log := logr.FromContext(ctx) now := time.Now() - var templateValues TemplateValues + var templateValues TemplateData var auto imagev1.ImageUpdateAutomation if err := r.Get(ctx, req.NamespacedName, &auto); err != nil { @@ -358,7 +360,7 @@ func cloneInto(ctx context.Context, access repoAccess, branch, path, impl string var errNoChanges error = errors.New("no changes made to working directory") -func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateValues) (string, error) { +func commitAll(ctx context.Context, repo *gogit.Repository, commit *imagev1.CommitSpec, values TemplateData) (string, error) { working, err := repo.Worktree() if err != nil { return "", err diff --git a/docs/spec/v1alpha1/imageupdateautomations.md b/docs/spec/v1alpha1/imageupdateautomations.md index b02ae62..6fe31da 100644 --- a/docs/spec/v1alpha1/imageupdateautomations.md +++ b/docs/spec/v1alpha1/imageupdateautomations.md @@ -156,6 +156,104 @@ spec: [ci skip] ``` +### Commit template data + +The data available to the commit message template have this structure (not reproduced verbatim): + +```go +// controllers/imageupdateautomation_controller.go + +// TemplateData is the type of the value given to the commit message +// template. +type TemplateData struct { + AutomationObject struct { + Name, Namespace string + } + Updated update.Result +} + +// pkg/update/result.go + +// ImageRef represents the image reference used to replace a field +// value in an update. +type ImageRef interface { + // String returns a string representation of the image ref as it + // is used in the update; e.g., "helloworld:v1.0.1" + String() string + // Identifier returns the tag or digest; e.g., "v1.0.1" + Identifier() string + // Repository returns the repository component of the ImageRef, + // with an implied defaults, e.g., "library/helloworld" + Repository() string + // Registry returns the registry component of the ImageRef, e.g., + // "index.docker.io" + Registry() string + // Name gives the fully-qualified reference name, e.g., + // "index.docker.io/library/helloworld:v1.0.1" + Name() string +} + +// ObjectIdentifier holds the identifying data for a particular +// object. This won't always have a name (e.g., a kustomization.yaml). +type ObjectIdentifier struct { + Name, Namespace, APIVersion, Kind string +} + +// Result reports the outcome of an automated update. It has a nested +// structure file->objects->images. Different projections (e.g., all +// the images, regardless of object) are available via methods. +type Result struct { + Files map[string]FileResult +} + +// FileResult gives the updates in a particular file. +type FileResult struct { + Objects map[ObjectIdentifier][]ImageRef +} +``` + +These methods are defined on `update.Result`: + +```go +// Images returns all the images that were involved in at least one +// update. +func (r Result) Images() []ImageRef { + // ... +} + +// Objects returns a map of all the objects against the images updated +// within, regardless of which file they appear in. +func (r Result) Objects() map[ObjectIdentifier][]ImageRef { + // ... +} +``` + +The methods let you range over the objects and images without descending the data structure. Here's +an example of using the fields and methods in a template: + +```go +commitTemplate := ` +`Automated image update + +Automation name: {{ .AutomationObject }} + +Files: +{{ range $filename, $_ := .Updated.Files -}} +- {{ $filename }} +{{ end -}} + +Objects: +{{ range $resource, $_ := .Updated.Objects -}} +- {{ $resource.Kind }} {{ $resource.Name }} +{{ end -}} + +Images: +{{ range .Updated.Images -}} +- {{.}} +{{ end -}} +` +``` + ## Status The status of an `ImageUpdateAutomation` object records the result of the last automation run. diff --git a/pkg/update/result.go b/pkg/update/result.go index 0ceea82..e028e4d 100644 --- a/pkg/update/result.go +++ b/pkg/update/result.go @@ -5,23 +5,62 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) -// Result reports the outcome of an update. It has a -// file->objects->images structure, i.e., from the top level down to the -// most detail. Different projections (e.g., all the images) are -// available via the methods. +// ImageRef represents the image reference used to replace a field +// value in an update. +type ImageRef interface { + // String returns a string representation of the image ref as it + // is used in the update; e.g., "helloworld:v1.0.1" + String() string + // Identifier returns the tag or digest; e.g., "v1.0.1" + Identifier() string + // Repository returns the repository component of the ImageRef, + // with an implied defaults, e.g., "library/helloworld" + Repository() string + // Registry returns the registry component of the ImageRef, e.g., + // "index.docker.io" + Registry() string + // Name gives the fully-qualified reference name, e.g., + // "index.docker.io/library/helloworld:v1.0.1" + Name() string +} + +type imageRef struct { + name.Reference +} + +// Repository gives the repository component of the image ref. +func (i imageRef) Repository() string { + return i.Context().RepositoryStr() +} + +// Registry gives the registry component of the image ref. +func (i imageRef) Registry() string { + return i.Context().Registry.String() +} + +// ObjectIdentifier holds the identifying data for a particular +// object. This won't always have a name (e.g., a kustomization.yaml). +type ObjectIdentifier struct { + yaml.ResourceIdentifier +} + +// Result reports the outcome of an automated update. It has a nested +// structure file->objects->images. Different projections (e.g., all +// the images, regardless of object) are available via methods. type Result struct { Files map[string]FileResult } +// FileResult gives the updates in a particular file. type FileResult struct { - Objects map[yaml.ResourceIdentifier][]name.Reference + Objects map[ObjectIdentifier][]ImageRef } // Images returns all the images that were involved in at least one // update. -func (r Result) Images() []name.Reference { - seen := make(map[name.Reference]struct{}) - var result []name.Reference +func (r Result) Images() []ImageRef { + seen := make(map[ImageRef]struct{}) + var result []ImageRef for _, file := range r.Files { for _, images := range file.Objects { for _, ref := range images { @@ -37,8 +76,8 @@ func (r Result) Images() []name.Reference { // Objects returns a map of all the objects against the images updated // within, regardless of which file they appear in. -func (r Result) Objects() map[yaml.ResourceIdentifier][]name.Reference { - result := make(map[yaml.ResourceIdentifier][]name.Reference) +func (r Result) Objects() map[ObjectIdentifier][]ImageRef { + result := make(map[ObjectIdentifier][]ImageRef) for _, file := range r.Files { for res, refs := range file.Objects { result[res] = append(result[res], refs...) diff --git a/pkg/update/result_test.go b/pkg/update/result_test.go index 214deb4..fcdd676 100644 --- a/pkg/update/result_test.go +++ b/pkg/update/result_test.go @@ -7,31 +7,52 @@ import ( "sigs.k8s.io/kustomize/kyaml/yaml" ) -func mustRef(ref string) name.Reference { +func mustRef(ref string) imageRef { r, err := name.ParseReference(ref) if err != nil { panic(err) } - return r + return imageRef{r} } +var _ = Describe("image ref", func() { + It("gives each component of an image ref", func() { + ref := mustRef("helloworld:v1.0.1") + Expect(ref.String()).To(Equal("helloworld:v1.0.1")) + Expect(ref.Identifier()).To(Equal("v1.0.1")) + Expect(ref.Repository()).To(Equal("library/helloworld")) + Expect(ref.Registry()).To(Equal("index.docker.io")) + Expect(ref.Name()).To(Equal("index.docker.io/library/helloworld:v1.0.1")) + }) + + It("deals with hostnames and digests", func() { + image := "localhost:5000/org/helloworld@sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be" + ref := mustRef(image) + Expect(ref.String()).To(Equal(image)) + Expect(ref.Identifier()).To(Equal("sha256:6745aaad46d795c9836632e1fb62f24b7e7f4c843144da8e47a5465c411a14be")) + Expect(ref.Repository()).To(Equal("org/helloworld")) + Expect(ref.Registry()).To(Equal("localhost:5000")) + Expect(ref.Name()).To(Equal(image)) + }) +}) + var _ = Describe("update results", func() { var result Result - objectNames := []yaml.ResourceIdentifier{ - yaml.ResourceIdentifier{ + objectNames := []ObjectIdentifier{ + ObjectIdentifier{yaml.ResourceIdentifier{ NameMeta: yaml.NameMeta{Namespace: "ns", Name: "foo"}, - }, - yaml.ResourceIdentifier{ + }}, + ObjectIdentifier{yaml.ResourceIdentifier{ NameMeta: yaml.NameMeta{Namespace: "ns", Name: "bar"}, - }, + }}, } BeforeEach(func() { result = Result{ Files: map[string]FileResult{ "foo.yaml": { - Objects: map[yaml.ResourceIdentifier][]name.Reference{ + Objects: map[ObjectIdentifier][]ImageRef{ objectNames[0]: { mustRef("image:v1.0"), mustRef("other:v2.0"), @@ -39,7 +60,7 @@ var _ = Describe("update results", func() { }, }, "bar.yaml": { - Objects: map[yaml.ResourceIdentifier][]name.Reference{ + Objects: map[ObjectIdentifier][]ImageRef{ objectNames[1]: { mustRef("image:v1.0"), mustRef("other:v2.0"), @@ -51,14 +72,14 @@ var _ = Describe("update results", func() { }) It("deduplicates images", func() { - Expect(result.Images()).To(Equal([]name.Reference{ + Expect(result.Images()).To(Equal([]ImageRef{ mustRef("image:v1.0"), mustRef("other:v2.0"), })) }) It("collects images by object", func() { - Expect(result.Objects()).To(Equal(map[yaml.ResourceIdentifier][]name.Reference{ + Expect(result.Objects()).To(Equal(map[ObjectIdentifier][]ImageRef{ objectNames[0]: { mustRef("image:v1.0"), mustRef("other:v2.0"), diff --git a/pkg/update/setters.go b/pkg/update/setters.go index f88d709..d5d79e9 100644 --- a/pkg/update/setters.go +++ b/pkg/update/setters.go @@ -190,7 +190,7 @@ updates: file, ok := result.Files[update.file] if !ok { file = FileResult{ - Objects: make(map[yaml.ResourceIdentifier][]name.Reference), + Objects: make(map[ObjectIdentifier][]ImageRef), } result.Files[update.file] = file } @@ -200,18 +200,20 @@ updates: if err != nil { continue updates } - id := meta.GetIdentifier() + id := ObjectIdentifier{meta.GetIdentifier()} + name, ok := nameToImage[update.name] if !ok { // this means an update was made that wasn't recorded as being an image continue updates } // if the name and tag of an image are both used, we don't need to record it twice + ref := imageRef{name} for _, n := range objects[id] { - if n == name { + if n == ref { continue updates } } - objects[id] = append(objects[id], name) + objects[id] = append(objects[id], ref) } return result } diff --git a/pkg/update/update_test.go b/pkg/update/update_test.go index c3ff347..790b963 100644 --- a/pkg/update/update_test.go +++ b/pkg/update/update_test.go @@ -79,13 +79,13 @@ var _ = Describe("Update image via kyaml setters2", func() { result, err := UpdateWithSetters("testdata/setters/original", tmp, policies) Expect(err).ToNot(HaveOccurred()) - kustomizeResourceID := yaml.ResourceIdentifier{ + kustomizeResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ TypeMeta: yaml.TypeMeta{ APIVersion: "kustomize.config.k8s.io/v1beta1", Kind: "Kustomization", }, - } - markedResourceID := yaml.ResourceIdentifier{ + }} + markedResourceID := ObjectIdentifier{yaml.ResourceIdentifier{ TypeMeta: yaml.TypeMeta{ APIVersion: "batch/v1beta1", Kind: "CronJob", @@ -94,20 +94,21 @@ var _ = Describe("Update image via kyaml setters2", func() { Namespace: "bar", Name: "foo", }, - } - expectedImageRef, _ := name.ParseReference("updated:v1.0.1") + }} + r, _ := name.ParseReference("updated:v1.0.1") + expectedImageRef := imageRef{r} expectedResult := Result{ Files: map[string]FileResult{ "kustomization.yaml": { - Objects: map[yaml.ResourceIdentifier][]name.Reference{ + Objects: map[ObjectIdentifier][]ImageRef{ kustomizeResourceID: { expectedImageRef, }, }, }, "marked.yaml": { - Objects: map[yaml.ResourceIdentifier][]name.Reference{ + Objects: map[ObjectIdentifier][]ImageRef{ markedResourceID: { expectedImageRef, },