Files
kustomize/api/resmap/reswrangler_test.go
Karl Isenberg 43868688d5 Use require for Error and NoError
Assert keeps going after failure, but require immediately fails
the tests, making it easier to find the output related to the test
failure, rather than having to comb through a bunch of subsequent
assertion failures. For equality tests, we may or may not want to
continue, but for error checks we almost always want to immediately
fail the test. Exceptions can be changed as-needed.
2024-03-20 13:19:18 -07:00

1786 lines
40 KiB
Go

// 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)
}