Make resource, resmap public.

This commit is contained in:
jregan
2019-10-20 10:01:06 -07:00
parent e2fd33c54a
commit 3af5a8afea
118 changed files with 288 additions and 288 deletions

136
api/resmap/factory.go Normal file
View File

@@ -0,0 +1,136 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap
import (
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/kusterr"
)
// Factory makes instances of ResMap.
type Factory struct {
resF *resource.Factory
tf PatchFactory
}
// NewFactory returns a new resmap.Factory.
func NewFactory(rf *resource.Factory, tf PatchFactory) *Factory {
return &Factory{resF: rf, tf: tf}
}
// RF returns a resource.Factory.
func (rmF *Factory) RF() *resource.Factory {
return rmF.resF
}
func New() ResMap {
return newOne()
}
// FromResource returns a ResMap with one entry.
func (rmF *Factory) FromResource(res *resource.Resource) ResMap {
m, err := newResMapFromResourceSlice([]*resource.Resource{res})
if err != nil {
panic(err)
}
return m
}
// FromFile returns a ResMap given a resource path.
func (rmF *Factory) FromFile(
loader ifc.Loader, path string) (ResMap, error) {
content, err := loader.Load(path)
if err != nil {
return nil, err
}
m, err := rmF.NewResMapFromBytes(content)
if err != nil {
return nil, kusterr.Handler(err, path)
}
return m, nil
}
// NewResMapFromBytes decodes a list of objects in byte array format.
func (rmF *Factory) NewResMapFromBytes(b []byte) (ResMap, error) {
resources, err := rmF.resF.SliceFromBytes(b)
if err != nil {
return nil, err
}
return newResMapFromResourceSlice(resources)
}
// NewResMapFromConfigMapArgs returns a Resource slice given
// a configmap metadata slice from kustomization file.
func (rmF *Factory) NewResMapFromConfigMapArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
argList []types.ConfigMapArgs) (ResMap, error) {
var resources []*resource.Resource
for _, args := range argList {
res, err := rmF.resF.MakeConfigMap(kvLdr, options, &args)
if err != nil {
return nil, errors.Wrap(err, "NewResMapFromConfigMapArgs")
}
resources = append(resources, res)
}
return newResMapFromResourceSlice(resources)
}
func (rmF *Factory) FromConfigMapArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args types.ConfigMapArgs) (ResMap, error) {
res, err := rmF.resF.MakeConfigMap(kvLdr, options, &args)
if err != nil {
return nil, err
}
return rmF.FromResource(res), nil
}
// NewResMapFromSecretArgs takes a SecretArgs slice, generates
// secrets from each entry, and accumulates them in a ResMap.
func (rmF *Factory) NewResMapFromSecretArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
argsList []types.SecretArgs) (ResMap, error) {
var resources []*resource.Resource
for _, args := range argsList {
res, err := rmF.resF.MakeSecret(kvLdr, options, &args)
if err != nil {
return nil, errors.Wrap(err, "NewResMapFromSecretArgs")
}
resources = append(resources, res)
}
return newResMapFromResourceSlice(resources)
}
func (rmF *Factory) FromSecretArgs(
kvLdr ifc.KvLoader,
options *types.GeneratorOptions,
args types.SecretArgs) (ResMap, error) {
res, err := rmF.resF.MakeSecret(kvLdr, options, &args)
if err != nil {
return nil, err
}
return rmF.FromResource(res), nil
}
func (rmF *Factory) MergePatches(patches []*resource.Resource) (
ResMap, error) {
return rmF.tf.MergePatches(patches, rmF.resF)
}
func newResMapFromResourceSlice(resources []*resource.Resource) (ResMap, error) {
result := New()
for _, res := range resources {
err := result.Append(res)
if err != nil {
return nil, err
}
}
return result, nil
}

266
api/resmap/factory_test.go Normal file
View File

@@ -0,0 +1,266 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap_test
import (
"encoding/base64"
"reflect"
"sigs.k8s.io/kustomize/v3/api/resid"
"testing"
"sigs.k8s.io/kustomize/v3/api/filesys"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/kv"
"sigs.k8s.io/kustomize/v3/api/loader"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/testutils/valtest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/internal/loadertest"
)
func TestFromFile(t *testing.T) {
resourceStr := `apiVersion: apps/v1
kind: Deployment
metadata:
name: dply1
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dply2
---
# some comment
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: dply2
namespace: test
---
`
l := loadertest.NewFakeLoader("/whatever/project")
if ferr := l.AddFile("/whatever/project/deployment.yaml", []byte(resourceStr)); ferr != nil {
t.Fatalf("Error adding fake file: %v\n", ferr)
}
expected := resmaptest_test.NewRmBuilder(t, rf).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply1",
}}).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply2",
}}).
Add(map[string]interface{}{
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": map[string]interface{}{
"name": "dply2",
"namespace": "test",
}}).ResMap()
m, _ := rmF.FromFile(l, "deployment.yaml")
if m.Size() != 3 {
t.Fatalf("result should contain 3, but got %d", m.Size())
}
if err := expected.ErrorIfNotEqualLists(m); err != nil {
t.Fatalf("actual doesn't match expected: %v", err)
}
}
func TestFromBytes(t *testing.T) {
encoded := []byte(`apiVersion: v1
kind: ConfigMap
metadata:
name: cm1
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cm2
`)
expected := 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()
m, err := rmF.NewResMapFromBytes(encoded)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reflect.DeepEqual(m, expected) {
t.Fatalf("%#v doesn't match expected %#v", m, expected)
}
}
var cmap = resid.Gvk{Version: "v1", Kind: "ConfigMap"}
func TestNewFromConfigMaps(t *testing.T) {
type testCase struct {
description string
input []types.ConfigMapArgs
filepath string
content string
expected ResMap
}
l := loadertest.NewFakeLoader("/whatever/project")
kvLdr := kv.NewLoader(l, valtest_test.MakeFakeValidator())
testCases := []testCase{
{
description: "construct config map from env",
input: []types.ConfigMapArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "envConfigMap",
KvPairSources: types.KvPairSources{
EnvSources: []string{"app.env"},
},
},
},
},
filepath: "/whatever/project/app.env",
content: "DB_USERNAME=admin\nDB_PASSWORD=somepw",
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "envConfigMap",
},
"data": map[string]interface{}{
"DB_USERNAME": "admin",
"DB_PASSWORD": "somepw",
}}).ResMap(),
},
{
description: "construct config map from file",
input: []types.ConfigMapArgs{{
GeneratorArgs: types.GeneratorArgs{
Name: "fileConfigMap",
KvPairSources: types.KvPairSources{
FileSources: []string{"app-init.ini"},
},
},
},
},
filepath: "/whatever/project/app-init.ini",
content: "FOO=bar\nBAR=baz\n",
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "fileConfigMap",
},
"data": map[string]interface{}{
"app-init.ini": `FOO=bar
BAR=baz
`,
},
}).ResMap(),
},
{
description: "construct config map from literal",
input: []types.ConfigMapArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "literalConfigMap",
KvPairSources: types.KvPairSources{
LiteralSources: []string{"a=x", "b=y", "c=\"Good Morning\"", "d=\"false\""},
},
},
},
},
expected: resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "literalConfigMap",
},
"data": map[string]interface{}{
"a": "x",
"b": "y",
"c": "Good Morning",
"d": "false",
},
}).ResMap(),
},
// TODO: add testcase for data coming from multiple sources like
// files/literal/env etc.
}
for _, tc := range testCases {
if fErr := l.AddFile(tc.filepath, []byte(tc.content)); fErr != nil {
t.Fatalf("Error adding fake file: %v\n", fErr)
}
r, err := rmF.NewResMapFromConfigMapArgs(kvLdr, nil, tc.input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if err = tc.expected.ErrorIfNotEqualLists(r); err != nil {
t.Fatalf("testcase: %q, err: %v", tc.description, err)
}
}
}
func TestNewResMapFromSecretArgs(t *testing.T) {
secrets := []types.SecretArgs{
{
GeneratorArgs: types.GeneratorArgs{
Name: "apple",
KvPairSources: types.KvPairSources{
LiteralSources: []string{
"DB_USERNAME=admin",
"DB_PASSWORD=somepw",
},
},
},
Type: ifc.SecretTypeOpaque,
},
}
fSys := filesys.MakeFsInMemory()
fSys.Mkdir(".")
actual, err := rmF.NewResMapFromSecretArgs(
kv.NewLoader(
loader.NewFileLoaderAtRoot(fSys),
valtest_test.MakeFakeValidator()), nil, secrets)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expected := resmaptest_test.NewRmBuilder(t, rf).Add(
map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "apple",
},
"type": ifc.SecretTypeOpaque,
"data": map[string]interface{}{
"DB_USERNAME": base64.StdEncoding.EncodeToString([]byte("admin")),
"DB_PASSWORD": base64.StdEncoding.EncodeToString([]byte("somepw")),
},
}).ResMap()
if err = expected.ErrorIfNotEqualLists(actual); err != nil {
t.Fatalf("error: %s", err)
}
}

37
api/resmap/idslice.go Normal file
View File

@@ -0,0 +1,37 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resmap
import (
"sort"
"sigs.k8s.io/kustomize/v3/api/resid"
)
// IdSlice implements the sort interface.
type IdSlice []resid.ResId
var _ sort.Interface = IdSlice{}
func (a IdSlice) Len() int { return len(a) }
func (a IdSlice) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a IdSlice) Less(i, j int) bool {
if !a[i].Gvk.Equals(a[j].Gvk) {
return a[i].Gvk.IsLessThan(a[j].Gvk)
}
return a[i].String() < a[j].String()
}

View File

@@ -0,0 +1,52 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package resmap
import (
"reflect"
"sort"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
)
func TestLess(t *testing.T) {
ids := IdSlice{
resid.NewResIdKindOnly("ConfigMap", "cm"),
resid.NewResIdKindOnly("Pod", "pod"),
resid.NewResIdKindOnly("Namespace", "ns1"),
resid.NewResIdKindOnly("Namespace", "ns2"),
resid.NewResIdKindOnly("Role", "ro"),
resid.NewResIdKindOnly("RoleBinding", "rb"),
resid.NewResIdKindOnly("CustomResourceDefinition", "crd"),
resid.NewResIdKindOnly("ServiceAccount", "sa"),
}
expected := IdSlice{
resid.NewResIdKindOnly("Namespace", "ns1"),
resid.NewResIdKindOnly("Namespace", "ns2"),
resid.NewResIdKindOnly("CustomResourceDefinition", "crd"),
resid.NewResIdKindOnly("ServiceAccount", "sa"),
resid.NewResIdKindOnly("Role", "ro"),
resid.NewResIdKindOnly("RoleBinding", "rb"),
resid.NewResIdKindOnly("ConfigMap", "cm"),
resid.NewResIdKindOnly("Pod", "pod"),
}
sort.Sort(ids)
if !reflect.DeepEqual(ids, expected) {
t.Fatalf("expected %+v but got %+v", expected, ids)
}
}

View File

@@ -0,0 +1,15 @@
/// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package patch holds miscellaneous interfaces used by kustomize.
package resmap
import (
"sigs.k8s.io/kustomize/v3/api/resource"
)
// PatchFactory makes transformers that require k8sdeps.
type PatchFactory interface {
MergePatches(patches []*resource.Resource,
rf *resource.Factory) (ResMap, error)
}

779
api/resmap/resmap.go Normal file
View File

@@ -0,0 +1,779 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
// Package resmap implements a map from ResId to Resource that
// tracks all resources in a kustomization.
package resmap
import (
"bytes"
"fmt"
"regexp"
"github.com/pkg/errors"
"sigs.k8s.io/kustomize/v3/api/ifc"
"sigs.k8s.io/kustomize/v3/api/resid"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/yaml"
)
// A Transformer modifies an instance of ResMap.
type Transformer interface {
// Transform modifies data in the argument,
// e.g. adding labels to resources that can be labelled.
Transform(m ResMap) error
}
// A Generator creates an instance of ResMap.
type Generator interface {
Generate() (ResMap, error)
}
// Something that's configurable accepts an
// instance of PluginHelpers and a raw config
// object (YAML in []byte form).
type Configurable interface {
Config(h *PluginHelpers, config []byte) error
}
// NewPluginHelpers makes an instance of PluginHelpers.
func NewPluginHelpers(ldr ifc.Loader, v ifc.Validator, rf *Factory) *PluginHelpers {
return &PluginHelpers{ldr: ldr, v: v, rf: rf}
}
// PluginHelpers holds things that any or all plugins might need.
// This should be available to each plugin, in addition to
// any plugin-specific configuration.
type PluginHelpers struct {
ldr ifc.Loader
v ifc.Validator
rf *Factory
}
func (c *PluginHelpers) Loader() ifc.Loader {
return c.ldr
}
func (c *PluginHelpers) ResmapFactory() *Factory {
return c.rf
}
func (c *PluginHelpers) Validator() ifc.Validator {
return c.v
}
type GeneratorPlugin interface {
Generator
Configurable
}
type TransformerPlugin interface {
Transformer
Configurable
}
// ResMap is an interface describing operations on the
// core kustomize data structure, a list of Resources.
//
// Every Resource has two ResIds: OrgId and CurId.
//
// In a ResMap, no two resources may have the same CurId,
// but they may have the same OrgId. The latter can happen
// when mixing two or more different overlays apply different
// transformations to a common base. When looking for a
// resource to transform, try the OrgId first, and if this
// fails or finds too many, it might make sense to then try
// the CurrId. Depends on the situation.
type ResMap interface {
// Size reports the number of resources.
Size() int
// Resources provides a discardable slice
// of resource pointers, returned in the order
// as appended.
Resources() []*resource.Resource
// Append adds a Resource. Error on CurId collision.
//
// A class invariant of ResMap is that all of its
// resources must differ in their value of
// CurId(), aka current Id. The Id is the tuple
// of {namespace, group, version, kind, name}
// (see ResId).
//
// This invariant reflects the invariant of a
// kubernetes cluster, where if one tries to add
// a resource to the cluster whose Id matches
// that of a resource already in the cluster,
// only two outcomes are allowed. Either the
// incoming resource is _merged_ into the existing
// one, or the incoming resource is rejected.
// One cannot end up with two resources
// in the cluster with the same Id.
Append(*resource.Resource) error
// AppendAll appends another ResMap to self,
// failing on any OrgId collision.
AppendAll(ResMap) error
// AbsorbAll appends, replaces or merges the contents
// of another ResMap into self,
// allowing and sometimes demanding ID collisions.
// A collision would be demanded, say, when a generated
// ConfigMap has the "replace" option in its generation
// instructions, meaning it _must_ replace
// something in the known set of resources.
// If a resource id for resource X is found to already
// be in self, then the behavior field for X must
// be BehaviorMerge or BehaviorReplace. If X is not in
// self, then its behavior _cannot_ be merge or replace.
AbsorbAll(ResMap) error
// AsYaml returns the yaml form of resources.
AsYaml() ([]byte, error)
// GetByIndex returns a resource at the given index,
// nil if out of range.
GetByIndex(int) *resource.Resource
// GetIndexOfCurrentId returns the index of the resource
// with the given CurId.
// Returns error if there is more than one match.
// Returns (-1, nil) if there is no match.
GetIndexOfCurrentId(id resid.ResId) (int, error)
// GetMatchingResourcesByCurrentId returns the resources
// who's CurId is matched by the argument.
GetMatchingResourcesByCurrentId(matches IdMatcher) []*resource.Resource
// GetMatchingResourcesByOriginalId returns the resources
// who's OriginalId is matched by the argument.
GetMatchingResourcesByOriginalId(matches IdMatcher) []*resource.Resource
// GetByCurrentId is shorthand for calling
// GetMatchingResourcesByCurrentId with a matcher requiring
// an exact match, returning an error on multiple or no matches.
GetByCurrentId(resid.ResId) (*resource.Resource, error)
// GetByOriginalId is shorthand for calling
// GetMatchingResourcesByOriginalId with a matcher requiring
// an exact match, returning an error on multiple or no matches.
GetByOriginalId(resid.ResId) (*resource.Resource, error)
// GetById is a helper function which first
// attempts GetByOriginalId, then GetByCurrentId,
// returning an error if both fail to find a single
// match.
GetById(resid.ResId) (*resource.Resource, error)
// GroupedByCurrentNamespace returns a map of namespace
// to a slice of *Resource in that namespace.
// Resources for whom IsNamespaceableKind is false are
// are not included at all (see NonNamespaceable).
// Resources with an empty namespace are placed
// in the resid.DefaultNamespace entry.
GroupedByCurrentNamespace() map[string][]*resource.Resource
// GroupByOrginalNamespace performs as GroupByNamespace
// but use the original namespace instead of the current
// one to perform the grouping.
GroupedByOriginalNamespace() map[string][]*resource.Resource
// NonNamespaceable returns a slice of resources that
// cannot be placed in a namespace, e.g.
// Node, ClusterRole, Namespace itself, etc.
NonNamespaceable() []*resource.Resource
// AllIds returns all CurrentIds.
AllIds() []resid.ResId
// Replace replaces the resource with the matching CurId.
// Error if there's no match or more than one match.
// Returns the index where the replacement happened.
Replace(*resource.Resource) (int, error)
// Remove removes the resource whose CurId matches the argument.
// Error if not found.
Remove(resid.ResId) error
// Clear removes all resources and Ids.
Clear()
// SubsetThatCouldBeReferencedByResource returns a ResMap subset
// of self with resources that could be referenced by the
// resource argument.
// This is a filter; it excludes things that cannot be
// referenced by the resource, e.g. objects in other
// namespaces. Cluster wide objects are never excluded.
SubsetThatCouldBeReferencedByResource(*resource.Resource) ResMap
// DeepCopy copies the ResMap and underlying resources.
DeepCopy() ResMap
// ShallowCopy copies the ResMap but
// not the underlying resources.
ShallowCopy() ResMap
// ErrorIfNotEqualSets returns an error if the
// argument doesn't have the same resources as self.
// Ordering is _not_ taken into account,
// as this function was solely used in tests written
// before internal resource order was maintained,
// and those tests are initialized with maps which
// by definition have random ordering, and will
// fail spuriously.
// TODO: modify tests to not use resmap.FromMap,
// TODO: - and replace this with a stricter equals.
ErrorIfNotEqualSets(ResMap) error
// ErrorIfNotEqualLists returns an error if the
// argument doesn't have the resource objects
// data as self, in the same order.
// Meta information is ignored; this is similar
// to comparing the AsYaml() strings, but allows
// for more informed errors on not equals.
ErrorIfNotEqualLists(ResMap) error
// Debug prints the ResMap.
Debug(title string)
// Select returns a list of resources that
// 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
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)) &&
r.InSameKustomizeCtx(rctxm) {
result.append(r)
}
}
return result
}
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; must 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
}

896
api/resmap/resmap_test.go Normal file
View File

@@ -0,0 +1,896 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package resmap_test
import (
"fmt"
"reflect"
"strings"
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/resource"
"sigs.k8s.io/kustomize/v3/api/testutils/resmaptest"
"sigs.k8s.io/kustomize/v3/api/types"
"sigs.k8s.io/kustomize/v3/k8sdeps/kunstruct"
)
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).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",
}, nil))
}
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(),
}, nil))
}
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",
}, nil))
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")
}
}

178
api/resmap/selector_test.go Normal file
View File

@@ -0,0 +1,178 @@
// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.
package resmap_test
import (
"testing"
"sigs.k8s.io/kustomize/v3/api/resid"
. "sigs.k8s.io/kustomize/v3/api/resmap"
"sigs.k8s.io/kustomize/v3/api/types"
)
func setupRMForPatchTargets(t *testing.T) ResMap {
result, err := rmF.NewResMapFromBytes([]byte(`
apiVersion: group1/v1
kind: Kind1
metadata:
name: name1
namespace: ns1
labels:
app: name1
annotations:
foo: bar
---
apiVersion: group1/v1
kind: Kind1
metadata:
name: name2
namespace: default
labels:
app: name2
annotations:
foo: bar
---
apiVersion: group1/v1
kind: Kind2
metadata:
name: name3
labels:
app: name3
annotations:
bar: baz
---
apiVersion: group1/v1
kind: Kind2
metadata:
name: x-name1
namespace: x-default
`))
if err != nil {
t.Fatalf("unexpected error %v", err)
}
return result
}
func TestFindPatchTargets(t *testing.T) {
rm := setupRMForPatchTargets(t)
testcases := []struct {
target types.Selector
count int
}{
{
target: types.Selector{
Name: "name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name.*",
AnnotationSelector: "foo=bar",
},
count: 2,
},
{
target: types.Selector{
LabelSelector: "app=name1",
},
count: 1,
},
{
target: types.Selector{
Gvk: resid.Gvk{
Kind: "Kind1",
},
Name: "name.*",
},
count: 2,
},
{
target: types.Selector{
Name: "NotMatched",
},
count: 0,
},
{
target: types.Selector{
Name: "",
},
count: 4,
},
{
target: types.Selector{
Namespace: "default",
},
count: 2,
},
{
target: types.Selector{
Namespace: "",
},
count: 4,
},
{
target: types.Selector{
Namespace: "default",
Name: "name.*",
Gvk: resid.Gvk{
Kind: "Kind1",
},
},
count: 1,
},
{
target: types.Selector{
Name: "^name.*",
},
count: 3,
},
{
target: types.Selector{
Name: "name.*$",
},
count: 3,
},
{
target: types.Selector{
Name: "^name.*$",
},
count: 3,
},
{
target: types.Selector{
Namespace: "^def.*",
},
count: 2,
},
{
target: types.Selector{
Namespace: "def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "^def.*$",
},
count: 2,
},
{
target: types.Selector{
Namespace: "default",
},
count: 2,
},
}
for _, testcase := range testcases {
actual, err := rm.Select(testcase.target)
if err != nil {
t.Errorf("unexpected error %v", err)
}
if len(actual) != testcase.count {
t.Errorf("expected %d objects, but got %d:\n%v", testcase.count, len(actual), actual)
}
}
}