From 2ff38e5358f5c80cffb906b351bad8340d6fbcec Mon Sep 17 00:00:00 2001 From: natasha41575 Date: Wed, 20 Oct 2021 11:43:52 -0700 Subject: [PATCH] provide utility helpers for preserving internal annotations --- api/resource/resource.go | 1 + cmd/config/internal/commands/cat.go | 1 + cmd/config/internal/commands/sink.go | 1 + kyaml/kio/kioutil/kioutil.go | 102 ++++++++++- kyaml/kio/kioutil/kioutil_test.go | 250 +++++++++++++++++++++++++++ 5 files changed, 348 insertions(+), 7 deletions(-) diff --git a/api/resource/resource.go b/api/resource/resource.go index 9e1e753fbbb..4985f725568 100644 --- a/api/resource/resource.go +++ b/api/resource/resource.go @@ -26,6 +26,7 @@ type Resource struct { refVarNames []string } +// nolint var BuildAnnotations = []string{ utils.BuildAnnotationPreviousKinds, utils.BuildAnnotationPreviousNames, diff --git a/cmd/config/internal/commands/cat.go b/cmd/config/internal/commands/cat.go index 62e8f3d968f..ea36bb6e3c0 100644 --- a/cmd/config/internal/commands/cat.go +++ b/cmd/config/internal/commands/cat.go @@ -166,6 +166,7 @@ func (r *CatRunner) catFilters() []kio.Filter { return fltrs } +// nolint: func (r *CatRunner) out(w io.Writer) ([]kio.Writer, error) { var outputs []kio.Writer var functionConfig *yaml.RNode diff --git a/cmd/config/internal/commands/sink.go b/cmd/config/internal/commands/sink.go index 653c0530227..3efd381de83 100644 --- a/cmd/config/internal/commands/sink.go +++ b/cmd/config/internal/commands/sink.go @@ -46,6 +46,7 @@ See discussion in https://github.com/kubernetes-sigs/kustomize/issues/3953.`) return err } +// nolint func (r *SinkRunner) runE(c *cobra.Command, args []string) error { var outputs []kio.Writer if len(args) == 1 { diff --git a/kyaml/kio/kioutil/kioutil.go b/kyaml/kio/kioutil/kioutil.go index bfe2e9334c3..e9304cd34e6 100644 --- a/kyaml/kio/kioutil/kioutil.go +++ b/kyaml/kio/kioutil/kioutil.go @@ -17,25 +17,29 @@ import ( type AnnotationKey = string const ( + // internalPrefix is the prefix given to internal annotations that are used + // internally by the orchestrator + internalPrefix string = "internal.config.kubernetes.io/" + // IndexAnnotation records the index of a specific resource in a file or input stream. - IndexAnnotation AnnotationKey = "internal.config.kubernetes.io/index" + IndexAnnotation AnnotationKey = internalPrefix + "index" // PathAnnotation records the path to the file the Resource was read from - PathAnnotation AnnotationKey = "internal.config.kubernetes.io/path" + PathAnnotation AnnotationKey = internalPrefix + "path" // SeqIndentAnnotation records the sequence nodes indentation of the input resource - SeqIndentAnnotation AnnotationKey = "internal.config.kubernetes.io/seqindent" + SeqIndentAnnotation AnnotationKey = internalPrefix + "seqindent" // IdAnnotation records the id of the resource to map inputs to outputs - IdAnnotation = "internal.config.kubernetes.io/id" + IdAnnotation AnnotationKey = internalPrefix + "id" - // LegacyIndexAnnotation is the deprecated annotation key for resource index + // Deprecated: LegacyIndexAnnotation is deprecated. LegacyIndexAnnotation AnnotationKey = "config.kubernetes.io/index" - // LegacyPathAnnotation is the deprecated annotation key for resource path + // Deprecated: LegacyPathAnnotation is deprecated. LegacyPathAnnotation AnnotationKey = "config.kubernetes.io/path" - // LegacyIdAnnotation is the deprecated annotation key for resource ids + // Deprecated: LegacyIdAnnotation is deprecated. LegacyIdAnnotation = "config.k8s.io/id" ) @@ -311,3 +315,87 @@ func SortNodes(nodes []*yaml.RNode) error { }) return errors.Wrap(err) } + +// CopyInternalAnnotations copies the annotations that begin with the prefix +// `internal.config.kubernetes.io` from the source RNode to the destination RNode. +// It takes a parameter exclusions, which is a list of annotation keys to ignore. +func CopyInternalAnnotations(src *yaml.RNode, dst *yaml.RNode, exclusions ...AnnotationKey) error { + srcAnnotations := GetInternalAnnotations(src) + for k, v := range srcAnnotations { + if stringSliceContains(exclusions, k) { + continue + } + if err := dst.PipeE(yaml.SetAnnotation(k, v)); err != nil { + return err + } + } + return nil +} + +// ConfirmInternalAnnotationUnchanged compares the annotations of the RNodes that begin with the prefix +// `internal.config.kubernetes.io`, throwing an error if they differ. // It takes a parameter exclusions, +// which is a list of annotation keys to ignore. +func ConfirmInternalAnnotationUnchanged(r1 *yaml.RNode, r2 *yaml.RNode, exclusions ...AnnotationKey) error { + r1Annotations := GetInternalAnnotations(r1) + r2Annotations := GetInternalAnnotations(r2) + + // this is a map to prevent duplicates + diffAnnos := make(map[string]bool) + + for k, v1 := range r1Annotations { + if stringSliceContains(exclusions, k) { + continue + } + if v2, ok := r2Annotations[k]; !ok || v1 != v2 { + diffAnnos[k] = true + } + } + + for k, v2 := range r2Annotations { + if stringSliceContains(exclusions, k) { + continue + } + if v1, ok := r1Annotations[k]; !ok || v2 != v1 { + diffAnnos[k] = true + } + } + + if len(diffAnnos) > 0 { + keys := make([]string, 0, len(diffAnnos)) + for k := range diffAnnos { + keys = append(keys, k) + } + sort.Strings(keys) + + errorString := "internal annotations differ: " + for _, key := range keys { + errorString = errorString + key + ", " + } + return errors.Errorf(errorString[0 : len(errorString)-2]) + } + + return nil +} + +// GetInternalAnnotations returns a map of all the annotations of the provided RNode that begin +// with the prefix `internal.config.kubernetes.io` +func GetInternalAnnotations(rn *yaml.RNode) map[string]string { + annotations := rn.GetAnnotations() + result := make(map[string]string) + for k, v := range annotations { + if strings.HasPrefix(k, internalPrefix) { + result[k] = v + } + } + return result +} + +// stringSliceContains returns true if the slice has the string. +func stringSliceContains(slice []string, str string) bool { + for _, s := range slice { + if s == str { + return true + } + } + return false +} diff --git a/kyaml/kio/kioutil/kioutil_test.go b/kyaml/kio/kioutil/kioutil_test.go index bf6c07b4eaa..3bbdea26a95 100644 --- a/kyaml/kio/kioutil/kioutil_test.go +++ b/kyaml/kio/kioutil/kioutil_test.go @@ -451,3 +451,253 @@ metadata: assert.Equal(t, tc.expected, nodes[0].MustString()) } } + +func TestCopyInternalAnnotations(t *testing.T) { + var tests = []struct { + input string + exclusions []kioutil.AnnotationKey + expected string + }{ + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: src + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar' +--- +apiVersion: v1 +kind: Foo +metadata: + name: dst + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' +`, + expected: `apiVersion: v1 +kind: Foo +metadata: + name: dst + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar' +`, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: src + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar-src' +--- +apiVersion: v1 +kind: Foo +metadata: + name: dst + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' + internal.config.kubernetes.io/foo: 'bar-dst' +`, + exclusions: []kioutil.AnnotationKey{ + kioutil.PathAnnotation, + kioutil.IndexAnnotation, + }, + expected: `apiVersion: v1 +kind: Foo +metadata: + name: dst + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' + internal.config.kubernetes.io/foo: 'bar-src' +`, + }, + } + + for _, tc := range tests { + rw := kio.ByteReadWriter{ + Reader: bytes.NewBufferString(tc.input), + OmitReaderAnnotations: true, + } + nodes, err := rw.Read() + assert.NoError(t, err) + assert.NoError(t, kioutil.CopyInternalAnnotations(nodes[0], nodes[1], tc.exclusions...)) + assert.Equal(t, tc.expected, nodes[1].MustString()) + } +} + +func TestConfirmInternalAnnotationUnchanged(t *testing.T) { + var tests = []struct { + input string + exclusions []kioutil.AnnotationKey + expectedErr string + }{ + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo-1 + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo-2 + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' +`, + expectedErr: `internal annotations differ: internal.config.kubernetes.io/index, internal.config.kubernetes.io/path`, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo-1 + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo-2 + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' +`, + exclusions: []kioutil.AnnotationKey{ + kioutil.PathAnnotation, + kioutil.IndexAnnotation, + }, + expectedErr: ``, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo-1 + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar-1' +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo-2 + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' + internal.config.kubernetes.io/foo: 'bar-2' +`, + exclusions: []kioutil.AnnotationKey{ + kioutil.PathAnnotation, + kioutil.IndexAnnotation, + }, + expectedErr: `internal annotations differ: internal.config.kubernetes.io/foo`, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo-1 + annotations: + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar-1' +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo-2 + annotations: + internal.config.kubernetes.io/path: 'c/d.yaml' + internal.config.kubernetes.io/index: '10' + internal.config.kubernetes.io/foo: 'bar-1' +`, + expectedErr: `internal annotations differ: internal.config.kubernetes.io/index, internal.config.kubernetes.io/path`, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo-1 + annotations: + internal.config.kubernetes.io/a: 'b' + internal.config.kubernetes.io/c: 'd' +--- +apiVersion: v1 +kind: Foo +metadata: + name: foo-2 + annotations: + internal.config.kubernetes.io/e: 'f' + internal.config.kubernetes.io/g: 'h' +`, + expectedErr: `internal annotations differ: internal.config.kubernetes.io/a, internal.config.kubernetes.io/c, internal.config.kubernetes.io/e, internal.config.kubernetes.io/g`, + }, + } + + for _, tc := range tests { + rw := kio.ByteReadWriter{ + Reader: bytes.NewBufferString(tc.input), + OmitReaderAnnotations: true, + } + nodes, err := rw.Read() + assert.NoError(t, err) + err = kioutil.ConfirmInternalAnnotationUnchanged(nodes[0], nodes[1], tc.exclusions...) + if tc.expectedErr == "" { + assert.NoError(t, err) + } else { + if err == nil { + t.Fatalf("expected error: %s\n", tc.expectedErr) + } + assert.Equal(t, tc.expectedErr, err.Error()) + } + } +} + +func TestGetInternalAnnotations(t *testing.T) { + var tests = []struct { + input string + expected map[string]string + }{ + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foobar + annotations: + foo: bar + internal.config.kubernetes.io/path: 'a/b.yaml' + internal.config.kubernetes.io/index: '5' + internal.config.kubernetes.io/foo: 'bar' +`, + expected: map[string]string{ + "internal.config.kubernetes.io/path": "a/b.yaml", + "internal.config.kubernetes.io/index": "5", + "internal.config.kubernetes.io/foo": "bar", + }, + }, + } + + for _, tc := range tests { + rw := kio.ByteReadWriter{ + Reader: bytes.NewBufferString(tc.input), + OmitReaderAnnotations: true, + } + nodes, err := rw.Read() + assert.NoError(t, err) + assert.Equal(t, tc.expected, kioutil.GetInternalAnnotations(nodes[0])) + } +}