Files
kustomize/api/resmap/reswrangler_test.go
Yusuke Abe ed09399cd1 fix: return error instead of log.Fatalf() (#5625)
* fix: return error instead of log.Fatalf()

* chore: add meaningful message to error output

* chore: add meaningful message to fatal function
2024-04-18 03:56:51 -07:00

1865 lines
42 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 {
r, err := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": fmt.Sprintf("cm%03d", i),
},
})
if err != nil {
panic(err)
}
return r
}
// 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, err1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "alice",
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
r2, err2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
r3, err3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
"namespace": "happy",
},
})
if err3 != nil {
t.Fatalf("failed to get new instance: %v", err3)
}
r4, err4 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
if err4 != nil {
t.Fatalf("failed to get new instance: %v", err4)
}
r5, err5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
if err5 != nil {
t.Fatalf("failed to get new instance: %v", err5)
}
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, err1 := 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",
},
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
r2, err2 := 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",
},
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
r3, err3 := 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",
},
},
})
if err3 != nil {
t.Fatalf("failed to get new instance: %v", err3)
}
r4, err4 := 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",
},
},
})
if err4 != nil {
t.Fatalf("failed to get new instance: %v", err4)
}
r5, err5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
if err5 != nil {
t.Fatalf("failed to get new instance: %v", err5)
}
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, err1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "alice",
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
r2, err2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
r3, err3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "bob",
"namespace": "happy",
},
})
if err3 != nil {
t.Fatalf("failed to get new instance: %v", err3)
}
r4, err4 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
if err4 != nil {
t.Fatalf("failed to get new instance: %v", err4)
}
r5, err5 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "charlie",
"namespace": "happy",
},
})
if err5 != nil {
t.Fatalf("failed to get new instance: %v", err5)
}
r5.AddNamePrefix("little-")
r6, err6 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "domino",
"namespace": "happy",
},
})
if err6 != nil {
t.Fatalf("failed to get new instance: %v", err6)
}
r6.AddNamePrefix("little-")
r7, err7 := rf.FromMap(
map[string]interface{}{
"apiVersion": "rbac.authorization.k8s.io/v1",
"kind": "ClusterRoleBinding",
"metadata": map[string]interface{}{
"name": "meh",
},
})
if err7 != nil {
t.Fatalf("failed to get new instance: %v", err7)
}
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, err1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
r2, err2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
r3, err3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
"namespace": "system",
},
})
if err3 != nil {
t.Fatalf("failed to get new instance: %v", err3)
}
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, err1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm1",
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
r2, err2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
r3, err3 := rf.FromMap(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "cm2",
"namespace": "system",
},
})
if err3 != nil {
t.Fatalf("failed to get new instance: %v", err3)
}
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, err1 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "foo-deploy1",
},
})
if err1 != nil {
t.Fatalf("failed to get new instance: %v", err1)
}
input1 := rmF.FromResource(r1)
r2, err2 := rf.FromMap(
map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "StatefulSet",
"metadata": map[string]interface{}{
"name": "bar-stateful",
},
})
if err2 != nil {
t.Fatalf("failed to get new instance: %v", err2)
}
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)
}