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 <michael@weave.works>
This commit is contained in:
Michael Bridgen 2021-03-03 11:00:54 +00:00
parent 908f8b775c
commit df7d570ae5
6 changed files with 198 additions and 35 deletions

View File

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

View File

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

View File

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

View File

@ -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"),

View File

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

View File

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