// Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 package resmap_test import ( "bytes" "fmt" "reflect" "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sigs.k8s.io/kustomize/api/filters/labels" "sigs.k8s.io/kustomize/api/provider" . "sigs.k8s.io/kustomize/api/resmap" "sigs.k8s.io/kustomize/api/resource" kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" resmaptest_test "sigs.k8s.io/kustomize/api/testutils/resmaptest" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/resid" "sigs.k8s.io/kustomize/kyaml/yaml" ) var depProvider = provider.NewDefaultDepProvider() var rf = depProvider.GetResourceFactory() var rmF = NewFactory(rf) var origin1 = &resource.Origin{ Repo: "github.com/myrepo", Ref: "master", ConfiguredIn: "config.yaml", ConfiguredBy: yaml.ResourceIdentifier{ TypeMeta: yaml.TypeMeta{ APIVersion: "builtin", Kind: "Generator", }, NameMeta: yaml.NameMeta{ Name: "my-name", Namespace: "my-namespace", }, }, } var origin2 = &resource.Origin{ ConfiguredIn: "../base/config.yaml", ConfiguredBy: yaml.ResourceIdentifier{ TypeMeta: yaml.TypeMeta{ APIVersion: "builtin", Kind: "Generator", }, NameMeta: yaml.NameMeta{ Name: "my-name", Namespace: "my-namespace", }, }, } func doAppend(t *testing.T, w ResMap, r *resource.Resource) { t.Helper() err := w.Append(r) if err != nil { t.Fatalf("append error: %v", err) } } func doRemove(t *testing.T, w ResMap, id resid.ResId) { t.Helper() err := w.Remove(id) if err != nil { t.Fatalf("remove error: %v", err) } } // Make a resource with a predictable name. func makeCm(i int) *resource.Resource { return rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": fmt.Sprintf("cm%03d", i), }, }) } // Maintain the class invariant that no two // resources can have the same CurId(). func TestAppendRejectsDuplicateResId(t *testing.T) { w := New() if err := w.Append(makeCm(1)); err != nil { t.Fatalf("append error: %v", err) } err := w.Append(makeCm(1)) if err == nil { t.Fatalf("expected append error") } if !strings.Contains( err.Error(), "may not add resource with an already registered id") { t.Fatalf("unexpected error: %v", err) } } func TestAppendRemove(t *testing.T) { w1 := New() doAppend(t, w1, makeCm(1)) doAppend(t, w1, makeCm(2)) doAppend(t, w1, makeCm(3)) doAppend(t, w1, makeCm(4)) doAppend(t, w1, makeCm(5)) doAppend(t, w1, makeCm(6)) doAppend(t, w1, makeCm(7)) doRemove(t, w1, makeCm(1).OrgId()) doRemove(t, w1, makeCm(3).OrgId()) doRemove(t, w1, makeCm(5).OrgId()) doRemove(t, w1, makeCm(7).OrgId()) w2 := New() doAppend(t, w2, makeCm(2)) doAppend(t, w2, makeCm(4)) doAppend(t, w2, makeCm(6)) if !reflect.DeepEqual(w1, w2) { w1.Debug("w1") w2.Debug("w2") t.Fatalf("mismatch") } err := w2.Append(makeCm(6)) if err == nil { t.Fatalf("expected error") } } func TestRemove(t *testing.T) { w := New() r := makeCm(1) err := w.Remove(r.OrgId()) if err == nil { t.Fatalf("expected error") } err = w.Append(r) if err != nil { t.Fatalf("unexpected error: %v", err) } err = w.Remove(r.OrgId()) if err != nil { t.Fatalf("unexpected error: %v", err) } err = w.Remove(r.OrgId()) if err == nil { t.Fatalf("expected error") } } func TestReplace(t *testing.T) { cm5 := makeCm(5) cm700 := makeCm(700) otherCm5 := makeCm(5) w := New() doAppend(t, w, makeCm(1)) doAppend(t, w, makeCm(2)) doAppend(t, w, makeCm(3)) doAppend(t, w, makeCm(4)) doAppend(t, w, cm5) doAppend(t, w, makeCm(6)) doAppend(t, w, makeCm(7)) oldSize := w.Size() _, err := w.Replace(otherCm5) if err != nil { t.Fatalf("unexpected error: %v", err) } if w.Size() != oldSize { t.Fatalf("unexpected size %d", w.Size()) } if r, err := w.GetByCurrentId(cm5.OrgId()); err != nil || r != otherCm5 { t.Fatalf("unexpected result r=%s, err=%v", r.CurId(), err) } if err := w.Append(cm5); err == nil { t.Fatalf("expected id already there error") } if err := w.Remove(cm5.OrgId()); err != nil { t.Fatalf("unexpected err: %v", err) } if err := w.Append(cm700); err != nil { t.Fatalf("unexpected err: %v", err) } if err := w.Append(cm5); err != nil { t.Fatalf("unexpected err: %v", err) } } func TestEncodeAsYaml(t *testing.T) { encoded := []byte(`apiVersion: v1 kind: ConfigMap metadata: name: cm1 --- apiVersion: v1 kind: ConfigMap metadata: name: cm2 `) input := resmaptest_test.NewRmBuilder(t, rf).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }, }).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", }, }).ResMap() out, err := input.AsYaml() if err != nil { t.Fatalf("unexpected error: %v", err) } if !reflect.DeepEqual(out, encoded) { t.Fatalf("%s doesn't match expected %s", out, encoded) } } func TestGetMatchingResourcesByCurrentId(t *testing.T) { cmap := resid.NewGvk("", "v1", "ConfigMap") r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "alice", }, }) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "bob", }, }) r3 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "bob", "namespace": "happy", }, }) r4 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", }, }) r5 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", }, }) m := resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).ResMap() result := m.GetMatchingResourcesByCurrentId( resid.NewResId(cmap, "alice").GvknEquals) if len(result) != 1 { t.Fatalf("Expected single map entry but got %v", result) } result = m.GetMatchingResourcesByCurrentId( resid.NewResId(cmap, "bob").GvknEquals) if len(result) != 2 { t.Fatalf("Expected two, got %v", result) } result = m.GetMatchingResourcesByCurrentId( resid.NewResIdWithNamespace(cmap, "bob", "system").GvknEquals) if len(result) != 2 { t.Fatalf("Expected two but got %v", result) } result = m.GetMatchingResourcesByCurrentId( resid.NewResIdWithNamespace(cmap, "bob", "happy").Equals) if len(result) != 1 { t.Fatalf("Expected single map entry but got %v", result) } result = m.GetMatchingResourcesByCurrentId( resid.NewResId(cmap, "charlie").GvknEquals) if len(result) != 1 { t.Fatalf("Expected single map entry but got %v", result) } //nolint:goconst tests := []struct { name string matcher IdMatcher count int }{ { "match everything", func(resid.ResId) bool { return true }, 5, }, { "match nothing", func(resid.ResId) bool { return false }, 0, }, { "name is alice", func(x resid.ResId) bool { return x.Name == "alice" }, 1, }, { "name is charlie", func(x resid.ResId) bool { return x.Name == "charlie" }, 2, }, { "name is bob", func(x resid.ResId) bool { return x.Name == "bob" }, 2, }, { "happy namespace", func(x resid.ResId) bool { return x.Namespace == "happy" }, 3, }, { "happy deployment", func(x resid.ResId) bool { return x.Namespace == "happy" && x.Gvk.Kind == "Deployment" }, 1, }, { "happy ConfigMap", func(x resid.ResId) bool { return x.Namespace == "happy" && x.Gvk.Kind == "ConfigMap" }, 2, }, } for _, tst := range tests { result := m.GetMatchingResourcesByCurrentId(tst.matcher) if len(result) != tst.count { t.Fatalf("test '%s'; actual: %d, expected: %d", tst.name, len(result), tst.count) } } } func TestGetMatchingResourcesByAnyId(t *testing.T) { r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "new-alice", "annotations": map[string]interface{}{ "internal.config.kubernetes.io/previousKinds": "ConfigMap", "internal.config.kubernetes.io/previousNames": "alice", "internal.config.kubernetes.io/previousNamespaces": "default", }, }, }) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "new-bob", "annotations": map[string]interface{}{ "internal.config.kubernetes.io/previousKinds": "ConfigMap,ConfigMap", "internal.config.kubernetes.io/previousNames": "bob,bob2", "internal.config.kubernetes.io/previousNamespaces": "default,default", }, }, }) r3 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "new-bob", "namespace": "new-happy", "annotations": map[string]interface{}{ "internal.config.kubernetes.io/previousKinds": "ConfigMap", "internal.config.kubernetes.io/previousNames": "bob", "internal.config.kubernetes.io/previousNamespaces": "happy", }, }, }) r4 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", "annotations": map[string]interface{}{ "internal.config.kubernetes.io/previousKinds": "ConfigMap", "internal.config.kubernetes.io/previousNames": "charlie", "internal.config.kubernetes.io/previousNamespaces": "default", }, }, }) r5 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", }, }) m := resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).ResMap() tests := []struct { name string matcher IdMatcher count int }{ { "match everything", func(resid.ResId) bool { return true }, 5, }, { "match nothing", func(resid.ResId) bool { return false }, 0, }, { "name is alice", func(x resid.ResId) bool { return x.Name == "alice" }, 1, }, { "name is charlie", func(x resid.ResId) bool { return x.Name == "charlie" }, 2, }, { "name is bob", func(x resid.ResId) bool { return x.Name == "bob" }, 2, }, { "happy namespace", func(x resid.ResId) bool { return x.Namespace == "happy" }, 3, }, { "happy deployment", func(x resid.ResId) bool { return x.Namespace == "happy" && x.Gvk.Kind == "Deployment" }, 1, }, { "happy ConfigMap", func(x resid.ResId) bool { return x.Namespace == "happy" && x.Gvk.Kind == "ConfigMap" }, 2, }, } for _, tst := range tests { result := m.GetMatchingResourcesByAnyId(tst.matcher) if len(result) != tst.count { t.Fatalf("test '%s'; actual: %d, expected: %d", tst.name, len(result), tst.count) } } } func TestSubsetThatCouldBeReferencedByResource(t *testing.T) { r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "alice", }, }) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "bob", }, }) r3 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "bob", "namespace": "happy", }, }) r4 := rf.FromMap( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", }, }) r5 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "charlie", "namespace": "happy", }, }) r5.AddNamePrefix("little-") r6 := rf.FromMap( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "domino", "namespace": "happy", }, }) r6.AddNamePrefix("little-") r7 := rf.FromMap( map[string]interface{}{ "apiVersion": "rbac.authorization.k8s.io/v1", "kind": "ClusterRoleBinding", "metadata": map[string]interface{}{ "name": "meh", }, }) tests := map[string]struct { filter *resource.Resource expected ResMap }{ "default namespace 1": { filter: r2, expected: resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r7).ResMap(), }, "default namespace 2": { filter: r1, expected: resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r7).ResMap(), }, "happy namespace no prefix": { filter: r3, expected: resmaptest_test.NewRmBuilder(t, rf). AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(), }, "happy namespace with prefix": { filter: r5, expected: resmaptest_test.NewRmBuilder(t, rf). AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(), }, "cluster level": { filter: r7, expected: resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap(), }, } m := resmaptest_test.NewRmBuilder(t, rf). AddR(r1).AddR(r2).AddR(r3).AddR(r4).AddR(r5).AddR(r6).AddR(r7).ResMap() for name, test := range tests { test := test t.Run(name, func(t *testing.T) { got, err1 := m.SubsetThatCouldBeReferencedByResource(test.filter) if err1 != nil { t.Fatalf("Expected error %v: ", err1) } err := test.expected.ErrorIfNotEqualLists(got) if err != nil { test.expected.Debug("expected") got.Debug("actual") t.Fatalf("Expected match") } }) } } func TestDeepCopy(t *testing.T) { rm1 := resmaptest_test.NewRmBuilder(t, rf).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }, }).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", }, }).ResMap() rm2 := rm1.DeepCopy() if &rm1 == &rm2 { t.Fatal("DeepCopy returned a reference to itself instead of a copy") } err := rm1.ErrorIfNotEqualLists(rm1) if err != nil { t.Fatal(err) } } func TestErrorIfNotEqualSets(t *testing.T) { r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }, }) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", }, }) r3 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", "namespace": "system", }, }) m1 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap() if err := m1.ErrorIfNotEqualSets(m1); err != nil { t.Fatalf("object should equal itself %v", err) } m2 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).ResMap() if err := m1.ErrorIfNotEqualSets(m2); err == nil { t.Fatalf("%v should not equal %v %v", m1, m2, err) } m3 := resmaptest_test.NewRmBuilder(t, rf).AddR(r2).ResMap() if err := m2.ErrorIfNotEqualSets(m3); err == nil { t.Fatalf("%v should not equal %v %v", m2, m3, err) } m3 = resmaptest_test.NewRmBuilder(t, rf).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }}).ResMap() if err := m2.ErrorIfNotEqualSets(m3); err != nil { t.Fatalf("%v should equal %v %v", m2, m3, err) } m4 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap() if err := m1.ErrorIfNotEqualSets(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } m4 = resmaptest_test.NewRmBuilder(t, rf).AddR(r3).AddR(r1).AddR(r2).ResMap() if err := m1.ErrorIfNotEqualSets(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } m4 = m1.ShallowCopy() if err := m1.ErrorIfNotEqualSets(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } m4 = m1.DeepCopy() if err := m1.ErrorIfNotEqualSets(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } } func TestErrorIfNotEqualLists(t *testing.T) { r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }, }) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", }, }) r3 := rf.FromMap( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm2", "namespace": "system", }, }) m1 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap() if err := m1.ErrorIfNotEqualLists(m1); err != nil { t.Fatalf("object should equal itself %v", err) } m2 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).ResMap() if err := m1.ErrorIfNotEqualLists(m2); err == nil { t.Fatalf("%v should not equal %v %v", m1, m2, err) } m3 := resmaptest_test.NewRmBuilder(t, rf).Add( map[string]interface{}{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cm1", }}).ResMap() if err := m2.ErrorIfNotEqualLists(m3); err != nil { t.Fatalf("%v should equal %v %v", m2, m3, err) } m4 := resmaptest_test.NewRmBuilder(t, rf).AddR(r1).AddR(r2).AddR(r3).ResMap() if err := m1.ErrorIfNotEqualLists(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } m4 = resmaptest_test.NewRmBuilder(t, rf).AddR(r3).AddR(r1).AddR(r2).ResMap() if err := m1.ErrorIfNotEqualLists(m4); err == nil { t.Fatalf("expected inequality between %v and %v, %v", m1, m4, err) } m4 = m1.ShallowCopy() if err := m1.ErrorIfNotEqualLists(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } m4 = m1.DeepCopy() if err := m1.ErrorIfNotEqualLists(m4); err != nil { t.Fatalf("expected equality between %v and %v, %v", m1, m4, err) } } func TestAppendAll(t *testing.T) { r1 := rf.FromMap( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "Deployment", "metadata": map[string]interface{}{ "name": "foo-deploy1", }, }) input1 := rmF.FromResource(r1) r2 := rf.FromMap( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "StatefulSet", "metadata": map[string]interface{}{ "name": "bar-stateful", }, }) input2 := rmF.FromResource(r2) expected := New() if err := expected.Append(r1); err != nil { t.Fatalf("unexpected error: %v", err) } if err := expected.Append(r2); err != nil { t.Fatalf("unexpected error: %v", err) } if err := input1.AppendAll(input2); err != nil { t.Fatalf("unexpected error: %v", err) } if err := expected.ErrorIfNotEqualLists(input1); err != nil { input1.Debug("1") expected.Debug("ex") t.Fatalf("%#v doesn't equal expected %#v", input1, expected) } if err := input1.AppendAll(nil); err != nil { t.Fatalf("unexpected error: %v", err) } if err := expected.ErrorIfNotEqualLists(input1); err != nil { t.Fatalf("%#v doesn't equal expected %#v", input1, expected) } } func makeMap1(t *testing.T) ResMap { t.Helper() r, err := rf.FromMapAndOption( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cmap", }, "data": map[string]interface{}{ "a": "x", "b": "y", }, }, &types.GeneratorArgs{ Behavior: "create", }) if err != nil { t.Fatalf("expected new intance with an options but got error: %v", err) } return rmF.FromResource(r) } func makeMap2(t *testing.T, b types.GenerationBehavior) ResMap { t.Helper() r, err := rf.FromMapAndOption( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "ConfigMap", "metadata": map[string]interface{}{ "name": "cmap", }, "data": map[string]interface{}{ "a": "u", "b": "v", "c": "w", }, }, &types.GeneratorArgs{ Behavior: b.String(), }) if err != nil { t.Fatalf("expected new intance with an options but got error: %v", err) } return rmF.FromResource(r) } func TestAbsorbAll(t *testing.T) { metadata := map[string]interface{}{ "name": "cmap", } r, err := rf.FromMapAndOption( map[string]interface{}{ "apiVersion": "apps/v1", "kind": "ConfigMap", "metadata": metadata, "data": map[string]interface{}{ "a": "u", "b": "v", "c": "w", }, }, &types.GeneratorArgs{ Behavior: "create", }) if err != nil { t.Fatalf("expected new intance with an options but got error: %v", err) } expected := rmF.FromResource(r) w := makeMap1(t) require.NoError(t, w.AbsorbAll(makeMap2(t, types.BehaviorMerge))) expected.RemoveBuildAnnotations() w.RemoveBuildAnnotations() require.NoError(t, expected.ErrorIfNotEqualLists(w)) w = makeMap1(t) require.NoError(t, w.AbsorbAll(nil)) require.NoError(t, w.ErrorIfNotEqualLists(makeMap1(t))) w = makeMap1(t) w2 := makeMap2(t, types.BehaviorReplace) require.NoError(t, w.AbsorbAll(w2)) w2.RemoveBuildAnnotations() require.NoError(t, w2.ErrorIfNotEqualLists(w)) err = makeMap1(t).AbsorbAll(makeMap2(t, types.BehaviorUnspecified)) require.Error(t, err) assert.True( t, strings.Contains(err.Error(), "behavior must be merge or replace")) } func TestToRNodeSlice(t *testing.T) { input := `apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: namespace-reader rules: - apiGroups: - "" resources: - namespaces verbs: - get - watch - list ` rm, err := rmF.NewResMapFromBytes([]byte(input)) if err != nil { t.Fatalf("unexpected error: %v", err) } b := bytes.NewBufferString("") for i, n := range rm.ToRNodeSlice() { if i != 0 { b.WriteString("---\n") } s, err := n.String() if err != nil { t.Fatalf("unexpected error: %v", err) } b.WriteString(s) } if !reflect.DeepEqual(input, b.String()) { t.Fatalf("actual doesn't match expected.\nActual:\n%s\n===\nExpected:\n%s\n", b.String(), input) } } func TestDeAnchorSingleDoc(t *testing.T) { input := `apiVersion: v1 kind: ConfigMap metadata: name: wildcard data: color: &color-used blue feeling: *color-used ` rm, err := rmF.NewResMapFromBytes([]byte(input)) require.NoError(t, err) require.NoError(t, rm.DeAnchor()) yaml, err := rm.AsYaml() require.NoError(t, err) assert.Equal(t, strings.TrimSpace(` apiVersion: v1 data: color: blue feeling: blue kind: ConfigMap metadata: name: wildcard `), strings.TrimSpace(string(yaml))) } func TestDeAnchorIntoDoc(t *testing.T) { input := `apiVersion: apps/v1 kind: Deployment metadata: name: probes-test spec: template: spec: readinessProbe: &probe periodSeconds: 5 timeoutSeconds: 3 failureThreshold: 3 httpGet: port: http path: /health livenessProbe: <<: *probe ` rm, err := rmF.NewResMapFromBytes([]byte(input)) require.NoError(t, err) require.NoError(t, rm.DeAnchor()) yaml, err := rm.AsYaml() require.NoError(t, err) assert.Equal(t, strings.TrimSpace(`apiVersion: apps/v1 kind: Deployment metadata: name: probes-test spec: template: spec: livenessProbe: failureThreshold: 3 httpGet: path: /health port: http periodSeconds: 5 timeoutSeconds: 3 readinessProbe: failureThreshold: 3 httpGet: path: /health port: http periodSeconds: 5 timeoutSeconds: 3 `), strings.TrimSpace(string(yaml))) } // Anchor references don't cross YAML document boundaries. func TestDeAnchorMultiDoc(t *testing.T) { input := `apiVersion: v1 kind: ConfigMap metadata: name: betty data: color: &color-used blue feeling: *color-used --- apiVersion: v1 kind: ConfigMap metadata: name: bob data: color: red feeling: *color-used ` _, err := rmF.NewResMapFromBytes([]byte(input)) require.Error(t, err) assert.Contains(t, err.Error(), "unknown anchor 'color-used' referenced") } // Anchor references cross list elements in a ResourceList. func TestDeAnchorResourceList(t *testing.T) { input := `apiVersion: config.kubernetes.io/v1 kind: ResourceList metadata: name: aShortList items: - apiVersion: v1 kind: ConfigMap metadata: name: betty data: color: &color-used blue feeling: *color-used - apiVersion: v1 kind: ConfigMap metadata: name: bob data: color: red feeling: *color-used ` rm, err := rmF.NewResMapFromBytes([]byte(input)) require.NoError(t, err) require.NoError(t, rm.DeAnchor()) yaml, err := rm.AsYaml() require.NoError(t, err) assert.Equal(t, strings.TrimSpace(` apiVersion: v1 data: color: blue feeling: blue kind: ConfigMap metadata: name: betty --- apiVersion: v1 data: color: red feeling: blue kind: ConfigMap metadata: name: bob `), strings.TrimSpace(string(yaml))) } func TestApplySmPatch_General(t *testing.T) { const ( myDeployment = "Deployment" myCRD = "myCRD" expectedResultSMP = `apiVersion: apps/v1 kind: Deployment metadata: name: deploy1 spec: template: metadata: labels: old-label: old-value some-label: some-value spec: containers: - env: - name: SOMEENV value: SOMEVALUE image: nginx name: nginx ` ) tests := map[string]struct { base []string patches []string expected []string errorExpected bool errorMsg string }{ "clown": { base: []string{`apiVersion: v1 kind: Deployment metadata: name: clown spec: numReplicas: 1 `, }, patches: []string{`apiVersion: v1 kind: Deployment metadata: name: clown spec: numReplicas: 999 `, }, errorExpected: false, expected: []string{ `apiVersion: v1 kind: Deployment metadata: name: clown spec: numReplicas: 999 `, }, }, "confusion": { base: []string{`apiVersion: example.com/v1 kind: Foo metadata: name: my-foo spec: bar: A: X B: Y `, }, patches: []string{`apiVersion: example.com/v1 kind: Foo metadata: name: my-foo spec: bar: B: C: Z `, `apiVersion: example.com/v1 kind: Foo metadata: name: my-foo spec: bar: C: Z D: W baz: hello: world `, }, errorExpected: false, expected: []string{ `apiVersion: example.com/v1 kind: Foo metadata: name: my-foo spec: bar: A: X C: Z D: W baz: hello: world `, }, }, "withschema-ns1-ns2-one": { base: []string{ addNamespace("ns1", baseResource(myDeployment)), addNamespace("ns2", baseResource(myDeployment)), }, patches: []string{ addNamespace("ns1", addLabelAndEnvPatch(myDeployment)), addNamespace("ns2", addLabelAndEnvPatch(myDeployment)), }, errorExpected: false, expected: []string{ addNamespace("ns1", expectedResultSMP), addNamespace("ns2", expectedResultSMP), }, }, "withschema-ns1-ns2-two": { base: []string{ addNamespace("ns1", baseResource(myDeployment)), }, patches: []string{ addNamespace("ns2", changeImagePatch(myDeployment)), }, expected: []string{ addNamespace("ns1", baseResource(myDeployment)), }, }, "withschema-ns1-ns2-three": { base: []string{ addNamespace("ns1", baseResource(myDeployment)), }, patches: []string{ addNamespace("ns1", changeImagePatch(myDeployment)), }, expected: []string{ `apiVersion: apps/v1 kind: Deployment metadata: name: deploy1 namespace: ns1 spec: template: metadata: labels: old-label: old-value spec: containers: - image: nginx:1.7.9 name: nginx `, }, }, "withschema-nil-ns2": { base: []string{ baseResource(myDeployment), }, patches: []string{ addNamespace("ns2", changeImagePatch(myDeployment)), }, expected: []string{ baseResource(myDeployment), }, }, "withschema-ns1-nil": { base: []string{ addNamespace("ns1", baseResource(myDeployment)), }, patches: []string{ changeImagePatch(myDeployment), }, expected: []string{ addNamespace("ns1", baseResource(myDeployment)), }, }, "noschema-ns1-ns2-one": { base: []string{ addNamespace("ns1", baseResource(myCRD)), addNamespace("ns2", baseResource(myCRD)), }, patches: []string{ addNamespace("ns1", addLabelAndEnvPatch(myCRD)), addNamespace("ns2", addLabelAndEnvPatch(myCRD)), }, errorExpected: false, expected: []string{ addNamespace("ns1", expectedResultJMP("")), addNamespace("ns2", expectedResultJMP("")), }, }, "noschema-ns1-ns2-two": { base: []string{addNamespace("ns1", baseResource(myCRD))}, patches: []string{addNamespace("ns2", changeImagePatch(myCRD))}, expected: []string{addNamespace("ns1", baseResource(myCRD))}, }, "noschema-nil-ns2": { base: []string{baseResource(myCRD)}, patches: []string{addNamespace("ns2", changeImagePatch(myCRD))}, expected: []string{baseResource(myCRD)}, }, "noschema-ns1-nil": { base: []string{addNamespace("ns1", baseResource(myCRD))}, patches: []string{changeImagePatch(myCRD)}, expected: []string{addNamespace("ns1", baseResource(myCRD))}, }, } for n := range tests { tc := tests[n] t.Run(n, func(t *testing.T) { m, err := rmF.NewResMapFromBytes([]byte(strings.Join(tc.base, "\n---\n"))) require.NoError(t, err) foundError := false for _, patch := range tc.patches { rp, err := rf.FromBytes([]byte(patch)) require.NoError(t, err) idSet := resource.MakeIdSet([]*resource.Resource{rp}) if err = m.ApplySmPatch(idSet, rp); err != nil { foundError = true break } } if foundError { assert.True(t, tc.errorExpected) // compare error message? return } assert.False(t, tc.errorExpected) m.RemoveBuildAnnotations() yml, err := m.AsYaml() require.NoError(t, err) assert.Equal(t, strings.Join(tc.expected, "---\n"), string(yml)) }) } } // simple utility function to add an namespace in a resource // used as base, patch or expected result. Simply looks // for specs: in order to add namespace: xxxx before this line func addNamespace(namespace string, base string) string { res := strings.Replace(base, "\nspec:\n", "\n namespace: "+namespace+"\nspec:\n", 1) return res } // DeleteOddsFilter deletes the odd entries, removing nodes. // This is a ridiculous filter for testing. type DeleteOddsFilter struct{} func (f DeleteOddsFilter) Filter( nodes []*yaml.RNode) (result []*yaml.RNode, err error) { for i := range nodes { if i%2 == 0 { // Keep the even entries, drop the odd entries. result = append(result, nodes[i]) } } return } // CloneOddsFilter deletes even entries and clones odd entries, // making new nodes. // This is a ridiculous filter for testing. type CloneOddsFilter struct{} func (f CloneOddsFilter) Filter( nodes []*yaml.RNode) (result []*yaml.RNode, err error) { for i := range nodes { if i%2 != 0 { newNode := nodes[i].Copy() // Add suffix to the name, so that it's unique (w/r to this test). newNode.SetName(newNode.GetName() + "Clone") // Return a ptr to the copy. result = append(result, nodes[i], newNode) } } return } func TestApplyFilter(t *testing.T) { tests := map[string]struct { input string f kio.Filter expected string }{ "labels": { input: ` apiVersion: example.com/v1 kind: Beans metadata: name: myBeans --- apiVersion: example.com/v1 kind: Franks metadata: name: myFranks `, f: labels.Filter{ Labels: map[string]string{ "a": "foo", "b": "bar", }, FsSlice: types.FsSlice{ { Gvk: resid.NewGvk("example.com", "v1", "Beans"), Path: "metadata/labels", CreateIfNotPresent: true, }, }, }, expected: ` apiVersion: example.com/v1 kind: Beans metadata: labels: a: foo b: bar name: myBeans --- apiVersion: example.com/v1 kind: Franks metadata: name: myFranks `, }, "deleteOddNodes": { input: ` apiVersion: example.com/v1 kind: Zero metadata: name: r0 --- apiVersion: example.com/v1 kind: One metadata: name: r1 --- apiVersion: example.com/v1 kind: Two metadata: name: r2 --- apiVersion: example.com/v1 kind: Three metadata: name: r3 `, f: DeleteOddsFilter{}, expected: ` apiVersion: example.com/v1 kind: Zero metadata: name: r0 --- apiVersion: example.com/v1 kind: Two metadata: name: r2 `, }, "cloneOddNodes": { // input list has five entries input: ` apiVersion: example.com/v1 kind: Zero metadata: name: r0 --- apiVersion: example.com/v1 kind: One metadata: name: r1 --- apiVersion: example.com/v1 kind: Two metadata: name: r2 --- apiVersion: example.com/v1 kind: Three metadata: name: r3 --- apiVersion: example.com/v1 kind: Four metadata: name: r4 `, f: CloneOddsFilter{}, // output has four, but half are newly created nodes. expected: ` apiVersion: example.com/v1 kind: One metadata: name: r1 --- apiVersion: example.com/v1 kind: One metadata: name: r1Clone --- apiVersion: example.com/v1 kind: Three metadata: name: r3 --- apiVersion: example.com/v1 kind: Three metadata: name: r3Clone `, }, } for name := range tests { tc := tests[name] t.Run(name, func(t *testing.T) { m, err := rmF.NewResMapFromBytes([]byte(tc.input)) require.NoError(t, err) require.NoError(t, m.ApplyFilter(tc.f)) kusttest_test.AssertActualEqualsExpectedWithTweak( t, m, nil, tc.expected) }) } } func TestApplySmPatch_Deletion(t *testing.T) { target := ` apiVersion: apps/v1 metadata: name: myDeploy kind: Deployment spec: replica: 2 template: metadata: labels: old-label: old-value spec: containers: - name: nginx image: nginx ` tests := map[string]struct { patch string expected string finalMapSize int }{ "delete1": { patch: `apiVersion: apps/v1 metadata: name: myDeploy kind: Deployment spec: replica: 2 template: $patch: delete metadata: labels: old-label: old-value spec: containers: - name: nginx image: nginx `, expected: `apiVersion: apps/v1 kind: Deployment metadata: name: myDeploy spec: replica: 2 `, finalMapSize: 1, }, "delete2": { patch: `apiVersion: apps/v1 metadata: name: myDeploy kind: Deployment spec: $patch: delete replica: 2 template: metadata: labels: old-label: old-value spec: containers: - name: nginx image: nginx `, expected: `apiVersion: apps/v1 kind: Deployment metadata: name: myDeploy `, finalMapSize: 1, }, "delete3": { patch: `apiVersion: apps/v1 metadata: name: myDeploy kind: Deployment $patch: delete `, expected: "", finalMapSize: 0, }, } for name := range tests { tc := tests[name] t.Run(name, func(t *testing.T) { m, err := rmF.NewResMapFromBytes([]byte(target)) require.NoError(t, err, name) idSet := resource.MakeIdSet(m.Resources()) assert.Equal(t, 1, idSet.Size(), name) p, err := rf.FromBytes([]byte(tc.patch)) require.NoError(t, err, name) require.NoError(t, m.ApplySmPatch(idSet, p), name) assert.Equal(t, tc.finalMapSize, m.Size(), name) m.RemoveBuildAnnotations() yml, err := m.AsYaml() require.NoError(t, err, name) assert.Equal(t, tc.expected, string(yml), name) }) } } func TestOriginAnnotations(t *testing.T) { w := New() for i := 0; i < 3; i++ { require.NoError(t, w.Append(makeCm(i))) } // this should add an origin annotation to every resource require.NoError(t, w.AddOriginAnnotation(origin1)) resources := w.Resources() for _, res := range resources { or, err := res.GetOrigin() require.NoError(t, err) assert.Equal(t, origin1, or) } // this should not overwrite the existing origin annotations require.NoError(t, w.AddOriginAnnotation(origin2)) for _, res := range resources { or, err := res.GetOrigin() require.NoError(t, err) assert.Equal(t, origin1, or) } // this should remove origin annotations from all resources require.NoError(t, w.RemoveOriginAnnotations()) for _, res := range resources { or, err := res.GetOrigin() require.NoError(t, err) assert.Nil(t, or) } } func TestTransformerAnnotations(t *testing.T) { w := New() for i := 0; i < 3; i++ { require.NoError(t, w.Append(makeCm(i))) } // this should add an origin annotation to every resource require.NoError(t, w.AddTransformerAnnotation(origin1)) resources := w.Resources() for _, res := range resources { or, err := res.GetOrigin() require.NoError(t, err) assert.Equal(t, origin1, or) } // this should add a transformer annotation to every resource require.NoError(t, w.AddTransformerAnnotation(origin2)) for _, res := range resources { or, err := res.GetOrigin() require.NoError(t, err) assert.Equal(t, origin1, or) tr, err := res.GetTransformations() require.NoError(t, err) assert.Equal(t, resource.Transformations{origin2}, tr) } // remove transformer annotations from all resources require.NoError(t, w.RemoveTransformerAnnotations()) for _, res := range resources { tr, err := res.GetTransformations() require.NoError(t, err) assert.Nil(t, tr) } } // baseResource produces a base object which used to test // patch transformation // Also the structure is matching the Deployment syntax // the kind can be replaced to allow testing using CRD // without access to the schema func baseResource(kind string) string { return fmt.Sprintf(`apiVersion: apps/v1 kind: %s metadata: name: deploy1 spec: template: metadata: labels: old-label: old-value spec: containers: - image: nginx name: nginx `, kind) } // addContainerAndEnvPatch produces a patch object which adds // an entry in the env slice of the first/nginx container // as well as adding a label in the metadata // Note that for SMP/WithSchema merge, the name:nginx entry // is mandatory func addLabelAndEnvPatch(kind string) string { return fmt.Sprintf(`apiVersion: apps/v1 kind: %s metadata: name: deploy1 spec: template: metadata: labels: some-label: some-value spec: containers: - name: nginx env: - name: SOMEENV value: SOMEVALUE`, kind) } // changeImagePatch produces a patch object which replaces // the value of the image field in the first/nginx container // Note that for SMP/WithSchema merge, the name:nginx entry // is mandatory func changeImagePatch(kind string) string { return fmt.Sprintf(`apiVersion: apps/v1 kind: %s metadata: name: deploy1 spec: template: spec: containers: - name: nginx image: "nginx:1.7.9"`, kind) } // utility method building the expected output of a JMP. // imagename parameter allows to build a result consistent // with the JMP behavior which basically overrides the // entire "containers" list. func expectedResultJMP(imagename string) string { if imagename == "" { return `apiVersion: apps/v1 kind: myCRD metadata: name: deploy1 spec: template: metadata: labels: old-label: old-value some-label: some-value spec: containers: - env: - name: SOMEENV value: SOMEVALUE name: nginx ` } return fmt.Sprintf(`apiVersion: apps/v1 kind: myCRD metadata: name: deploy1 spec: template: metadata: labels: old-label: old-value some-label: some-value spec: containers: - image: %s name: nginx `, imagename) }