diff --git a/pkg/kapp/diff/explicit_versioned_ref.go b/pkg/kapp/diff/explicit_versioned_ref.go new file mode 100644 index 000000000..aa9fc86ed --- /dev/null +++ b/pkg/kapp/diff/explicit_versioned_ref.go @@ -0,0 +1,58 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package diff + +import ( + "fmt" + + ctlres "github.com/k14s/kapp/pkg/kapp/resources" + "gopkg.in/yaml.v2" +) + +type ExplicitVersionedRef struct { + AnnotationKey string + Annotation string +} + +const ( + explicitReferenceKey = "kapp.k14s.io/versioned-explicit-ref" + explicitReferenceKeyPrefix = "kapp.k14s.io/versioned-explicit-ref." +) + +func NewExplicitVersionedRef(annotationKey string, annotation string) *ExplicitVersionedRef { + return &ExplicitVersionedRef{annotationKey, annotation} +} + +func (e *ExplicitVersionedRef) AsObjectRef() (map[string]interface{}, error) { + var objectRef map[string]interface{} + err := yaml.Unmarshal([]byte(e.Annotation), &objectRef) + if err != nil { + return nil, fmt.Errorf("Parsing versioned explicit reference from annotation '%s': %s", e.AnnotationKey, err) + } + + _, hasAPIVersionKey := objectRef["apiVersion"] + _, hasKindKey := objectRef["kind"] + _, hasNameKey := objectRef["name"] + + if !(hasAPIVersionKey && hasKindKey && hasNameKey) { + return nil, fmt.Errorf("Expected versioned explicit reference to specify non-empty apiVersion, kind and name keys") + } + + return objectRef, nil +} + +func (e *ExplicitVersionedRef) AnnotationMod(objectRef map[string]interface{}) (ctlres.StringMapAppendMod, error) { + value, err := yaml.Marshal(objectRef) + if err != nil { + return ctlres.StringMapAppendMod{}, fmt.Errorf("Marshalling explicit reference: %s", err) + } + + return ctlres.StringMapAppendMod{ + ResourceMatcher: ctlres.AllMatcher{}, + Path: ctlres.NewPathFromStrings([]string{"metadata", "annotations"}), + KVs: map[string]string{ + e.AnnotationKey: string(value), + }, + }, nil +} diff --git a/pkg/kapp/diff/versioned_resource.go b/pkg/kapp/diff/versioned_resource.go index 2b17e3797..ba1cd0dbb 100644 --- a/pkg/kapp/diff/versioned_resource.go +++ b/pkg/kapp/diff/versioned_resource.go @@ -91,6 +91,34 @@ func (d VersionedResource) updateAffected(rule ctlconf.TemplateRule, rs []ctlres } } + for _, res := range rs { + for k, v := range res.Annotations() { + if k == explicitReferenceKey || strings.HasPrefix(k, explicitReferenceKeyPrefix) { + explicitRef := NewExplicitVersionedRef(k, v) + objectRef, err := explicitRef.AsObjectRef() + if err != nil { + return fmt.Errorf("Parsing versioned explicit ref on resource '%s': %s", res.Description(), err) + } + + // Passing empty TemplateAffectedObjRef as explicit references do not have a special name key + err = d.buildObjRefReplacementFunc(ctlconf.TemplateAffectedObjRef{})(objectRef) + if err != nil { + return fmt.Errorf("Processing object ref for explicit ref on resource '%s': %s", res.Description(), err) + } + + annotationMod, err := explicitRef.AnnotationMod(objectRef) + if err != nil { + return fmt.Errorf("Preparing annotation mod for versioned explicit ref on resource '%s': %s", res.Description(), err) + } + + err = annotationMod.Apply(res) + if err != nil { + return fmt.Errorf("Updating versioned explicit ref on resource '%s': %s", res.Description(), err) + } + } + } + } + return nil } diff --git a/test/e2e/versioned_explicit_reference_test.go b/test/e2e/versioned_explicit_reference_test.go new file mode 100644 index 000000000..4c5baf704 --- /dev/null +++ b/test/e2e/versioned_explicit_reference_test.go @@ -0,0 +1,232 @@ +// Copyright 2020 VMware, Inc. +// SPDX-License-Identifier: Apache-2.0 + +package e2e + +import ( + "reflect" + "strings" + "testing" + + uitest "github.com/cppforlife/go-cli-ui/ui/test" +) + +func TestVersionedExplicitReference(t *testing.T) { + env := BuildEnv(t) + logger := Logger{} + kapp := Kapp{t, env.Namespace, env.KappBinaryPath, logger} + + yaml1 := ` +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-1 + annotations: + kapp.k14s.io/versioned: "" +data: + foo: bar +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-2 + annotations: + kapp.k14s.io/versioned-explicit-ref: | + apiVersion: v1 + kind: ConfigMap + name: config-1 +data: +foo: bar +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-3 + annotations: + kapp.k14s.io/versioned-explicit-ref.match: | + apiVersion: v1 + kind: ConfigMap + name: config-1 +data: + foo: bar +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-4 + annotations: + kapp.k14s.io/versioned-explicit-ref.nomatch: | + apiVersion: v1 + kind: ConfigMap + name: config-2 +data: + foo: bar +` + + yaml2 := ` +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-1 + annotations: + kapp.k14s.io/versioned: "" +data: + foo: alpha +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-2 + annotations: + kapp.k14s.io/versioned-explicit-ref: | + apiVersion: v1 + kind: ConfigMap + name: config-1 +data: + foo: bar +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-3 + annotations: + kapp.k14s.io/versioned-explicit-ref.match: | + apiVersion: v1 + kind: ConfigMap + name: config-1 +data: + foo: bar +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-4 + annotations: + kapp.k14s.io/versioned-explicit-ref.nomatch: | + apiVersion: v1 + kind: ConfigMap + name: config-2 +data: + foo: bar +` + + name := "test-versioned-explicit-references" + cleanUp := func() { + kapp.Run([]string{"delete", "-a", name}) + } + + cleanUp() + defer cleanUp() + + logger.Section("deploy initial", func() { + out, _ := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "--json"}, + RunOpts{IntoNs: true, StdinReader: strings.NewReader(yaml1)}) + resp := uitest.JSONUIFromBytes(t, []byte(out)) + + expected := []map[string]string{ + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-1-ver-1", + "namespace": "kapp-test", + "op": "create", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "", + "wait_to": "reconcile", + }, + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-2", + "namespace": "kapp-test", + "op": "create", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "", + "wait_to": "reconcile", + }, + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-3", + "namespace": "kapp-test", + "op": "create", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "", + "wait_to": "reconcile", + }, + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-4", + "namespace": "kapp-test", + "op": "create", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "", + "wait_to": "reconcile", + }, + } + + if !reflect.DeepEqual(resp.Tables[0].Rows, expected) { + t.Fatalf("Expected to see correct changes but recieved >>%s<<", out) + } + }) + + logger.Section("update versioned resource", func() { + out, _ := kapp.RunWithOpts([]string{"deploy", "-f", "-", "-a", name, "--json"}, + RunOpts{IntoNs: true, StdinReader: strings.NewReader(yaml2)}) + resp := uitest.JSONUIFromBytes(t, []byte(out)) + + expected := []map[string]string{ + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-1-ver-2", + "namespace": "kapp-test", + "op": "create", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "", + "wait_to": "reconcile", + }, + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-2", + "namespace": "kapp-test", + "op": "update", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "ok", + "wait_to": "reconcile", + }, + { + "age": "", + "conditions": "", + "kind": "ConfigMap", + "name": "config-3", + "namespace": "kapp-test", + "op": "update", + "op_strategy": "", + "reconcile_info": "", + "reconcile_state": "ok", + "wait_to": "reconcile", + }, + } + + if !reflect.DeepEqual(replaceAge(resp.Tables[0].Rows), expected) { + t.Fatalf("Expected to see correct changes but recieved >>%s<<", out) + } + }) +}