Adapt pkg/update so it returns a report of updates
It's desirable (see #6) to be able to enumerate the updates that were made by automation, in the commit message and perhaps in an event announcing success. Doing this is counter-intuitively difficult. A `kyaml.setters2.Set` filter will keep a count of the times its used. Previously, one `Set` was used with the `SetAll` flag set, which would replace any marker that corresponded to an image, in one traversal. But to keep track of images individually, you need to have a setter for _each_ image (and its tag, and its name, since those can be used separately). This means `3 x policies` traversals of each node! The saving grace, possibly, is that only files with a marker in them are considered. Since you might want to dice the results in different ways, the result returned is a nested map of file->object->image. Signed-off-by: Michael Bridgen <michael@weave.works>
This commit is contained in:
parent
351b7b6fb6
commit
fbdfa78e87
|
|
@ -178,7 +178,7 @@ func (r *ImageUpdateAutomationReconciler) Reconcile(ctx context.Context, req ctr
|
||||||
return failWithError(err)
|
return failWithError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := updateAccordingToSetters(ctx, tmp, policies.Items); err != nil {
|
if _, err := updateAccordingToSetters(ctx, tmp, policies.Items); err != nil {
|
||||||
return failWithError(err)
|
return failWithError(err)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
@ -523,6 +523,6 @@ func (r *ImageUpdateAutomationReconciler) recordReadinessMetric(ctx context.Cont
|
||||||
|
|
||||||
// updateAccordingToSetters updates files under the root by treating
|
// updateAccordingToSetters updates files under the root by treating
|
||||||
// the given image policies as kyaml setters.
|
// the given image policies as kyaml setters.
|
||||||
func updateAccordingToSetters(ctx context.Context, path string, policies []imagev1_reflect.ImagePolicy) error {
|
func updateAccordingToSetters(ctx context.Context, path string, policies []imagev1_reflect.ImagePolicy) (update.Result, error) {
|
||||||
return update.UpdateWithSetters(path, path, policies)
|
return update.UpdateWithSetters(path, path, policies)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package update
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
|
"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.
|
||||||
|
type Result struct {
|
||||||
|
Files map[string]FileResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileResult struct {
|
||||||
|
Objects map[yaml.ResourceIdentifier][]name.Reference
|
||||||
|
}
|
||||||
|
|
@ -23,8 +23,11 @@ import (
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
"sigs.k8s.io/kustomize/kyaml/fieldmeta"
|
||||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/kio/kioutil"
|
||||||
"sigs.k8s.io/kustomize/kyaml/openapi"
|
"sigs.k8s.io/kustomize/kyaml/openapi"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/sets"
|
||||||
"sigs.k8s.io/kustomize/kyaml/setters2"
|
"sigs.k8s.io/kustomize/kyaml/setters2"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
|
||||||
imagev1alpha1_reflect "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
|
imagev1alpha1_reflect "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
|
||||||
)
|
)
|
||||||
|
|
@ -47,7 +50,7 @@ func init() {
|
||||||
// UpdateWithSetters takes all YAML files from `inpath`, updates any
|
// UpdateWithSetters takes all YAML files from `inpath`, updates any
|
||||||
// that contain an "in scope" image policy marker, and writes files it
|
// that contain an "in scope" image policy marker, and writes files it
|
||||||
// updated (and only those files) back to `outpath`.
|
// updated (and only those files) back to `outpath`.
|
||||||
func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.ImagePolicy) error {
|
func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.ImagePolicy) (Result, error) {
|
||||||
// the OpenAPI schema is a package variable in kyaml/openapi. In
|
// the OpenAPI schema is a package variable in kyaml/openapi. In
|
||||||
// lieu of being able to isolate invocations (per
|
// lieu of being able to isolate invocations (per
|
||||||
// https://github.com/kubernetes-sigs/kustomize/issues/3058), I
|
// https://github.com/kubernetes-sigs/kustomize/issues/3058), I
|
||||||
|
|
@ -83,6 +86,12 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
|
||||||
// used to separate namespace and name in the key, because a slash
|
// used to separate namespace and name in the key, because a slash
|
||||||
// would be interpreted as part of the $ref path.
|
// would be interpreted as part of the $ref path.
|
||||||
|
|
||||||
|
var settersSchema spec.Schema
|
||||||
|
var setters []*setters2.Set
|
||||||
|
setterToImage := make(map[string]name.Reference)
|
||||||
|
|
||||||
|
// collect setter defs and setters by going through all the image
|
||||||
|
// policies available.
|
||||||
defs := map[string]spec.Schema{}
|
defs := map[string]spec.Schema{}
|
||||||
for _, policy := range policies {
|
for _, policy := range policies {
|
||||||
if policy.Status.LatestImage == "" {
|
if policy.Status.LatestImage == "" {
|
||||||
|
|
@ -97,7 +106,7 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
|
||||||
image := policy.Status.LatestImage
|
image := policy.Status.LatestImage
|
||||||
ref, err := name.ParseReference(image, name.WeakValidation)
|
ref, err := name.ParseReference(image, name.WeakValidation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("encountered invalid image ref %q: %w", policy.Status.LatestImage, err)
|
return Result{}, fmt.Errorf("encountered invalid image ref %q: %w", policy.Status.LatestImage, err)
|
||||||
}
|
}
|
||||||
tag := ref.Identifier()
|
tag := ref.Identifier()
|
||||||
// annoyingly, neither the library imported above, nor an
|
// annoyingly, neither the library imported above, nor an
|
||||||
|
|
@ -107,15 +116,35 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
|
||||||
|
|
||||||
imageSetter := fmt.Sprintf("%s:%s", policy.GetNamespace(), policy.GetName())
|
imageSetter := fmt.Sprintf("%s:%s", policy.GetNamespace(), policy.GetName())
|
||||||
defs[fieldmeta.SetterDefinitionPrefix+imageSetter] = setterSchema(imageSetter, policy.Status.LatestImage)
|
defs[fieldmeta.SetterDefinitionPrefix+imageSetter] = setterSchema(imageSetter, policy.Status.LatestImage)
|
||||||
|
setterToImage[imageSetter] = ref
|
||||||
|
setters = append(setters, &setters2.Set{
|
||||||
|
Name: imageSetter,
|
||||||
|
SettersSchema: &settersSchema,
|
||||||
|
})
|
||||||
|
|
||||||
tagSetter := imageSetter + ":tag"
|
tagSetter := imageSetter + ":tag"
|
||||||
|
|
||||||
defs[fieldmeta.SetterDefinitionPrefix+tagSetter] = setterSchema(tagSetter, tag)
|
defs[fieldmeta.SetterDefinitionPrefix+tagSetter] = setterSchema(tagSetter, tag)
|
||||||
|
setterToImage[tagSetter] = ref
|
||||||
|
setters = append(setters, &setters2.Set{
|
||||||
|
Name: tagSetter,
|
||||||
|
SettersSchema: &settersSchema,
|
||||||
|
})
|
||||||
|
|
||||||
// Context().Name() gives the image repository _as supplied_
|
// Context().Name() gives the image repository _as supplied_
|
||||||
nameSetter := imageSetter + ":name"
|
nameSetter := imageSetter + ":name"
|
||||||
|
setterToImage[nameSetter] = ref
|
||||||
defs[fieldmeta.SetterDefinitionPrefix+nameSetter] = setterSchema(nameSetter, name)
|
defs[fieldmeta.SetterDefinitionPrefix+nameSetter] = setterSchema(nameSetter, name)
|
||||||
|
setters = append(setters, &setters2.Set{
|
||||||
|
Name: nameSetter,
|
||||||
|
SettersSchema: &settersSchema,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
var settersSchema spec.Schema
|
|
||||||
settersSchema.Definitions = defs
|
settersSchema.Definitions = defs
|
||||||
|
setAll := &setAllRecorder{
|
||||||
|
setters: setters,
|
||||||
|
}
|
||||||
|
|
||||||
// get ready with the reader and writer
|
// get ready with the reader and writer
|
||||||
reader := &ScreeningLocalReader{
|
reader := &ScreeningLocalReader{
|
||||||
|
|
@ -130,18 +159,102 @@ func UpdateWithSetters(inpath, outpath string, policies []imagev1alpha1_reflect.
|
||||||
Inputs: []kio.Reader{reader},
|
Inputs: []kio.Reader{reader},
|
||||||
Outputs: []kio.Writer{writer},
|
Outputs: []kio.Writer{writer},
|
||||||
Filters: []kio.Filter{
|
Filters: []kio.Filter{
|
||||||
setters2.SetAll( // run the enclosed single-node setters2.Filter on all nodes,
|
setAll,
|
||||||
// and only include those in files that changed in the output
|
|
||||||
&setters2.Set{
|
|
||||||
SetAll: true,
|
|
||||||
SettersSchema: &settersSchema,
|
|
||||||
}, // set all images that are in the constructed schema
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// go!
|
// go!
|
||||||
return pipeline.Execute()
|
err := pipeline.Execute()
|
||||||
|
if err != nil {
|
||||||
|
return Result{}, err
|
||||||
|
}
|
||||||
|
return setAll.getResult(setterToImage), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type update struct {
|
||||||
|
file, name string
|
||||||
|
object *yaml.RNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type setAllRecorder struct {
|
||||||
|
setters []*setters2.Set
|
||||||
|
updates []update
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *setAllRecorder) getResult(nameToImage map[string]name.Reference) Result {
|
||||||
|
result := Result{
|
||||||
|
Files: make(map[string]FileResult),
|
||||||
|
}
|
||||||
|
updates:
|
||||||
|
for _, update := range s.updates {
|
||||||
|
file, ok := result.Files[update.file]
|
||||||
|
if !ok {
|
||||||
|
file = FileResult{
|
||||||
|
Objects: make(map[yaml.ResourceIdentifier][]name.Reference),
|
||||||
|
}
|
||||||
|
result.Files[update.file] = file
|
||||||
|
}
|
||||||
|
objects := file.Objects
|
||||||
|
|
||||||
|
meta, err := update.object.GetMeta()
|
||||||
|
if err != nil {
|
||||||
|
continue updates
|
||||||
|
}
|
||||||
|
id := 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
|
||||||
|
for _, n := range objects[id] {
|
||||||
|
if n == name {
|
||||||
|
continue updates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objects[id] = append(objects[id], name)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter is an implementation of kio.Filter which records each use of
|
||||||
|
// a setter at each object in each file, and only includes the files
|
||||||
|
// that were updated in the output nodes. The implementation is
|
||||||
|
// adapted from
|
||||||
|
// https://github.com/kubernetes-sigs/kustomize/blob/kyaml/v0.10.13/kyaml/setters2/set.go#L503
|
||||||
|
func (s *setAllRecorder) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||||
|
filesToUpdate := sets.String{}
|
||||||
|
for i := range nodes {
|
||||||
|
for _, setter := range s.setters {
|
||||||
|
preCount := setter.Count
|
||||||
|
_, err := setter.Filter(nodes[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if setter.Count > preCount {
|
||||||
|
path, _, err := kioutil.GetFileAnnotations(nodes[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filesToUpdate.Insert(path)
|
||||||
|
s.updates = append(s.updates, update{
|
||||||
|
file: path,
|
||||||
|
name: setter.Name,
|
||||||
|
object: nodes[i],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var nodesInUpdatedFiles []*yaml.RNode
|
||||||
|
for i := range nodes {
|
||||||
|
path, _, err := kioutil.GetFileAnnotations(nodes[i])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if filesToUpdate.Has(path) {
|
||||||
|
nodesInUpdatedFiles = append(nodesInUpdatedFiles, nodes[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nodesInUpdatedFiles, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func setterSchema(name, value string) spec.Schema {
|
func setterSchema(name, value string) spec.Schema {
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,11 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||||
|
|
||||||
"github.com/fluxcd/image-automation-controller/pkg/test"
|
"github.com/fluxcd/image-automation-controller/pkg/test"
|
||||||
imagev1alpha1_reflect "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
|
imagev1alpha1_reflect "github.com/fluxcd/image-reflector-controller/api/v1alpha1"
|
||||||
|
|
@ -52,7 +54,68 @@ var _ = Describe("Update image via kyaml setters2", func() {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
Expect(UpdateWithSetters("testdata/setters/original", tmp, policies)).To(Succeed())
|
_, err = UpdateWithSetters("testdata/setters/original", tmp, policies)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
test.ExpectMatchingDirectories(tmp, "testdata/setters/expected")
|
test.ExpectMatchingDirectories(tmp, "testdata/setters/expected")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
It("gives the result of the updates", func() {
|
||||||
|
tmp, err := ioutil.TempDir("", "gotest")
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
defer os.RemoveAll(tmp)
|
||||||
|
|
||||||
|
policies := []imagev1alpha1_reflect.ImagePolicy{
|
||||||
|
imagev1alpha1_reflect.ImagePolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{ // name matches marker used in testdata/setters/{original,expected}
|
||||||
|
Namespace: "automation-ns",
|
||||||
|
Name: "policy",
|
||||||
|
},
|
||||||
|
Status: imagev1alpha1_reflect.ImagePolicyStatus{
|
||||||
|
LatestImage: "updated:v1.0.1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := UpdateWithSetters("testdata/setters/original", tmp, policies)
|
||||||
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
|
||||||
|
kustomizeResourceID := yaml.ResourceIdentifier{
|
||||||
|
TypeMeta: yaml.TypeMeta{
|
||||||
|
APIVersion: "kustomize.config.k8s.io/v1beta1",
|
||||||
|
Kind: "Kustomization",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
markedResourceID := yaml.ResourceIdentifier{
|
||||||
|
TypeMeta: yaml.TypeMeta{
|
||||||
|
APIVersion: "batch/v1beta1",
|
||||||
|
Kind: "CronJob",
|
||||||
|
},
|
||||||
|
NameMeta: yaml.NameMeta{
|
||||||
|
Namespace: "bar",
|
||||||
|
Name: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expectedImageRef, _ := name.ParseReference("updated:v1.0.1")
|
||||||
|
|
||||||
|
expectedResult := Result{
|
||||||
|
Files: map[string]FileResult{
|
||||||
|
"kustomization.yaml": {
|
||||||
|
Objects: map[yaml.ResourceIdentifier][]name.Reference{
|
||||||
|
kustomizeResourceID: {
|
||||||
|
expectedImageRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"marked.yaml": {
|
||||||
|
Objects: map[yaml.ResourceIdentifier][]name.Reference{
|
||||||
|
markedResourceID: {
|
||||||
|
expectedImageRef,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(result).To(Equal(expectedResult))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue