diff --git a/api/ifc/ifc.go b/api/ifc/ifc.go index b17da2c59..d5e125baa 100644 --- a/api/ifc/ifc.go +++ b/api/ifc/ifc.go @@ -38,8 +38,7 @@ type Loader interface { Cleanup() error } -// Kunstructured allows manipulation of k8s objects -// that do not have Golang structs. +// Kunstructured represents a Kubernetes Resource Model object. type Kunstructured interface { // Several uses. Copy() Kunstructured diff --git a/api/resmap/factory.go b/api/resmap/factory.go index 37a05ab6d..6e8f4a7d6 100644 --- a/api/resmap/factory.go +++ b/api/resmap/factory.go @@ -11,10 +11,19 @@ import ( "sigs.k8s.io/kustomize/api/types" ) +// Merginator merges resources. +type Merginator interface { + // Merge creates a new ResMap by merging incoming resources. + // Error if conflict found. + Merge([]*resource.Resource) (ResMap, error) +} + // Factory makes instances of ResMap. type Factory struct { + // Makes resources. resF *resource.Factory - pm Merginator + // Makes ResMaps via merging. + pm Merginator } // NewFactory returns a new resmap.Factory. diff --git a/api/resmap/merginator.go b/api/resmap/merginator.go deleted file mode 100644 index 10d7377c5..000000000 --- a/api/resmap/merginator.go +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package resmap - -import "sigs.k8s.io/kustomize/api/resource" - -// Merginator merges resources. -type Merginator interface { - // Merge creates a new ResMap by merging incoming resources. - // Error if conflict found. - Merge([]*resource.Resource) (ResMap, error) -} diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index b86b2f699..2afa95a1a 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -6,16 +6,10 @@ package resmap import ( - "bytes" - "fmt" - "regexp" - - "github.com/pkg/errors" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/yaml" ) // A Transformer modifies an instance of ResMap. @@ -242,569 +236,3 @@ type ResMap interface { // are selected by a Selector Select(types.Selector) ([]*resource.Resource, error) } - -// resWrangler holds the content manipulated by kustomize. -type resWrangler struct { - // Resource list maintained in load (append) order. - // This is important for transformers, which must - // be performed in a specific order, and for users - // who for whatever reasons wish the order they - // specify in kustomizations to be maintained and - // available as an option for final YAML rendering. - rList []*resource.Resource -} - -func newOne() *resWrangler { - result := &resWrangler{} - result.Clear() - return result -} - -// Clear implements ResMap. -func (m *resWrangler) Clear() { - m.rList = nil -} - -// Size implements ResMap. -func (m *resWrangler) Size() int { - return len(m.rList) -} - -func (m *resWrangler) indexOfResource(other *resource.Resource) int { - for i, r := range m.rList { - if r == other { - return i - } - } - return -1 -} - -// Resources implements ResMap. -func (m *resWrangler) Resources() []*resource.Resource { - tmp := make([]*resource.Resource, len(m.rList)) - copy(tmp, m.rList) - return tmp -} - -// Append implements ResMap. -func (m *resWrangler) Append(res *resource.Resource) error { - id := res.CurId() - if r := m.GetMatchingResourcesByCurrentId(id.Equals); len(r) > 0 { - return fmt.Errorf( - "may not add resource with an already registered id: %s", id) - } - m.rList = append(m.rList, res) - return nil -} - -// Remove implements ResMap. -func (m *resWrangler) Remove(adios resid.ResId) error { - tmp := newOne() - for _, r := range m.rList { - if r.CurId() != adios { - tmp.Append(r) - } - } - if tmp.Size() != m.Size()-1 { - return fmt.Errorf("id %s not found in removal", adios) - } - m.rList = tmp.rList - return nil -} - -// Replace implements ResMap. -func (m *resWrangler) Replace(res *resource.Resource) (int, error) { - id := res.CurId() - i, err := m.GetIndexOfCurrentId(id) - if err != nil { - return -1, errors.Wrap(err, "in Replace") - } - if i < 0 { - return -1, fmt.Errorf("cannot find resource with id %s to replace", id) - } - m.rList[i] = res - return i, nil -} - -// AllIds implements ResMap. -func (m *resWrangler) AllIds() (ids []resid.ResId) { - ids = make([]resid.ResId, m.Size()) - for i, r := range m.rList { - ids[i] = r.CurId() - } - return -} - -// Debug implements ResMap. -func (m *resWrangler) Debug(title string) { - fmt.Println("--------------------------- " + title) - firstObj := true - for i, r := range m.rList { - if firstObj { - firstObj = false - } else { - fmt.Println("---") - } - fmt.Printf("# %d %s\n", i, r.OrgId()) - blob, err := yaml.Marshal(r.Map()) - if err != nil { - panic(err) - } - fmt.Println(string(blob)) - } -} - -type IdMatcher func(resid.ResId) bool - -// GetByIndex implements ResMap. -func (m *resWrangler) GetByIndex(i int) *resource.Resource { - if i < 0 || i >= m.Size() { - return nil - } - return m.rList[i] -} - -// GetIndexOfCurrentId implements ResMap. -func (m *resWrangler) GetIndexOfCurrentId(id resid.ResId) (int, error) { - count := 0 - result := -1 - for i, r := range m.rList { - if id.Equals(r.CurId()) { - count++ - result = i - } - } - if count > 1 { - return -1, fmt.Errorf("id matched %d resources", count) - } - return result, nil -} - -type IdFromResource func(r *resource.Resource) resid.ResId - -func GetOriginalId(r *resource.Resource) resid.ResId { return r.OrgId() } -func GetCurrentId(r *resource.Resource) resid.ResId { return r.CurId() } - -// GetMatchingResourcesByCurrentId implements ResMap. -func (m *resWrangler) GetMatchingResourcesByCurrentId( - matches IdMatcher) []*resource.Resource { - return m.filteredById(matches, GetCurrentId) -} - -// GetMatchingResourcesByOriginalId implements ResMap. -func (m *resWrangler) GetMatchingResourcesByOriginalId( - matches IdMatcher) []*resource.Resource { - return m.filteredById(matches, GetOriginalId) -} - -func (m *resWrangler) filteredById( - matches IdMatcher, idGetter IdFromResource) []*resource.Resource { - var result []*resource.Resource - for _, r := range m.rList { - if matches(idGetter(r)) { - result = append(result, r) - } - } - return result -} - -// GetByCurrentId implements ResMap. -func (m *resWrangler) GetByCurrentId( - id resid.ResId) (*resource.Resource, error) { - return demandOneMatch(m.GetMatchingResourcesByCurrentId, id, "Current") -} - -// GetByOriginalId implements ResMap. -func (m *resWrangler) GetByOriginalId( - id resid.ResId) (*resource.Resource, error) { - return demandOneMatch(m.GetMatchingResourcesByOriginalId, id, "Original") -} - -// GetById implements ResMap. -func (m *resWrangler) GetById( - id resid.ResId) (*resource.Resource, error) { - match, err1 := m.GetByOriginalId(id) - if err1 == nil { - return match, nil - } - match, err2 := m.GetByCurrentId(id) - if err2 == nil { - return match, nil - } - return nil, fmt.Errorf( - "%s; %s; failed to find unique target for patch %s", - err1.Error(), err2.Error(), id.GvknString()) -} - -type resFinder func(IdMatcher) []*resource.Resource - -func demandOneMatch( - f resFinder, id resid.ResId, s string) (*resource.Resource, error) { - r := f(id.Equals) - if len(r) == 1 { - return r[0], nil - } - if len(r) > 1 { - return nil, fmt.Errorf("multiple matches for %sId %s", s, id) - } - return nil, fmt.Errorf("no matches for %sId %s", s, id) -} - -// GroupedByCurrentNamespace implements ResMap.GroupByCurrentNamespace -func (m *resWrangler) GroupedByCurrentNamespace() map[string][]*resource.Resource { - items := m.groupedByCurrentNamespace() - delete(items, resid.TotallyNotANamespace) - return items -} - -// NonNamespaceable implements ResMap.NonNamespaceable -func (m *resWrangler) NonNamespaceable() []*resource.Resource { - return m.groupedByCurrentNamespace()[resid.TotallyNotANamespace] -} - -func (m *resWrangler) groupedByCurrentNamespace() map[string][]*resource.Resource { - byNamespace := make(map[string][]*resource.Resource) - for _, res := range m.rList { - namespace := res.CurId().EffectiveNamespace() - if _, found := byNamespace[namespace]; !found { - byNamespace[namespace] = []*resource.Resource{} - } - byNamespace[namespace] = append(byNamespace[namespace], res) - } - return byNamespace -} - -// GroupedByNamespace implements ResMap.GroupByOrginalNamespace -func (m *resWrangler) GroupedByOriginalNamespace() map[string][]*resource.Resource { - items := m.groupedByOriginalNamespace() - delete(items, resid.TotallyNotANamespace) - return items -} - -func (m *resWrangler) groupedByOriginalNamespace() map[string][]*resource.Resource { - byNamespace := make(map[string][]*resource.Resource) - for _, res := range m.rList { - namespace := res.OrgId().EffectiveNamespace() - if _, found := byNamespace[namespace]; !found { - byNamespace[namespace] = []*resource.Resource{} - } - byNamespace[namespace] = append(byNamespace[namespace], res) - } - return byNamespace -} - -// AsYaml implements ResMap. -func (m *resWrangler) AsYaml() ([]byte, error) { - firstObj := true - var b []byte - buf := bytes.NewBuffer(b) - for _, res := range m.Resources() { - out, err := yaml.Marshal(res.Map()) - if err != nil { - return nil, err - } - if firstObj { - firstObj = false - } else { - if _, err = buf.WriteString("---\n"); err != nil { - return nil, err - } - } - if _, err = buf.Write(out); err != nil { - return nil, err - } - } - return buf.Bytes(), nil -} - -// ErrorIfNotEqualSets implements ResMap. -func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error { - m2, ok := other.(*resWrangler) - if !ok { - panic("bad cast") - } - if m.Size() != m2.Size() { - return fmt.Errorf( - "lists have different number of entries: %#v doesn't equal %#v", - m.rList, m2.rList) - } - seen := make(map[int]bool) - for _, r1 := range m.rList { - id := r1.CurId() - others := m2.GetMatchingResourcesByCurrentId(id.Equals) - if len(others) == 0 { - return fmt.Errorf( - "id in self missing from other; id: %s", id) - } - if len(others) > 1 { - return fmt.Errorf( - "id in self matches %d in other; id: %s", len(others), id) - } - r2 := others[0] - if !r1.KunstructEqual(r2) { - return fmt.Errorf( - "kunstruct not equal: \n -- %s,\n -- %s\n\n--\n%#v\n------\n%#v\n", - r1, r2, r1, r2) - } - seen[m2.indexOfResource(r2)] = true - } - if len(seen) != m.Size() { - return fmt.Errorf("counting problem %d != %d", len(seen), m.Size()) - } - return nil -} - -// ErrorIfNotEqualList implements ResMap. -func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error { - m2, ok := other.(*resWrangler) - if !ok { - panic("bad cast") - } - if m.Size() != m2.Size() { - return fmt.Errorf( - "lists have different number of entries: %#v doesn't equal %#v", - m.rList, m2.rList) - } - for i, r1 := range m.rList { - r2 := m2.rList[i] - if !r1.Equals(r2) { - return fmt.Errorf( - "Item i=%d differs:\n n1 = %s\n n2 = %s\n o1 = %s\n o2 = %s\n", - i, r1.OrgId(), r2.OrgId(), r1, r2) - } - } - return nil -} - -type resCopier func(r *resource.Resource) *resource.Resource - -// ShallowCopy implements ResMap. -func (m *resWrangler) ShallowCopy() ResMap { - return m.makeCopy( - func(r *resource.Resource) *resource.Resource { - return r - }) -} - -// DeepCopy implements ResMap. -func (m *resWrangler) DeepCopy() ResMap { - return m.makeCopy( - func(r *resource.Resource) *resource.Resource { - return r.DeepCopy() - }) -} - -// makeCopy copies the ResMap. -func (m *resWrangler) makeCopy(copier resCopier) ResMap { - result := &resWrangler{} - result.rList = make([]*resource.Resource, m.Size()) - for i, r := range m.rList { - result.rList[i] = copier(r) - } - return result -} - -// SubsetThatCouldBeReferencedByResource implements ResMap. -func (m *resWrangler) SubsetThatCouldBeReferencedByResource( - inputRes *resource.Resource) ResMap { - result := newOne() - inputId := inputRes.CurId() - isInputIdNamespaceable := inputId.IsNamespaceableKind() - rctxm := inputRes.PrefixesSuffixesEquals - subjectNamespaces := getNamespacesForRoleBinding(inputRes) - for _, r := range m.Resources() { - // Need to match more accuratly both at the time of selection and transformation. - // OutmostPrefixSuffixEquals is not accurate enough since it is only using - // the outer most suffix and the last prefix. Use PrefixedSuffixesEquals instead. - resId := r.CurId() - if (!isInputIdNamespaceable || !resId.IsNamespaceableKind() || resId.IsNsEquals(inputId) || - isRoleBindingNamespace(&subjectNamespaces, r.GetNamespace())) && r.InSameKustomizeCtx(rctxm) { - result.append(r) - } - } - return result -} - -// isRoleBindingNamespace returns true is the namespace `ns` is in role binding -// namespaces `m` -func isRoleBindingNamespace(m *map[string]bool, ns string) bool { - return (*m)[ns] -} - -// getNamespacesForRoleBinding returns referenced ServiceAccount namespaces if the inputRes is -// a RoleBinding -func getNamespacesForRoleBinding(inputRes *resource.Resource) map[string]bool { - res := make(map[string]bool) - if inputRes.GetKind() != "RoleBinding" { - return res - } - subjects, err := inputRes.GetSlice("subjects") - if err != nil || subjects == nil { - return res - } - - for _, s := range subjects { - subject := s.(map[string]interface{}) - if subject["namespace"] == nil || subject["kind"] == nil || - subject["kind"].(string) != "ServiceAccount" { - continue - } - res[subject["namespace"].(string)] = true - } - - return res -} - -func (m *resWrangler) append(res *resource.Resource) { - m.rList = append(m.rList, res) -} - -// AppendAll implements ResMap. -func (m *resWrangler) AppendAll(other ResMap) error { - if other == nil { - return nil - } - for _, res := range other.Resources() { - if err := m.Append(res); err != nil { - return err - } - } - return nil -} - -// AbsorbAll implements ResMap. -func (m *resWrangler) AbsorbAll(other ResMap) error { - if other == nil { - return nil - } - for _, r := range other.Resources() { - err := m.appendReplaceOrMerge(r) - if err != nil { - return err - } - } - return nil -} - -func (m *resWrangler) appendReplaceOrMerge( - res *resource.Resource) error { - id := res.CurId() - matches := m.GetMatchingResourcesByOriginalId(id.Equals) - if len(matches) == 0 { - matches = m.GetMatchingResourcesByCurrentId(id.Equals) - } - switch len(matches) { - case 0: - switch res.Behavior() { - case types.BehaviorMerge, types.BehaviorReplace: - return fmt.Errorf( - "id %#v does not exist; cannot merge or replace", id) - default: - // presumably types.BehaviorCreate - err := m.Append(res) - if err != nil { - return err - } - } - case 1: - old := matches[0] - if old == nil { - return fmt.Errorf("id lookup failure") - } - index := m.indexOfResource(old) - if index < 0 { - return fmt.Errorf("indexing problem") - } - switch res.Behavior() { - case types.BehaviorReplace: - res.Replace(old) - case types.BehaviorMerge: - res.Merge(old) - default: - return fmt.Errorf( - "id %#v exists; behavior must be merge or replace", id) - } - i, err := m.Replace(res) - if err != nil { - return err - } - if i != index { - return fmt.Errorf("unexpected index in replacement") - } - default: - return fmt.Errorf( - "found multiple objects %v that could accept merge of %v", - matches, id) - } - return nil -} - -func anchorRegex(pattern string) string { - if pattern == "" { - return pattern - } - return "^" + pattern + "$" -} - -// Select returns a list of resources that -// are selected by a Selector -func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { - ns := regexp.MustCompile(anchorRegex(s.Namespace)) - nm := regexp.MustCompile(anchorRegex(s.Name)) - var result []*resource.Resource - for _, r := range m.Resources() { - curId := r.CurId() - orgId := r.OrgId() - - // matches the namespace when namespace is not empty in the selector - // It first tries to match with the original namespace - // then matches with the current namespace - if r.GetNamespace() != "" { - matched := ns.MatchString(orgId.EffectiveNamespace()) - if !matched { - matched = ns.MatchString(curId.EffectiveNamespace()) - if !matched { - continue - } - } - } - - // matches the name when name is not empty in the selector - // It first tries to match with the original name - // then matches with the current name - if r.GetName() != "" { - matched := nm.MatchString(orgId.Name) - if !matched { - matched = nm.MatchString(curId.Name) - if !matched { - continue - } - } - } - - // matches the GVK - if !r.GetGvk().IsSelected(&s.Gvk) { - continue - } - - // matches the label selector - matched, err := r.MatchesLabelSelector(s.LabelSelector) - if err != nil { - return nil, err - } - if !matched { - continue - } - - // matches the annotation selector - matched, err = r.MatchesAnnotationSelector(s.AnnotationSelector) - if err != nil { - return nil, err - } - if !matched { - continue - } - result = append(result, r) - } - return result, nil -} diff --git a/api/resmap/resmap_test.go b/api/resmap/resmap_test.go index b16a60f76..f250c9775 100644 --- a/api/resmap/resmap_test.go +++ b/api/resmap/resmap_test.go @@ -3,899 +3,4 @@ package resmap_test -import ( - "fmt" - "reflect" - "strings" - "testing" - - "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" - "sigs.k8s.io/kustomize/api/resid" - . "sigs.k8s.io/kustomize/api/resmap" - "sigs.k8s.io/kustomize/api/resource" - resmaptest_test "sigs.k8s.io/kustomize/api/testutils/resmaptest" - "sigs.k8s.io/kustomize/api/types" -) - -var rf = resource.NewFactory( - kunstruct.NewKunstructuredFactoryImpl()) -var rmF = NewFactory(rf, nil) - -func doAppend(t *testing.T, w ResMap, r *resource.Resource) { - err := w.Append(r) - if err != nil { - t.Fatalf("append error: %v", err) - } -} -func doRemove(t *testing.T, w ResMap, id resid.ResId) { - 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, w1) { - 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) { - 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 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": "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": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "domino", - "namespace": "happy", - }, - }) - r6.AddNamePrefix("little-") - r7 := rf.FromMap( - map[string]interface{}{ - "apiVersion": "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 := m.SubsetThatCouldBeReferencedByResource(test.filter) - err := test.expected.ErrorIfNotEqualLists(got) - if err != nil { - test.expected.Debug("expected") - got.Debug("actual") - t.Fatalf("Expected match") - } - }) - } -} - -func addPfxSfx(r *resource.Resource, prefixes []string, suffixes []string) { - for _, pfx := range prefixes { - r.AddNamePrefix(pfx) - } - - for _, sfx := range suffixes { - r.AddNameSuffix(sfx) - } -} - -func TestSubsetThatCouldBeReferencedByResourceMultiLevel(t *testing.T) { - // Simulates ConfigMap and Deployment defined at level 1 - // No prefix nor suffix added at that level - cm1 := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "level1", - }, - }) - addPfxSfx(cm1, []string{""}, []string{""}) - dep1 := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "level1", - }, - }) - addPfxSfx(dep1, []string{""}, []string{""}) - - // Simulates ConfigMap and Deployment defined at level 1 - // and prefix added in level 2 of kustomization - cm2p := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "level2p", - }, - }) - addPfxSfx(cm2p, []string{"", "level2p-"}, []string{"", ""}) - - dep2p := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "level2p", - }, - }) - addPfxSfx(dep2p, []string{"", "level2p-"}, []string{"", ""}) - - // Simulates ConfigMap and Deployment defined at level 1 - // and suffix added in level 2 of kustomization - cm2s := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "level2s", - }, - }) - addPfxSfx(cm2s, []string{"", ""}, []string{"", "-level2s"}) - - dep2s := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "level2s", - }, - }) - addPfxSfx(dep2s, []string{"", ""}, []string{"", "-level2s"}) - - // Simulates ConfigMap and Deployment defined at level 1, - // prefix added in levels 2 and 3 of kustomization. - cm3e := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "level3e", - }, - }) - addPfxSfx(cm3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""}) - - dep3e := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "level3e", - }, - }) - addPfxSfx(dep3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""}) - - // Simulates Deployment defined at level 1, ConfigMap defined at level 2, - // prefix added in levels 2 and 3 of kustomization. - // This reproduce issue 1440. - cm3i := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "level3i", - }, - }) - addPfxSfx(cm3i, []string{"level2p-", "level3i-"}, []string{"", ""}) - - dep3i := rf.FromMap( - map[string]interface{}{ - "apiVersion": "v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "level3i", - }, - }) - addPfxSfx(dep3i, []string{"", "level2p-", "level3i-"}, []string{"", "", ""}) - - tests := map[string]struct { - filter *resource.Resource - expected ResMap - }{ - "level1": { - filter: dep1, - expected: resmaptest_test.NewRmBuilder(t, rf). - AddR(cm1).AddR(dep1).ResMap(), - }, - "level2p": { - filter: dep2p, - expected: resmaptest_test.NewRmBuilder(t, rf). - AddR(cm2p).AddR(dep2p).ResMap(), - }, - "level2s": { - filter: dep2s, - expected: resmaptest_test.NewRmBuilder(t, rf). - AddR(cm2s).AddR(dep2s).ResMap(), - }, - "level3p": { - filter: dep3e, - expected: resmaptest_test.NewRmBuilder(t, rf). - AddR(cm3e).AddR(dep3e).ResMap(), - }, - "level3i": { - filter: dep3i, - expected: resmaptest_test.NewRmBuilder(t, rf). - AddR(cm3i).AddR(dep3i).ResMap(), - }, - } - m := resmaptest_test.NewRmBuilder(t, rf). - AddR(cm1).AddR(dep1).AddR(cm2s).AddR(dep2s).AddR(cm2p).AddR(dep2p).AddR(cm3e).AddR(dep3e).AddR(cm3i).AddR(dep3i).ResMap() - for name, test := range tests { - test := test - t.Run(name, func(t *testing.T) { - got := m.SubsetThatCouldBeReferencedByResource(test.filter) - 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() ResMap { - return rmF.FromResource(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", - })) -} - -func makeMap2(b types.GenerationBehavior) ResMap { - return rmF.FromResource(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(), - })) -} - -func TestAbsorbAll(t *testing.T) { - expected := rmF.FromResource(rf.FromMapAndOption( - map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "annotations": map[string]interface{}{}, - "labels": map[string]interface{}{}, - "name": "cmap", - }, - "data": map[string]interface{}{ - "a": "u", - "b": "v", - "c": "w", - }, - }, &types.GeneratorArgs{ - Behavior: "create", - })) - w := makeMap1() - if err := w.AbsorbAll(makeMap2(types.BehaviorMerge)); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := expected.ErrorIfNotEqualLists(w); err != nil { - t.Fatal(err) - } - w = makeMap1() - if err := w.AbsorbAll(nil); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := w.ErrorIfNotEqualLists(makeMap1()); err != nil { - t.Fatal(err) - } - w = makeMap1() - w2 := makeMap2(types.BehaviorReplace) - if err := w.AbsorbAll(w2); err != nil { - t.Fatalf("unexpected error: %v", err) - } - if err := w2.ErrorIfNotEqualLists(w); err != nil { - t.Fatal(err) - } - w = makeMap1() - w2 = makeMap2(types.BehaviorUnspecified) - err := w.AbsorbAll(w2) - if err == nil { - t.Fatalf("expected error with unspecified behavior") - } -} +// See reswrangler_test.go diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go new file mode 100644 index 000000000..68ba98c66 --- /dev/null +++ b/api/resmap/reswrangler.go @@ -0,0 +1,582 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package resmap + +import ( + "bytes" + "fmt" + "regexp" + + "github.com/pkg/errors" + "sigs.k8s.io/kustomize/api/resid" + "sigs.k8s.io/kustomize/api/resource" + "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/yaml" +) + +// resWrangler implements ResMap. +type resWrangler struct { + // Resource list maintained in load (append) order. + // This is important for transformers, which must + // be performed in a specific order, and for users + // who for whatever reasons wish the order they + // specify in kustomizations to be maintained and + // available as an option for final YAML rendering. + rList []*resource.Resource +} + +func newOne() *resWrangler { + result := &resWrangler{} + result.Clear() + return result +} + +// Clear implements ResMap. +func (m *resWrangler) Clear() { + m.rList = nil +} + +// Size implements ResMap. +func (m *resWrangler) Size() int { + return len(m.rList) +} + +func (m *resWrangler) indexOfResource(other *resource.Resource) int { + for i, r := range m.rList { + if r == other { + return i + } + } + return -1 +} + +// Resources implements ResMap. +func (m *resWrangler) Resources() []*resource.Resource { + tmp := make([]*resource.Resource, len(m.rList)) + copy(tmp, m.rList) + return tmp +} + +// Append implements ResMap. +func (m *resWrangler) Append(res *resource.Resource) error { + id := res.CurId() + if r := m.GetMatchingResourcesByCurrentId(id.Equals); len(r) > 0 { + return fmt.Errorf( + "may not add resource with an already registered id: %s", id) + } + m.rList = append(m.rList, res) + return nil +} + +// Remove implements ResMap. +func (m *resWrangler) Remove(adios resid.ResId) error { + tmp := newOne() + for _, r := range m.rList { + if r.CurId() != adios { + tmp.Append(r) + } + } + if tmp.Size() != m.Size()-1 { + return fmt.Errorf("id %s not found in removal", adios) + } + m.rList = tmp.rList + return nil +} + +// Replace implements ResMap. +func (m *resWrangler) Replace(res *resource.Resource) (int, error) { + id := res.CurId() + i, err := m.GetIndexOfCurrentId(id) + if err != nil { + return -1, errors.Wrap(err, "in Replace") + } + if i < 0 { + return -1, fmt.Errorf("cannot find resource with id %s to replace", id) + } + m.rList[i] = res + return i, nil +} + +// AllIds implements ResMap. +func (m *resWrangler) AllIds() (ids []resid.ResId) { + ids = make([]resid.ResId, m.Size()) + for i, r := range m.rList { + ids[i] = r.CurId() + } + return +} + +// Debug implements ResMap. +func (m *resWrangler) Debug(title string) { + fmt.Println("--------------------------- " + title) + firstObj := true + for i, r := range m.rList { + if firstObj { + firstObj = false + } else { + fmt.Println("---") + } + fmt.Printf("# %d %s\n", i, r.OrgId()) + blob, err := yaml.Marshal(r.Map()) + if err != nil { + panic(err) + } + fmt.Println(string(blob)) + } +} + +type IdMatcher func(resid.ResId) bool + +// GetByIndex implements ResMap. +func (m *resWrangler) GetByIndex(i int) *resource.Resource { + if i < 0 || i >= m.Size() { + return nil + } + return m.rList[i] +} + +// GetIndexOfCurrentId implements ResMap. +func (m *resWrangler) GetIndexOfCurrentId(id resid.ResId) (int, error) { + count := 0 + result := -1 + for i, r := range m.rList { + if id.Equals(r.CurId()) { + count++ + result = i + } + } + if count > 1 { + return -1, fmt.Errorf("id matched %d resources", count) + } + return result, nil +} + +type IdFromResource func(r *resource.Resource) resid.ResId + +func GetOriginalId(r *resource.Resource) resid.ResId { return r.OrgId() } +func GetCurrentId(r *resource.Resource) resid.ResId { return r.CurId() } + +// GetMatchingResourcesByCurrentId implements ResMap. +func (m *resWrangler) GetMatchingResourcesByCurrentId( + matches IdMatcher) []*resource.Resource { + return m.filteredById(matches, GetCurrentId) +} + +// GetMatchingResourcesByOriginalId implements ResMap. +func (m *resWrangler) GetMatchingResourcesByOriginalId( + matches IdMatcher) []*resource.Resource { + return m.filteredById(matches, GetOriginalId) +} + +func (m *resWrangler) filteredById( + matches IdMatcher, idGetter IdFromResource) []*resource.Resource { + var result []*resource.Resource + for _, r := range m.rList { + if matches(idGetter(r)) { + result = append(result, r) + } + } + return result +} + +// GetByCurrentId implements ResMap. +func (m *resWrangler) GetByCurrentId( + id resid.ResId) (*resource.Resource, error) { + return demandOneMatch(m.GetMatchingResourcesByCurrentId, id, "Current") +} + +// GetByOriginalId implements ResMap. +func (m *resWrangler) GetByOriginalId( + id resid.ResId) (*resource.Resource, error) { + return demandOneMatch(m.GetMatchingResourcesByOriginalId, id, "Original") +} + +// GetById implements ResMap. +func (m *resWrangler) GetById( + id resid.ResId) (*resource.Resource, error) { + match, err1 := m.GetByOriginalId(id) + if err1 == nil { + return match, nil + } + match, err2 := m.GetByCurrentId(id) + if err2 == nil { + return match, nil + } + return nil, fmt.Errorf( + "%s; %s; failed to find unique target for patch %s", + err1.Error(), err2.Error(), id.GvknString()) +} + +type resFinder func(IdMatcher) []*resource.Resource + +func demandOneMatch( + f resFinder, id resid.ResId, s string) (*resource.Resource, error) { + r := f(id.Equals) + if len(r) == 1 { + return r[0], nil + } + if len(r) > 1 { + return nil, fmt.Errorf("multiple matches for %sId %s", s, id) + } + return nil, fmt.Errorf("no matches for %sId %s", s, id) +} + +// GroupedByCurrentNamespace implements ResMap.GroupByCurrentNamespace +func (m *resWrangler) GroupedByCurrentNamespace() map[string][]*resource.Resource { + items := m.groupedByCurrentNamespace() + delete(items, resid.TotallyNotANamespace) + return items +} + +// NonNamespaceable implements ResMap.NonNamespaceable +func (m *resWrangler) NonNamespaceable() []*resource.Resource { + return m.groupedByCurrentNamespace()[resid.TotallyNotANamespace] +} + +func (m *resWrangler) groupedByCurrentNamespace() map[string][]*resource.Resource { + byNamespace := make(map[string][]*resource.Resource) + for _, res := range m.rList { + namespace := res.CurId().EffectiveNamespace() + if _, found := byNamespace[namespace]; !found { + byNamespace[namespace] = []*resource.Resource{} + } + byNamespace[namespace] = append(byNamespace[namespace], res) + } + return byNamespace +} + +// GroupedByNamespace implements ResMap.GroupByOrginalNamespace +func (m *resWrangler) GroupedByOriginalNamespace() map[string][]*resource.Resource { + items := m.groupedByOriginalNamespace() + delete(items, resid.TotallyNotANamespace) + return items +} + +func (m *resWrangler) groupedByOriginalNamespace() map[string][]*resource.Resource { + byNamespace := make(map[string][]*resource.Resource) + for _, res := range m.rList { + namespace := res.OrgId().EffectiveNamespace() + if _, found := byNamespace[namespace]; !found { + byNamespace[namespace] = []*resource.Resource{} + } + byNamespace[namespace] = append(byNamespace[namespace], res) + } + return byNamespace +} + +// AsYaml implements ResMap. +func (m *resWrangler) AsYaml() ([]byte, error) { + firstObj := true + var b []byte + buf := bytes.NewBuffer(b) + for _, res := range m.Resources() { + out, err := yaml.Marshal(res.Map()) + if err != nil { + return nil, err + } + if firstObj { + firstObj = false + } else { + if _, err = buf.WriteString("---\n"); err != nil { + return nil, err + } + } + if _, err = buf.Write(out); err != nil { + return nil, err + } + } + return buf.Bytes(), nil +} + +// ErrorIfNotEqualSets implements ResMap. +func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error { + m2, ok := other.(*resWrangler) + if !ok { + panic("bad cast") + } + if m.Size() != m2.Size() { + return fmt.Errorf( + "lists have different number of entries: %#v doesn't equal %#v", + m.rList, m2.rList) + } + seen := make(map[int]bool) + for _, r1 := range m.rList { + id := r1.CurId() + others := m2.GetMatchingResourcesByCurrentId(id.Equals) + if len(others) == 0 { + return fmt.Errorf( + "id in self missing from other; id: %s", id) + } + if len(others) > 1 { + return fmt.Errorf( + "id in self matches %d in other; id: %s", len(others), id) + } + r2 := others[0] + if !r1.KunstructEqual(r2) { + return fmt.Errorf( + "kunstruct not equal: \n -- %s,\n -- %s\n\n--\n%#v\n------\n%#v\n", + r1, r2, r1, r2) + } + seen[m2.indexOfResource(r2)] = true + } + if len(seen) != m.Size() { + return fmt.Errorf("counting problem %d != %d", len(seen), m.Size()) + } + return nil +} + +// ErrorIfNotEqualList implements ResMap. +func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error { + m2, ok := other.(*resWrangler) + if !ok { + panic("bad cast") + } + if m.Size() != m2.Size() { + return fmt.Errorf( + "lists have different number of entries: %#v doesn't equal %#v", + m.rList, m2.rList) + } + for i, r1 := range m.rList { + r2 := m2.rList[i] + if !r1.Equals(r2) { + return fmt.Errorf( + "Item i=%d differs:\n n1 = %s\n n2 = %s\n o1 = %s\n o2 = %s\n", + i, r1.OrgId(), r2.OrgId(), r1, r2) + } + } + return nil +} + +type resCopier func(r *resource.Resource) *resource.Resource + +// ShallowCopy implements ResMap. +func (m *resWrangler) ShallowCopy() ResMap { + return m.makeCopy( + func(r *resource.Resource) *resource.Resource { + return r + }) +} + +// DeepCopy implements ResMap. +func (m *resWrangler) DeepCopy() ResMap { + return m.makeCopy( + func(r *resource.Resource) *resource.Resource { + return r.DeepCopy() + }) +} + +// makeCopy copies the ResMap. +func (m *resWrangler) makeCopy(copier resCopier) ResMap { + result := &resWrangler{} + result.rList = make([]*resource.Resource, m.Size()) + for i, r := range m.rList { + result.rList[i] = copier(r) + } + return result +} + +// SubsetThatCouldBeReferencedByResource implements ResMap. +func (m *resWrangler) SubsetThatCouldBeReferencedByResource( + inputRes *resource.Resource) ResMap { + result := newOne() + inputId := inputRes.CurId() + isInputIdNamespaceable := inputId.IsNamespaceableKind() + rctxm := inputRes.PrefixesSuffixesEquals + subjectNamespaces := getNamespacesForRoleBinding(inputRes) + for _, r := range m.Resources() { + // Need to match more accuratly both at the time of selection and transformation. + // OutmostPrefixSuffixEquals is not accurate enough since it is only using + // the outer most suffix and the last prefix. Use PrefixedSuffixesEquals instead. + resId := r.CurId() + if (!isInputIdNamespaceable || !resId.IsNamespaceableKind() || resId.IsNsEquals(inputId) || + isRoleBindingNamespace(&subjectNamespaces, r.GetNamespace())) && r.InSameKustomizeCtx(rctxm) { + result.append(r) + } + } + return result +} + +// isRoleBindingNamespace returns true is the namespace `ns` is in role binding +// namespaces `m` +func isRoleBindingNamespace(m *map[string]bool, ns string) bool { + return (*m)[ns] +} + +// getNamespacesForRoleBinding returns referenced ServiceAccount namespaces if the inputRes is +// a RoleBinding +func getNamespacesForRoleBinding(inputRes *resource.Resource) map[string]bool { + res := make(map[string]bool) + if inputRes.GetKind() != "RoleBinding" { + return res + } + subjects, err := inputRes.GetSlice("subjects") + if err != nil || subjects == nil { + return res + } + + for _, s := range subjects { + subject := s.(map[string]interface{}) + if subject["namespace"] == nil || subject["kind"] == nil || + subject["kind"].(string) != "ServiceAccount" { + continue + } + res[subject["namespace"].(string)] = true + } + + return res +} + +func (m *resWrangler) append(res *resource.Resource) { + m.rList = append(m.rList, res) +} + +// AppendAll implements ResMap. +func (m *resWrangler) AppendAll(other ResMap) error { + if other == nil { + return nil + } + for _, res := range other.Resources() { + if err := m.Append(res); err != nil { + return err + } + } + return nil +} + +// AbsorbAll implements ResMap. +func (m *resWrangler) AbsorbAll(other ResMap) error { + if other == nil { + return nil + } + for _, r := range other.Resources() { + err := m.appendReplaceOrMerge(r) + if err != nil { + return err + } + } + return nil +} + +func (m *resWrangler) appendReplaceOrMerge( + res *resource.Resource) error { + id := res.CurId() + matches := m.GetMatchingResourcesByOriginalId(id.Equals) + if len(matches) == 0 { + matches = m.GetMatchingResourcesByCurrentId(id.Equals) + } + switch len(matches) { + case 0: + switch res.Behavior() { + case types.BehaviorMerge, types.BehaviorReplace: + return fmt.Errorf( + "id %#v does not exist; cannot merge or replace", id) + default: + // presumably types.BehaviorCreate + err := m.Append(res) + if err != nil { + return err + } + } + case 1: + old := matches[0] + if old == nil { + return fmt.Errorf("id lookup failure") + } + index := m.indexOfResource(old) + if index < 0 { + return fmt.Errorf("indexing problem") + } + switch res.Behavior() { + case types.BehaviorReplace: + res.Replace(old) + case types.BehaviorMerge: + res.Merge(old) + default: + return fmt.Errorf( + "id %#v exists; behavior must be merge or replace", id) + } + i, err := m.Replace(res) + if err != nil { + return err + } + if i != index { + return fmt.Errorf("unexpected index in replacement") + } + default: + return fmt.Errorf( + "found multiple objects %v that could accept merge of %v", + matches, id) + } + return nil +} + +func anchorRegex(pattern string) string { + if pattern == "" { + return pattern + } + return "^" + pattern + "$" +} + +// Select returns a list of resources that +// are selected by a Selector +func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { + ns := regexp.MustCompile(anchorRegex(s.Namespace)) + nm := regexp.MustCompile(anchorRegex(s.Name)) + var result []*resource.Resource + for _, r := range m.Resources() { + curId := r.CurId() + orgId := r.OrgId() + + // matches the namespace when namespace is not empty in the selector + // It first tries to match with the original namespace + // then matches with the current namespace + if r.GetNamespace() != "" { + matched := ns.MatchString(orgId.EffectiveNamespace()) + if !matched { + matched = ns.MatchString(curId.EffectiveNamespace()) + if !matched { + continue + } + } + } + + // matches the name when name is not empty in the selector + // It first tries to match with the original name + // then matches with the current name + if r.GetName() != "" { + matched := nm.MatchString(orgId.Name) + if !matched { + matched = nm.MatchString(curId.Name) + if !matched { + continue + } + } + } + + // matches the GVK + if !r.GetGvk().IsSelected(&s.Gvk) { + continue + } + + // matches the label selector + matched, err := r.MatchesLabelSelector(s.LabelSelector) + if err != nil { + return nil, err + } + if !matched { + continue + } + + // matches the annotation selector + matched, err = r.MatchesAnnotationSelector(s.AnnotationSelector) + if err != nil { + return nil, err + } + if !matched { + continue + } + result = append(result, r) + } + return result, nil +} diff --git a/api/resmap/reswrangler_test.go b/api/resmap/reswrangler_test.go new file mode 100644 index 000000000..b16a60f76 --- /dev/null +++ b/api/resmap/reswrangler_test.go @@ -0,0 +1,901 @@ +// Copyright 2019 The Kubernetes Authors. +// SPDX-License-Identifier: Apache-2.0 + +package resmap_test + +import ( + "fmt" + "reflect" + "strings" + "testing" + + "sigs.k8s.io/kustomize/api/k8sdeps/kunstruct" + "sigs.k8s.io/kustomize/api/resid" + . "sigs.k8s.io/kustomize/api/resmap" + "sigs.k8s.io/kustomize/api/resource" + resmaptest_test "sigs.k8s.io/kustomize/api/testutils/resmaptest" + "sigs.k8s.io/kustomize/api/types" +) + +var rf = resource.NewFactory( + kunstruct.NewKunstructuredFactoryImpl()) +var rmF = NewFactory(rf, nil) + +func doAppend(t *testing.T, w ResMap, r *resource.Resource) { + err := w.Append(r) + if err != nil { + t.Fatalf("append error: %v", err) + } +} +func doRemove(t *testing.T, w ResMap, id resid.ResId) { + 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, w1) { + 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) { + 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 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": "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": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "domino", + "namespace": "happy", + }, + }) + r6.AddNamePrefix("little-") + r7 := rf.FromMap( + map[string]interface{}{ + "apiVersion": "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 := m.SubsetThatCouldBeReferencedByResource(test.filter) + err := test.expected.ErrorIfNotEqualLists(got) + if err != nil { + test.expected.Debug("expected") + got.Debug("actual") + t.Fatalf("Expected match") + } + }) + } +} + +func addPfxSfx(r *resource.Resource, prefixes []string, suffixes []string) { + for _, pfx := range prefixes { + r.AddNamePrefix(pfx) + } + + for _, sfx := range suffixes { + r.AddNameSuffix(sfx) + } +} + +func TestSubsetThatCouldBeReferencedByResourceMultiLevel(t *testing.T) { + // Simulates ConfigMap and Deployment defined at level 1 + // No prefix nor suffix added at that level + cm1 := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "level1", + }, + }) + addPfxSfx(cm1, []string{""}, []string{""}) + dep1 := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "level1", + }, + }) + addPfxSfx(dep1, []string{""}, []string{""}) + + // Simulates ConfigMap and Deployment defined at level 1 + // and prefix added in level 2 of kustomization + cm2p := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "level2p", + }, + }) + addPfxSfx(cm2p, []string{"", "level2p-"}, []string{"", ""}) + + dep2p := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "level2p", + }, + }) + addPfxSfx(dep2p, []string{"", "level2p-"}, []string{"", ""}) + + // Simulates ConfigMap and Deployment defined at level 1 + // and suffix added in level 2 of kustomization + cm2s := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "level2s", + }, + }) + addPfxSfx(cm2s, []string{"", ""}, []string{"", "-level2s"}) + + dep2s := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "level2s", + }, + }) + addPfxSfx(dep2s, []string{"", ""}, []string{"", "-level2s"}) + + // Simulates ConfigMap and Deployment defined at level 1, + // prefix added in levels 2 and 3 of kustomization. + cm3e := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "level3e", + }, + }) + addPfxSfx(cm3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""}) + + dep3e := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "level3e", + }, + }) + addPfxSfx(dep3e, []string{"", "level2p-", "level3e-"}, []string{"", "", ""}) + + // Simulates Deployment defined at level 1, ConfigMap defined at level 2, + // prefix added in levels 2 and 3 of kustomization. + // This reproduce issue 1440. + cm3i := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "level3i", + }, + }) + addPfxSfx(cm3i, []string{"level2p-", "level3i-"}, []string{"", ""}) + + dep3i := rf.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "level3i", + }, + }) + addPfxSfx(dep3i, []string{"", "level2p-", "level3i-"}, []string{"", "", ""}) + + tests := map[string]struct { + filter *resource.Resource + expected ResMap + }{ + "level1": { + filter: dep1, + expected: resmaptest_test.NewRmBuilder(t, rf). + AddR(cm1).AddR(dep1).ResMap(), + }, + "level2p": { + filter: dep2p, + expected: resmaptest_test.NewRmBuilder(t, rf). + AddR(cm2p).AddR(dep2p).ResMap(), + }, + "level2s": { + filter: dep2s, + expected: resmaptest_test.NewRmBuilder(t, rf). + AddR(cm2s).AddR(dep2s).ResMap(), + }, + "level3p": { + filter: dep3e, + expected: resmaptest_test.NewRmBuilder(t, rf). + AddR(cm3e).AddR(dep3e).ResMap(), + }, + "level3i": { + filter: dep3i, + expected: resmaptest_test.NewRmBuilder(t, rf). + AddR(cm3i).AddR(dep3i).ResMap(), + }, + } + m := resmaptest_test.NewRmBuilder(t, rf). + AddR(cm1).AddR(dep1).AddR(cm2s).AddR(dep2s).AddR(cm2p).AddR(dep2p).AddR(cm3e).AddR(dep3e).AddR(cm3i).AddR(dep3i).ResMap() + for name, test := range tests { + test := test + t.Run(name, func(t *testing.T) { + got := m.SubsetThatCouldBeReferencedByResource(test.filter) + 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() ResMap { + return rmF.FromResource(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", + })) +} + +func makeMap2(b types.GenerationBehavior) ResMap { + return rmF.FromResource(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(), + })) +} + +func TestAbsorbAll(t *testing.T) { + expected := rmF.FromResource(rf.FromMapAndOption( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{}, + "labels": map[string]interface{}{}, + "name": "cmap", + }, + "data": map[string]interface{}{ + "a": "u", + "b": "v", + "c": "w", + }, + }, &types.GeneratorArgs{ + Behavior: "create", + })) + w := makeMap1() + if err := w.AbsorbAll(makeMap2(types.BehaviorMerge)); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := expected.ErrorIfNotEqualLists(w); err != nil { + t.Fatal(err) + } + w = makeMap1() + if err := w.AbsorbAll(nil); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := w.ErrorIfNotEqualLists(makeMap1()); err != nil { + t.Fatal(err) + } + w = makeMap1() + w2 := makeMap2(types.BehaviorReplace) + if err := w.AbsorbAll(w2); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if err := w2.ErrorIfNotEqualLists(w); err != nil { + t.Fatal(err) + } + w = makeMap1() + w2 = makeMap2(types.BehaviorUnspecified) + err := w.AbsorbAll(w2) + if err == nil { + t.Fatalf("expected error with unspecified behavior") + } +}