diff --git a/api/builtins/HashTransformer.go b/api/builtins/HashTransformer.go index c7ce6f7e8..2e48923db 100644 --- a/api/builtins/HashTransformer.go +++ b/api/builtins/HashTransformer.go @@ -24,7 +24,7 @@ func (p *HashTransformerPlugin) Config( func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error { for _, res := range m.Resources() { if res.NeedHashSuffix() { - h, err := p.hasher.Hash(res) + h, err := res.Hash(p.hasher) if err != nil { return err } diff --git a/api/builtins/NamespaceTransformer.go b/api/builtins/NamespaceTransformer.go index 0ed2796a4..6794f26e8 100644 --- a/api/builtins/NamespaceTransformer.go +++ b/api/builtins/NamespaceTransformer.go @@ -30,20 +30,15 @@ func (p *NamespaceTransformerPlugin) Transform(m resmap.ResMap) error { return nil } for _, r := range m.Resources() { - empty, err := r.IsEmpty() - if err != nil { - return err - } - if empty { + if r.IsEmpty() { // Don't mutate empty objects? continue } r.StorePreviousId() - err = r.ApplyFilter(namespace.Filter{ + if err := r.ApplyFilter(namespace.Filter{ Namespace: p.Namespace, FsSlice: p.FieldSpecs, - }) - if err != nil { + }); err != nil { return err } matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals) diff --git a/api/go.mod b/api/go.mod index c2ec89663..91e71deaa 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,13 +6,11 @@ require ( github.com/evanphx/json-patch v4.5.0+incompatible github.com/go-errors/errors v1.0.1 github.com/go-openapi/spec v0.19.5 - github.com/google/go-cmp v0.4.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/imdario/mergo v0.3.5 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.4.0 gopkg.in/yaml.v2 v2.4.0 - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c sigs.k8s.io/kustomize/kyaml v0.10.15 sigs.k8s.io/yaml v1.2.0 ) diff --git a/api/go.sum b/api/go.sum index cdddf75f9..999238e26 100644 --- a/api/go.sum +++ b/api/go.sum @@ -242,7 +242,6 @@ golang.org/x/tools v0.0.0-20190125232054-d66bd3c5d5a6/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190617190820-da514acc4774/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= diff --git a/api/hasher/hasher.go b/api/hasher/hasher.go index 36d930af6..aef436d91 100644 --- a/api/hasher/hasher.go +++ b/api/hasher/hasher.go @@ -20,12 +20,12 @@ func SortArrayAndComputeHash(s []string) (string, error) { if err != nil { return "", err } - return Encode(Hash(string(data))) + return encode(hex256(string(data))) } // Copied from https://github.com/kubernetes/kubernetes // /blob/master/pkg/kubectl/util/hash/hash.go -func Encode(hex string) (string, error) { +func encode(hex string) (string, error) { if len(hex) < 10 { return "", fmt.Errorf( "input length must be at least 10") @@ -48,23 +48,18 @@ func Encode(hex string) (string, error) { return string(enc), nil } -// Hash returns the hex form of the sha256 of the argument. -func Hash(data string) string { +// hex256 returns the hex form of the sha256 of the argument. +func hex256(data string) string { return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) } -// HashRNode returns the hash value of input RNode -func HashRNode(node *yaml.RNode) (string, error) { - // get node kind - kindNode, err := node.Pipe(yaml.FieldMatcher{Name: "kind"}) - if err != nil { - return "", err - } - kind := kindNode.YNode().Value +// Hasher computes the hash of an RNode. +type Hasher struct{} - // calculate hash for different kinds - encoded := "" - switch kind { +// Hash returns a hash of the argument. +func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) { + var encoded string + switch node.GetKind() { case "ConfigMap": encoded, err = encodeConfigMap(node) case "Secret": @@ -77,10 +72,11 @@ func HashRNode(node *yaml.RNode) (string, error) { if err != nil { return "", err } - return Encode(Hash(encoded)) + return encode(hex256(encoded)) } -func getNodeValues(node *yaml.RNode, paths []string) (map[string]interface{}, error) { +func getNodeValues( + node *yaml.RNode, paths []string) (map[string]interface{}, error) { values := make(map[string]interface{}) for _, p := range paths { vn, err := node.Pipe(yaml.Lookup(p)) @@ -117,8 +113,11 @@ func encodeConfigMap(node *yaml.RNode) (string, error) { if err != nil { return "", err } - m := map[string]interface{}{"kind": "ConfigMap", "name": values["metadata/name"], - "data": values["data"]} + m := map[string]interface{}{ + "kind": "ConfigMap", + "name": values["metadata/name"], + "data": values["data"], + } if _, ok := values["binaryData"].(map[string]interface{}); ok { m["binaryData"] = values["binaryData"] } diff --git a/api/hasher/hasher_test.go b/api/hasher/hasher_test.go index f7e0c512e..5a31b934b 100644 --- a/api/hasher/hasher_test.go +++ b/api/hasher/hasher_test.go @@ -32,10 +32,10 @@ func TestSortArrayAndComputeHash(t *testing.T) { } } -func TestHash(t *testing.T) { +func Test_hex256(t *testing.T) { // hash the empty string to be sure that sha256 is being used expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" - sum := Hash("") + sum := hex256("") if expect != sum { t.Errorf("expected hash %q but got %q", expect, sum) } @@ -93,17 +93,17 @@ data: binaryData: two: ""`, "698h7c7t9m", ""}, } - + h := &Hasher{} for _, c := range cases { node, err := yaml.Parse(c.cmYaml) if err != nil { t.Fatal(err) } - h, err := HashRNode(node) + hashed, err := h.Hash(node) if SkipRest(t, c.desc, err, c.err) { continue } - if c.hash != h { + if c.hash != hashed { t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) } } @@ -154,17 +154,17 @@ type: my-type data: one: ""`, "74bd68bm66", ""}, } - + h := &Hasher{} for _, c := range cases { node, err := yaml.Parse(c.secretYaml) if err != nil { t.Fatal(err) } - h, err := HashRNode(node) + hashed, err := h.Hash(node) if SkipRest(t, c.desc, err, c.err) { continue } - if c.hash != h { + if c.hash != hashed { t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) } } @@ -191,17 +191,17 @@ spec: foo: 1 bar: abc`, "59m2mdccg4", ""}, } - + h := &Hasher{} for _, c := range cases { node, err := yaml.Parse(c.unstructured) if err != nil { t.Fatal(err) } - h, err := HashRNode(node) + hashed, err := h.Hash(node) if SkipRest(t, c.desc, err, c.err) { continue } - if c.hash != h { + if c.hash != hashed { t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) } } @@ -334,9 +334,10 @@ data: } } -// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, -// and logs the appropriate error on the test object. -// The return value indicates whether we should skip the rest of the test case due to the error result. +// SkipRest returns true if there was a non-nil error or if we expected an +// error that didn't happen, and logs the appropriate error on the test object. +// The return value indicates whether we should skip the rest of the test case +// due to the error result. func SkipRest(t *testing.T, desc string, err error, contains string) bool { if err != nil { if len(contains) == 0 { diff --git a/api/ifc/ifc.go b/api/ifc/ifc.go index ee1ce0338..acbfa868b 100644 --- a/api/ifc/ifc.go +++ b/api/ifc/ifc.go @@ -7,6 +7,7 @@ package ifc import ( "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/yaml" ) // Validator provides functions to validate annotations and labels @@ -123,7 +124,7 @@ type KunstructuredFactory interface { // KunstructuredHasher returns a hash of the argument // or an error. type KunstructuredHasher interface { - Hash(Kunstructured) (string, error) + Hash(*yaml.RNode) (string, error) } // See core.v1.SecretTypeOpaque diff --git a/api/internal/target/multitransformer.go b/api/internal/target/multitransformer.go index 1896bdf04..552e7f5d3 100644 --- a/api/internal/target/multitransformer.go +++ b/api/internal/target/multitransformer.go @@ -37,22 +37,10 @@ func (o *multiTransformer) Transform(m resmap.ResMap) error { func (o *multiTransformer) transform(m resmap.ResMap) error { for _, t := range o.transformers { - err := t.Transform(m) - if err != nil { + if err := t.Transform(m); err != nil { return err } - } - for _, r := range m.Resources() { - empty, err := r.IsEmpty() - if err != nil { - return err - } - if empty { - err := m.Remove(r.CurId()) - if err != nil { - return err - } - } + m.DropEmpties() } return nil } diff --git a/api/internal/wrappy/factory.go b/api/internal/wrappy/factory.go deleted file mode 100644 index b155ed3c6..000000000 --- a/api/internal/wrappy/factory.go +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package wrappy - -import ( - "fmt" - - "sigs.k8s.io/kustomize/api/hasher" - "sigs.k8s.io/kustomize/api/ifc" - "sigs.k8s.io/kustomize/api/internal/generators" - "sigs.k8s.io/kustomize/api/konfig" - "sigs.k8s.io/kustomize/api/types" - "sigs.k8s.io/kustomize/kyaml/filtersutil" - "sigs.k8s.io/kustomize/kyaml/kio" - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -// WNodeFactory makes instances of WNode. -// -// These instances in turn adapt -// sigs.k8s.io/kustomize/kyaml/yaml.RNode -// to implement ifc.Unstructured. -// This factory is meant to implement ifc.KunstructuredFactory. -// -// This implementation should be thin, as both WNode and WNodeFactory must be -// factored away (deleted) along with ifc.Kunstructured in favor of direct use -// of RNode methods upon completion of -// https://github.com/kubernetes-sigs/kustomize/issues/2506. -// -// See also api/krusty/internal/provider/depprovider.go -type WNodeFactory struct { -} - -var _ ifc.KunstructuredFactory = (*WNodeFactory)(nil) - -func (k *WNodeFactory) SliceFromBytes(bs []byte) ([]ifc.Kunstructured, error) { - yamlRNodes, err := kio.FromBytes(bs) - if err != nil { - return nil, err - } - var result []ifc.Kunstructured - for i := range yamlRNodes { - rn := yamlRNodes[i] - meta, err := rn.GetValidatedMetadata() - if err != nil { - return nil, err - } - if !shouldDropObject(meta) { - if foundNil, path := rn.HasNilEntryInList(); foundNil { - return nil, fmt.Errorf("empty item at %v in object %v", path, rn) - } - result = append(result, FromRNode(rn)) - } - } - return result, nil -} - -// shouldDropObject returns true if the resource should not be accumulated. -func shouldDropObject(m yaml.ResourceMeta) bool { - _, y := m.ObjectMeta.Annotations[konfig.IgnoredByKustomizeAnnotation] - return y -} - -func (k *WNodeFactory) FromMap(m map[string]interface{}) ifc.Kunstructured { - rn, err := FromMap(m) - if err != nil { - // TODO(#WNodeFactory): handle or bubble error" - panic(err) - } - return rn -} - -// kustHash computes a hash of an unstructured object. -type kustHash struct{} - -// Hash returns a hash of the given object -func (h *kustHash) Hash(m ifc.Kunstructured) (string, error) { - node, err := filtersutil.GetRNode(m) - if err != nil { - return "", err - } - return hasher.HashRNode(node) -} - -func (k *WNodeFactory) Hasher() ifc.KunstructuredHasher { - return &kustHash{} -} - -// MakeConfigMap makes a wrapped configmap. -func (k *WNodeFactory) MakeConfigMap( - ldr ifc.KvLoader, args *types.ConfigMapArgs) (ifc.Kunstructured, error) { - rn, err := generators.MakeConfigMap(ldr, args) - if err != nil { - return nil, err - } - return FromRNode(rn), nil -} - -// MakeSecret makes a wrapped secret. -func (k *WNodeFactory) MakeSecret( - ldr ifc.KvLoader, args *types.SecretArgs) (ifc.Kunstructured, error) { - rn, err := generators.MakeSecret(ldr, args) - if err != nil { - return nil, err - } - return FromRNode(rn), nil -} diff --git a/api/internal/wrappy/factory_test.go b/api/internal/wrappy/factory_test.go deleted file mode 100644 index cbc73c565..000000000 --- a/api/internal/wrappy/factory_test.go +++ /dev/null @@ -1,324 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package wrappy_test - -import ( - "fmt" - "reflect" - "testing" - - "github.com/stretchr/testify/assert" - . "sigs.k8s.io/kustomize/api/internal/wrappy" -) - -func TestHasher(t *testing.T) { - input := ` -apiVersion: v1 -kind: ConfigMap -metadata: - name: foo -data: - one: "" -binaryData: - two: "" -` - expect := "698h7c7t9m" - - factory := &WNodeFactory{} - k, err := factory.SliceFromBytes([]byte(input)) - if err != nil { - t.Fatal(err) - } - - hasher := factory.Hasher() - result, err := hasher.Hash(k[0]) - if err != nil { - t.Fatal(err) - } - if result != expect { - t.Fatalf("expect %s but got %s", expect, result) - } -} - -func TestSliceFromBytes(t *testing.T) { - factory := &WNodeFactory{} - testConfigMap := - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMap", - "metadata": map[string]interface{}{ - "name": "winnie", - }, - } - testConfigMapList := - map[string]interface{}{ - "apiVersion": "v1", - "kind": "ConfigMapList", - "items": []interface{}{ - testConfigMap, - testConfigMap, - }, - } - testDeploymentSpec := map[string]interface{}{ - "template": map[string]interface{}{ - "spec": map[string]interface{}{ - "hostAliases": []interface{}{ - map[string]interface{}{ - "hostnames": []interface{}{ - "a.example.com", - }, - "ip": "8.8.8.8", - }, - }, - }, - }, - } - testDeploymentA := map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "deployment-a", - }, - "spec": testDeploymentSpec, - } - testDeploymentB := map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "deployment-b", - }, - "spec": testDeploymentSpec, - } - testDeploymentList := - map[string]interface{}{ - "apiVersion": "v1", - "kind": "DeploymentList", - "items": []interface{}{ - testDeploymentA, - testDeploymentB, - }, - } - - type expected struct { - out []map[string]interface{} - isErr bool - } - - testCases := map[string]struct { - input []byte - exp expected - }{ - "garbage": { - input: []byte("garbageIn: garbageOut"), - exp: expected{ - isErr: true, - }, - }, - "noBytes": { - input: []byte{}, - exp: expected{ - out: []map[string]interface{}{}, - }, - }, - "goodJson": { - input: []byte(` -{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}} -`), - exp: expected{ - out: []map[string]interface{}{testConfigMap}, - }, - }, - "goodYaml1": { - input: []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie -`), - exp: expected{ - out: []map[string]interface{}{testConfigMap}, - }, - }, - "goodYaml2": { - input: []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie -`), - exp: expected{ - out: []map[string]interface{}{testConfigMap, testConfigMap}, - }, - }, - "localConfigYaml": { - input: []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie-skip - annotations: - # this annotation causes the Resource to be ignored by kustomize - config.kubernetes.io/local-config: "" ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie -`), - exp: expected{ - out: []map[string]interface{}{testConfigMap}, - }, - }, - "garbageInOneOfTwoObjects": { - input: []byte(` -apiVersion: v1 -kind: ConfigMap -metadata: - name: winnie ---- -WOOOOOOOOOOOOOOOOOOOOOOOOT: woot -`), - exp: expected{ - isErr: true, - }, - }, - "emptyObjects": { - input: []byte(` ---- -#a comment - ---- - -`), - exp: expected{ - out: []map[string]interface{}{}, - }, - }, - "Missing .metadata.name in object": { - input: []byte(` -apiVersion: v1 -kind: Namespace -metadata: - annotations: - foo: bar -`), - exp: expected{ - isErr: true, - }, - }, - "nil value in list": { - input: []byte(` -apiVersion: builtin -kind: ConfigMapGenerator -metadata: - name: kube100-site - labels: - app: web -testList: -- testA -- -`), - exp: expected{ - isErr: true, - }, - }, - "List": { - input: []byte(` -apiVersion: v1 -kind: List -items: -- apiVersion: v1 - kind: ConfigMap - metadata: - name: winnie -- apiVersion: v1 - kind: ConfigMap - metadata: - name: winnie -`), - exp: expected{ - out: []map[string]interface{}{ - testConfigMap, - testConfigMap}, - }, - }, - "ConfigMapList": { - input: []byte(` -apiVersion: v1 -kind: ConfigMapList -items: -- apiVersion: v1 - kind: ConfigMap - metadata: - name: winnie -- apiVersion: v1 - kind: ConfigMap - metadata: - name: winnie -`), - exp: expected{ - out: []map[string]interface{}{testConfigMapList}, - }, - }, - "listWithAnchors": { - input: []byte(` -apiVersion: v1 -kind: DeploymentList -items: -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: deployment-a - spec: &hostAliases - template: - spec: - hostAliases: - - hostnames: - - a.example.com - ip: 8.8.8.8 -- apiVersion: apps/v1 - kind: Deployment - metadata: - name: deployment-b - spec: - <<: *hostAliases -`), - exp: expected{ - out: []map[string]interface{}{testDeploymentList}, - }, - }, - } - - for n := range testCases { - tc := testCases[n] - t.Run(n, func(t *testing.T) { - rs, err := factory.SliceFromBytes(tc.input) - if err != nil { - assert.True(t, tc.exp.isErr) - return - } - assert.False(t, tc.exp.isErr) - assert.Equal(t, len(tc.exp.out), len(rs)) - for i := range rs { - rsMap, err := rs[i].Map() - assert.NoError(t, err) - assert.Equal( - t, fmt.Sprintf("%v", tc.exp.out[i]), fmt.Sprintf("%v", rsMap)) - if n != "listWithAnchors" { - // https://github.com/kubernetes-sigs/kustomize/issues/3271 - m, _ := rs[i].Map() - if !reflect.DeepEqual(tc.exp.out[i], m) { - t.Fatalf("%s:\nexpected: %v\n actual: %v", - n, tc.exp.out[i], m) - } - } - } - }) - } -} diff --git a/api/internal/wrappy/wnode.go b/api/internal/wrappy/wnode.go deleted file mode 100644 index 81a35d174..000000000 --- a/api/internal/wrappy/wnode.go +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2020 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package wrappy - -import ( - "fmt" - "log" - "regexp" - "strconv" - "strings" - - "sigs.k8s.io/kustomize/api/ifc" - "sigs.k8s.io/kustomize/api/resid" - "sigs.k8s.io/kustomize/kyaml/yaml" -) - -// WNode implements ifc.Kunstructured using yaml.RNode. -// -// It exists only to help manage a switch from -// kunstruct.UnstructAdapter to yaml.RNode as the core -// representation of KRM objects in kustomize. -// -// It's got a silly name because we don't want it around for long, -// and want its use to be obvious. -type WNode struct { - node *yaml.RNode -} - -var _ ifc.Kunstructured = (*WNode)(nil) - -func NewWNode() *WNode { - return FromRNode(yaml.NewRNode(nil)) -} - -func FromMap(m map[string]interface{}) (*WNode, error) { - n, err := yaml.FromMap(m) - if err != nil { - return nil, err - } - return FromRNode(n), nil -} - -func FromRNode(node *yaml.RNode) *WNode { - return &WNode{node: node} -} - -func (wn *WNode) AsRNode() *yaml.RNode { - return wn.node -} - -func (wn *WNode) demandMetaData(label string) yaml.ResourceMeta { - meta, err := wn.node.GetMeta() - if err != nil { - // Log and die since interface doesn't allow error. - log.Fatalf("for %s', expected valid resource: %v", label, err) - } - return meta -} - -// Copy implements ifc.Kunstructured. -func (wn *WNode) Copy() ifc.Kunstructured { - return &WNode{node: wn.node.Copy()} -} - -// GetAnnotations implements ifc.Kunstructured. -func (wn *WNode) GetAnnotations() map[string]string { - return wn.demandMetaData("GetAnnotations").Annotations -} - -// convertSliceIndex traverses the items in `fields` and find -// if there is a slice index in the item and change it to a -// valid Lookup field path. For example, 'ports[0]' will be -// converted to 'ports' and '0'. -func convertSliceIndex(fields []string) []string { - var res []string - for _, s := range fields { - if !strings.HasSuffix(s, "]") { - res = append(res, s) - continue - } - re := regexp.MustCompile(`^(.*)\[(\d+)\]$`) - groups := re.FindStringSubmatch(s) - if len(groups) == 0 { - // no match, add to result - res = append(res, s) - continue - } - if groups[1] != "" { - res = append(res, groups[1]) - } - res = append(res, groups[2]) - } - return res -} - -// GetFieldValue implements ifc.Kunstructured. -func (wn *WNode) GetFieldValue(path string) (interface{}, error) { - fields := convertSliceIndex(strings.Split(path, ".")) - rn, err := wn.node.Pipe(yaml.Lookup(fields...)) - if err != nil { - return nil, err - } - if rn == nil { - return nil, NoFieldError{path} - } - yn := rn.YNode() - - // If this is an alias node, resolve it - if yn.Kind == yaml.AliasNode { - yn = yn.Alias - } - - // Return value as map for DocumentNode and MappingNode kinds - if yn.Kind == yaml.DocumentNode || yn.Kind == yaml.MappingNode { - var result map[string]interface{} - if err := yn.Decode(&result); err != nil { - return nil, err - } - return result, err - } - - // Return value as slice for SequenceNode kind - if yn.Kind == yaml.SequenceNode { - var result []interface{} - if err := yn.Decode(&result); err != nil { - return nil, err - } - return result, nil - } - if yn.Kind != yaml.ScalarNode { - return nil, fmt.Errorf("expected ScalarNode, got Kind=%d", yn.Kind) - } - - // TODO: When doing kustomize var replacement, which is likely a - // a primary use of this function and the reason it returns interface{} - // rather than string, we do conversion from Nodes to Go types and back - // to nodes. We should figure out how to do replacement using raw nodes, - // assuming we keep the var feature in kustomize. - // The other end of this is: refvar.go:updateNodeValue. - switch yn.Tag { - case yaml.NodeTagString: - return yn.Value, nil - case yaml.NodeTagInt: - return strconv.Atoi(yn.Value) - case yaml.NodeTagFloat: - return strconv.ParseFloat(yn.Value, 64) - case yaml.NodeTagBool: - return strconv.ParseBool(yn.Value) - default: - // Possibly this should be an error or log. - return yn.Value, nil - } -} - -// GetGvk implements ifc.Kunstructured. -func (wn *WNode) GetGvk() resid.Gvk { - meta := wn.demandMetaData("GetGvk") - g, v := resid.ParseGroupVersion(meta.APIVersion) - return resid.Gvk{Group: g, Version: v, Kind: meta.Kind} -} - -// GetDataMap implements ifc.Kunstructured. -func (wn *WNode) GetDataMap() map[string]string { - return wn.node.GetDataMap() -} - -// SetDataMap implements ifc.Kunstructured. -func (wn *WNode) SetDataMap(m map[string]string) { - wn.node.SetDataMap(m) -} - -// GetBinaryDataMap implements ifc.Kunstructured. -func (wn *WNode) GetBinaryDataMap() map[string]string { - return wn.node.GetBinaryDataMap() -} - -// SetBinaryDataMap implements ifc.Kunstructured. -func (wn *WNode) SetBinaryDataMap(m map[string]string) { - wn.node.SetBinaryDataMap(m) -} - -// GetKind implements ifc.Kunstructured. -func (wn *WNode) GetKind() string { - return wn.demandMetaData("GetKind").Kind -} - -// GetLabels implements ifc.Kunstructured. -func (wn *WNode) GetLabels() map[string]string { - return wn.demandMetaData("GetLabels").Labels -} - -// GetName implements ifc.Kunstructured. -func (wn *WNode) GetName() string { - return wn.demandMetaData("GetName").Name -} - -// GetSlice implements ifc.Kunstructured. -func (wn *WNode) GetSlice(path string) ([]interface{}, error) { - value, err := wn.GetFieldValue(path) - if err != nil { - return nil, err - } - if sliceValue, ok := value.([]interface{}); ok { - return sliceValue, nil - } - return nil, fmt.Errorf("node %s is not a slice", path) -} - -// GetSlice implements ifc.Kunstructured. -func (wn *WNode) GetString(path string) (string, error) { - value, err := wn.GetFieldValue(path) - if err != nil { - return "", err - } - if v, ok := value.(string); ok { - return v, nil - } - return "", fmt.Errorf("node %s is not a string: %v", path, value) -} - -// Map implements ifc.Kunstructured. -func (wn *WNode) Map() (map[string]interface{}, error) { - return wn.node.Map() -} - -// MarshalJSON implements ifc.Kunstructured. -func (wn *WNode) MarshalJSON() ([]byte, error) { - return wn.node.MarshalJSON() -} - -// MatchesAnnotationSelector implements ifc.Kunstructured. -func (wn *WNode) MatchesAnnotationSelector(selector string) (bool, error) { - return wn.node.MatchesAnnotationSelector(selector) -} - -// MatchesLabelSelector implements ifc.Kunstructured. -func (wn *WNode) MatchesLabelSelector(selector string) (bool, error) { - return wn.node.MatchesLabelSelector(selector) -} - -// SetAnnotations implements ifc.Kunstructured. -func (wn *WNode) SetAnnotations(annotations map[string]string) { - if err := wn.node.SetAnnotations(annotations); err != nil { - log.Fatal(err) // interface doesn't allow error. - } -} - -// SetGvk implements ifc.Kunstructured. -func (wn *WNode) SetGvk(gvk resid.Gvk) { - wn.setMapField(yaml.NewScalarRNode(gvk.Kind), yaml.KindField) - wn.setMapField(yaml.NewScalarRNode(gvk.ApiVersion()), yaml.APIVersionField) -} - -// SetLabels implements ifc.Kunstructured. -func (wn *WNode) SetLabels(labels map[string]string) { - if err := wn.node.SetLabels(labels); err != nil { - log.Fatal(err) // interface doesn't allow error. - } -} - -// SetName implements ifc.Kunstructured. -func (wn *WNode) SetName(name string) { - wn.setMapField(yaml.NewScalarRNode(name), yaml.MetadataField, yaml.NameField) -} - -// SetNamespace implements ifc.Kunstructured. -func (wn *WNode) SetNamespace(ns string) { - if err := wn.node.SetNamespace(ns); err != nil { - log.Fatal(err) // interface doesn't allow error. - } -} - -func (wn *WNode) setMapField(value *yaml.RNode, path ...string) { - if err := wn.node.SetMapField(value, path...); err != nil { - // Log and die since interface doesn't allow error. - log.Fatalf("failed to set field %v: %v", path, err) - } -} - -// UnmarshalJSON implements ifc.Kunstructured. -func (wn *WNode) UnmarshalJSON(data []byte) error { - return wn.node.UnmarshalJSON(data) -} - -type NoFieldError struct { - Field string -} - -func (e NoFieldError) Error() string { - return fmt.Sprintf("no field named '%s'", e.Field) -} diff --git a/api/internal/wrappy/wnode_test.go b/api/internal/wrappy/wnode_test.go deleted file mode 100644 index c23f8d430..000000000 --- a/api/internal/wrappy/wnode_test.go +++ /dev/null @@ -1,670 +0,0 @@ -// Copyright 2019 The Kubernetes Authors. -// SPDX-License-Identifier: Apache-2.0 - -package wrappy - -import ( - "strings" - "testing" - - "github.com/google/go-cmp/cmp" - "github.com/stretchr/testify/assert" - "gopkg.in/yaml.v3" - "sigs.k8s.io/kustomize/api/resid" - kyaml "sigs.k8s.io/kustomize/kyaml/yaml" -) - -const ( - deploymentLittleJson = `{"apiVersion":"apps/v1","kind":"Deployment",` + - `"metadata":{"name":"homer","namespace":"simpsons"}}` - - deploymentBiggerJson = ` -{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": { - "name": "homer", - "namespace": "simpsons", - "labels": { - "fruit": "apple", - "veggie": "carrot" - }, - "annotations": { - "area": "51", - "greeting": "Take me to your leader." - } - }, - "spec": { - "template": { - "spec": { - "containers": [ - { - "env": [ - { - "name": "CM_FOO", - "valueFrom": { - "configMapKeyRef": { - "key": "somekey", - "name": "myCm" - } - } - }, - { - "name": "SECRET_FOO", - "valueFrom": { - "secretKeyRef": { - "key": "someKey", - "name": "mySecret" - } - } - } - ], - "image": "nginx:1.7.9", - "name": "nginx" - } - ] - } - } - } -} -` - bigMapYaml = `Kind: Service -complextree: - - field1: - - boolfield: true - floatsubfield: 1.01 - intsubfield: 1010 - stringsubfield: idx1010 - - boolfield: false - floatsubfield: 1.011 - intsubfield: 1011 - stringsubfield: idx1011 - field2: - - boolfield: true - floatsubfield: 1.02 - intsubfield: 1020 - stringsubfield: idx1020 - - boolfield: false - floatsubfield: 1.021 - intsubfield: 1021 - stringsubfield: idx1021 - - field1: - - boolfield: true - floatsubfield: 1.11 - intsubfield: 1110 - stringsubfield: idx1110 - - boolfield: false - floatsubfield: 1.111 - intsubfield: 1111 - stringsubfield: idx1111 - field2: - - boolfield: true - floatsubfield: 1.112 - intsubfield: 1120 - stringsubfield: idx1120 - - boolfield: false - floatsubfield: 1.1121 - intsubfield: 1121 - stringsubfield: idx1121 -metadata: - labels: - app: application-name - name: service-name -spec: - ports: - port: 80 -that: - - idx0 - - idx1 - - idx2 - - idx3 -these: - - field1: - - idx010 - - idx011 - field2: - - idx020 - - idx021 - - field1: - - idx110 - - idx111 - field2: - - idx120 - - idx121 - - field1: - - idx210 - - idx211 - field2: - - idx220 - - idx221 -this: - is: - aBool: true - aFloat: 1.001 - aNilValue: null - aNumber: 1000 - anEmptyMap: {} - anEmptySlice: [] -those: - - field1: idx0foo - field2: idx0bar - - field1: idx1foo - field2: idx1bar - - field1: idx2foo - field2: idx2bar -` -) - -func makeBigMap() map[string]interface{} { - return map[string]interface{}{ - "Kind": "Service", - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": "application-name", - }, - "name": "service-name", - }, - "spec": map[string]interface{}{ - "ports": map[string]interface{}{ - "port": int64(80), - }, - }, - "this": map[string]interface{}{ - "is": map[string]interface{}{ - "aNumber": int64(1000), - "aFloat": float64(1.001), - "aNilValue": nil, - "aBool": true, - "anEmptyMap": map[string]interface{}{}, - "anEmptySlice": []interface{}{}, - /* - TODO: test for unrecognizable (e.g. a function) - "unrecognizable": testing.InternalExample{ - Name: "fooBar", - }, - */ - }, - }, - "that": []interface{}{ - "idx0", - "idx1", - "idx2", - "idx3", - }, - "those": []interface{}{ - map[string]interface{}{ - "field1": "idx0foo", - "field2": "idx0bar", - }, - map[string]interface{}{ - "field1": "idx1foo", - "field2": "idx1bar", - }, - map[string]interface{}{ - "field1": "idx2foo", - "field2": "idx2bar", - }, - }, - "these": []interface{}{ - map[string]interface{}{ - "field1": []interface{}{"idx010", "idx011"}, - "field2": []interface{}{"idx020", "idx021"}, - }, - map[string]interface{}{ - "field1": []interface{}{"idx110", "idx111"}, - "field2": []interface{}{"idx120", "idx121"}, - }, - map[string]interface{}{ - "field1": []interface{}{"idx210", "idx211"}, - "field2": []interface{}{"idx220", "idx221"}, - }, - }, - "complextree": []interface{}{ - map[string]interface{}{ - "field1": []interface{}{ - map[string]interface{}{ - "stringsubfield": "idx1010", - "intsubfield": int64(1010), - "floatsubfield": float64(1.010), - "boolfield": true, - }, - map[string]interface{}{ - "stringsubfield": "idx1011", - "intsubfield": int64(1011), - "floatsubfield": float64(1.011), - "boolfield": false, - }, - }, - "field2": []interface{}{ - map[string]interface{}{ - "stringsubfield": "idx1020", - "intsubfield": int64(1020), - "floatsubfield": float64(1.020), - "boolfield": true, - }, - map[string]interface{}{ - "stringsubfield": "idx1021", - "intsubfield": int64(1021), - "floatsubfield": float64(1.021), - "boolfield": false, - }, - }, - }, - map[string]interface{}{ - "field1": []interface{}{ - map[string]interface{}{ - "stringsubfield": "idx1110", - "intsubfield": int64(1110), - "floatsubfield": float64(1.110), - "boolfield": true, - }, - map[string]interface{}{ - "stringsubfield": "idx1111", - "intsubfield": int64(1111), - "floatsubfield": float64(1.111), - "boolfield": false, - }, - }, - "field2": []interface{}{ - map[string]interface{}{ - "stringsubfield": "idx1120", - "intsubfield": int64(1120), - "floatsubfield": float64(1.1120), - "boolfield": true, - }, - map[string]interface{}{ - "stringsubfield": "idx1121", - "intsubfield": int64(1121), - "floatsubfield": float64(1.1121), - "boolfield": false, - }, - }, - }, - }, - } -} - -func TestBasicYamlOperationFromMap(t *testing.T) { - bytes, err := yaml.Marshal(makeBigMap()) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - if string(bytes) != bigMapYaml { - t.Fatalf("unexpected string equality") - } - rNode, err := kyaml.Parse(string(bytes)) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - rNodeString := rNode.MustString() - // The result from MustString has more indentation - // than bigMapYaml. - rNodeStrings := strings.Split(rNodeString, "\n") - bigMapStrings := strings.Split(bigMapYaml, "\n") - if len(rNodeStrings) != len(bigMapStrings) { - t.Fatalf("line count mismatch") - } - for i := range rNodeStrings { - s1 := strings.TrimSpace(rNodeStrings[i]) - s2 := strings.TrimSpace(bigMapStrings[i]) - if s1 != s2 { - t.Fatalf("expected '%s'=='%s'", s1, s2) - } - } -} - -func TestRoundTripJSON(t *testing.T) { - wn := NewWNode() - err := wn.UnmarshalJSON([]byte(deploymentLittleJson)) - if err != nil { - t.Fatalf("unexpected UnmarshalJSON err: %v", err) - } - data, err := wn.MarshalJSON() - if err != nil { - t.Fatalf("unexpected MarshalJSON err: %v", err) - } - actual := string(data) - if actual != deploymentLittleJson { - t.Fatalf("expected %s, got %s", deploymentLittleJson, actual) - } -} - -func TestGettingFields(t *testing.T) { - wn := NewWNode() - err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)) - if err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - gvk := wn.GetGvk() - expected := "apps" - actual := gvk.Group - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - expected = "v1" - actual = gvk.Version - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - expected = "Deployment" - actual = gvk.Kind - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - actual = wn.GetKind() - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - expected = "homer" - actual = wn.GetName() - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - actualMap := wn.GetLabels() - v, ok := actualMap["fruit"] - if !ok || v != "apple" { - t.Fatalf("unexpected labels '%v'", actualMap) - } - actualMap = wn.GetAnnotations() - v, ok = actualMap["greeting"] - if !ok || v != "Take me to your leader." { - t.Fatalf("unexpected annotations '%v'", actualMap) - } -} - -func TestGetFieldValueReturnsMap(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - expected := map[string]interface{}{ - "fruit": "apple", - "veggie": "carrot", - } - actual, err := wn.GetFieldValue("metadata.labels") - if err != nil { - t.Fatalf("error getting field value: %v", err) - } - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual map does not deep equal expected map:\n%v", diff) - } -} - -func TestGetFieldValueReturnsStuff(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - expected := []interface{}{ - map[string]interface{}{ - "env": []interface{}{ - map[string]interface{}{ - "name": "CM_FOO", - "valueFrom": map[string]interface{}{ - "configMapKeyRef": map[string]interface{}{ - "key": "somekey", - "name": "myCm", - }, - }, - }, - map[string]interface{}{ - "name": "SECRET_FOO", - "valueFrom": map[string]interface{}{ - "secretKeyRef": map[string]interface{}{ - "key": "someKey", - "name": "mySecret", - }, - }, - }, - }, - "image": string("nginx:1.7.9"), - "name": string("nginx"), - }, - } - actual, err := wn.GetFieldValue("spec.template.spec.containers") - if err != nil { - t.Fatalf("error getting field value: %v", err) - } - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual map does not deep equal expected map:\n%v", diff) - } - // Cannot go deeper yet. - _, err = wn.GetFieldValue("spec.template.spec.containers.env") - if err == nil { - t.Fatalf("expected err %v", err) - } -} - -func TestGetFieldValueReturnsSlice(t *testing.T) { - bytes, err := yaml.Marshal(makeBigMap()) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - rNode, err := kyaml.Parse(string(bytes)) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - wn := FromRNode(rNode) - expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} - actual, err := wn.GetFieldValue("that") - if err != nil { - t.Fatalf("error getting slice: %v", err) - } - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) - } -} - -func TestGetFieldValueReturnsSliceOfMappings(t *testing.T) { - bytes, err := yaml.Marshal(makeBigMap()) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - rNode, err := kyaml.Parse(string(bytes)) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - wn := FromRNode(rNode) - expected := []interface{}{ - map[string]interface{}{ - "field1": "idx0foo", - "field2": "idx0bar", - }, - map[string]interface{}{ - "field1": "idx1foo", - "field2": "idx1bar", - }, - map[string]interface{}{ - "field1": "idx2foo", - "field2": "idx2bar", - }, - } - actual, err := wn.GetFieldValue("those") - if err != nil { - t.Fatalf("error getting slice: %v", err) - } - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) - } -} - -func TestGetFieldValueReturnsString(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - actual, err := wn.GetFieldValue("metadata.labels.fruit") - if err != nil { - t.Fatalf("error getting field value: %v", err) - } - v, ok := actual.(string) - if !ok || v != "apple" { - t.Fatalf("unexpected value '%v'", actual) - } -} - -func TestGetFieldValueResolvesAlias(t *testing.T) { - yamlWithAlias := ` -foo: &a theValue -bar: *a -` - rNode, err := kyaml.Parse(yamlWithAlias) - if err != nil { - t.Fatalf("unexpected yaml parse error: %v", err) - } - wn := FromRNode(rNode) - actual, err := wn.GetFieldValue("bar") - if err != nil { - t.Fatalf("error getting field value: %v", err) - } - v, ok := actual.(string) - if !ok || v != "theValue" { - t.Fatalf("unexpected value '%v'", actual) - } -} - -func TestGetString(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - expected := "carrot" - actual, err := wn.GetString("metadata.labels.veggie") - if err != nil { - t.Fatalf("error getting string: %v", err) - } - if expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} - -func TestGetSlice(t *testing.T) { - bytes, err := yaml.Marshal(makeBigMap()) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - rNode, err := kyaml.Parse(string(bytes)) - if err != nil { - t.Fatalf("unexpected yaml.Marshal err: %v", err) - } - wn := FromRNode(rNode) - expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} - actual, err := wn.GetSlice("that") - if err != nil { - t.Fatalf("error getting slice: %v", err) - } - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) - } -} - -func TestMapEmpty(t *testing.T) { - newNodeMap, err := NewWNode().Map() - assert.NoError(t, err) - assert.Equal(t, 0, len(newNodeMap)) -} - -func TestMap(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentLittleJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - - expected := map[string]interface{}{ - "apiVersion": "apps/v1", - "kind": "Deployment", - "metadata": map[string]interface{}{ - "name": "homer", - "namespace": "simpsons", - }, - } - - actual, err := wn.Map() - assert.NoError(t, err) - if diff := cmp.Diff(expected, actual); diff != "" { - t.Fatalf("actual map does not deep equal expected map:\n%v", diff) - } -} - -func TestSetName(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - wn.SetName("marge") - if expected, actual := "marge", wn.GetName(); expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} - -func TestSetNamespace(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - wn.SetNamespace("flanders") - meta, _ := wn.node.GetMeta() - if expected, actual := "flanders", meta.Namespace; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} - -func TestSetLabels(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - wn.SetLabels(map[string]string{ - "label1": "foo", - "label2": "bar", - }) - labels := wn.GetLabels() - if expected, actual := 2, len(labels); expected != actual { - t.Fatalf("expected '%d', got '%d'", expected, actual) - } - if expected, actual := "foo", labels["label1"]; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - if expected, actual := "bar", labels["label2"]; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} - -func TestGetAnnotations(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - wn.SetAnnotations(map[string]string{ - "annotation1": "foo", - "annotation2": "bar", - }) - annotations := wn.GetAnnotations() - if expected, actual := 2, len(annotations); expected != actual { - t.Fatalf("expected '%d', got '%d'", expected, actual) - } - if expected, actual := "foo", annotations["annotation1"]; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - if expected, actual := "bar", annotations["annotation2"]; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} - -func TestSetGvk(t *testing.T) { - wn := NewWNode() - if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { - t.Fatalf("unexpected unmarshaljson err: %v", err) - } - wn.SetGvk(resid.GvkFromString("grp_ver_knd")) - gvk := wn.GetGvk() - if expected, actual := "grp", gvk.Group; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - if expected, actual := "ver", gvk.Version; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } - if expected, actual := "knd", gvk.Kind; expected != actual { - t.Fatalf("expected '%s', got '%s'", expected, actual) - } -} diff --git a/api/krusty/duplicatekeys_test.go b/api/krusty/duplicatekeys_test.go index 016aa61f0..bf480ca73 100644 --- a/api/krusty/duplicatekeys_test.go +++ b/api/krusty/duplicatekeys_test.go @@ -3,6 +3,7 @@ package krusty_test import ( "testing" + "github.com/stretchr/testify/assert" kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" ) @@ -35,5 +36,8 @@ spec: - name: PODINFO_UI_COLOR value: "#34577c" `) - th.RunWithErr(".", th.MakeDefaultOptions()) + m := th.Run(".", th.MakeDefaultOptions()) + _, err := m.AsYaml() + assert.Error(t, err) + assert.Contains(t, err.Error(), "mapping key \"env\" already defined") } diff --git a/api/provider/depprovider.go b/api/provider/depprovider.go index ff950578f..c6adba684 100644 --- a/api/provider/depprovider.go +++ b/api/provider/depprovider.go @@ -4,10 +4,10 @@ package provider import ( + "sigs.k8s.io/kustomize/api/hasher" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/internal/conflict" "sigs.k8s.io/kustomize/api/internal/validate" - "sigs.k8s.io/kustomize/api/internal/wrappy" "sigs.k8s.io/kustomize/api/resource" ) @@ -145,17 +145,14 @@ import ( // If you're reading this, plan not done. // type DepProvider struct { - kFactory ifc.KunstructuredFactory resourceFactory *resource.Factory conflictDectectorFactory resource.ConflictDetectorFactory fieldValidator ifc.Validator } func NewDepProvider() *DepProvider { - kf := &wrappy.WNodeFactory{} - rf := resource.NewFactory(kf) + rf := resource.NewFactory(&hasher.Hasher{}) return &DepProvider{ - kFactory: kf, resourceFactory: rf, conflictDectectorFactory: conflict.NewFactory(), fieldValidator: validate.NewFieldValidator(), @@ -166,10 +163,6 @@ func NewDefaultDepProvider() *DepProvider { return NewDepProvider() } -func (dp *DepProvider) GetKunstructuredFactory() ifc.KunstructuredFactory { - return dp.kFactory -} - func (dp *DepProvider) GetResourceFactory() *resource.Factory { return dp.resourceFactory } diff --git a/api/resid/gvk.go b/api/resid/gvk.go index 70f710738..b112151d3 100644 --- a/api/resid/gvk.go +++ b/api/resid/gvk.go @@ -36,6 +36,14 @@ func ParseGroupVersion(apiVersion string) (group, version string) { // GvkFromString makes a Gvk from the output of Gvk.String(). func GvkFromString(s string) Gvk { values := strings.Split(s, fieldSep) + if len(values) != 3 { + // ...then the string didn't come from Gvk.String(). + return Gvk{ + Group: noGroup, + Version: noVersion, + Kind: noKind, + } + } g := values[0] if g == noGroup { g = "" @@ -213,7 +221,10 @@ func (x Gvk) toKyamlTypeMeta() yaml.TypeMeta { } } -// IsNamespaceableKind returns true if x is a namespaceable Gvk +// IsNamespaceableKind returns true if x is a namespaceable Gvk, +// e.g. instances of Pod and Deployment are namespaceable, +// but instances of Node and Namespace are not namespaceable. +// Alternative name for this method: IsNotClusterScoped. // Implements https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#not-all-objects-are-in-a-namespace func (x Gvk) IsNamespaceableKind() bool { isNamespaceScoped, found := openapi.IsNamespaceScoped(x.toKyamlTypeMeta()) diff --git a/api/resid/gvk_test.go b/api/resid/gvk_test.go index e6ea4f209..790cc087e 100644 --- a/api/resid/gvk_test.go +++ b/api/resid/gvk_test.go @@ -103,6 +103,12 @@ func TestString(t *testing.T) { } } +func TestGvkFromString(t *testing.T) { + for _, hey := range stringTests { + assert.Equal(t, hey.x, GvkFromString(hey.s)) + } +} + func TestApiVersion(t *testing.T) { for _, hey := range []struct { x Gvk diff --git a/api/resmap/factory.go b/api/resmap/factory.go index a93b0ad6c..0e287530c 100644 --- a/api/resmap/factory.go +++ b/api/resmap/factory.go @@ -146,18 +146,10 @@ func newResMapFromResourceSlice( } // NewResMapFromRNodeSlice returns a ResMap from a slice of RNodes -func (rmF *Factory) NewResMapFromRNodeSlice(rnodes []*yaml.RNode) (ResMap, error) { - var resources []*resource.Resource - for _, rnode := range rnodes { - s, err := rnode.String() - if err != nil { - return nil, err - } - r, err := rmF.resF.SliceFromBytes([]byte(s)) - if err != nil { - return nil, err - } - resources = append(resources, r...) +func (rmF *Factory) NewResMapFromRNodeSlice(s []*yaml.RNode) (ResMap, error) { + rs, err := rmF.resF.ResourcesFromRNodes(s) + if err != nil { + return nil, err } - return newResMapFromResourceSlice(resources) + return newResMapFromResourceSlice(rs) } diff --git a/api/resmap/factory_test.go b/api/resmap/factory_test.go index 040bc6d19..e0f2ff601 100644 --- a/api/resmap/factory_test.go +++ b/api/resmap/factory_test.go @@ -336,17 +336,18 @@ metadata: expected: resmaptest_test.NewRmBuilder(t, rf).ResMap(), }, } - for name, tc := range testcases { - rnodes := []*yaml.RNode{ - yaml.MustParse(tc.input), - } - rm, err := rmF.NewResMapFromRNodeSlice(rnodes) - if err != nil { - t.Fatalf("unexpected error in test case [%s]: %v", name, err) - } - if err = tc.expected.ErrorIfNotEqualLists(rm); err != nil { - t.Fatalf("error in test case [%s]: %s", name, err) - } + for name := range testcases { + tc := testcases[name] + t.Run(name, func(t *testing.T) { + rm, err := rmF.NewResMapFromRNodeSlice( + []*yaml.RNode{yaml.MustParse(tc.input)}) + if err != nil { + t.Fatalf("unexpected error in test case [%s]: %v", name, err) + } + if err = tc.expected.ErrorIfNotEqualLists(rm); err != nil { + t.Fatalf("error in test case [%s]: %s", name, err) + } + }) } } diff --git a/api/resmap/resmap.go b/api/resmap/resmap.go index b803453e1..41d2ad5e5 100644 --- a/api/resmap/resmap.go +++ b/api/resmap/resmap.go @@ -80,6 +80,9 @@ type TransformerPlugin interface { // 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. +// +// TODO: get rid of this interface (use bare resWrangler). +// There aren't multiple implementations any more. type ResMap interface { // Size reports the number of resources. Size() int @@ -189,6 +192,9 @@ type ResMap interface { // Clear removes all resources and Ids. Clear() + // DropEmpties drops empty resources from the ResMap. + DropEmpties() + // SubsetThatCouldBeReferencedByResource returns a ResMap subset // of self with resources that could be referenced by the // resource argument. @@ -231,9 +237,8 @@ type ResMap interface { // are selected by a Selector Select(types.Selector) ([]*resource.Resource, error) - // ToRNodeSlice converts the resources in the resmp - // to a list of RNodes - ToRNodeSlice() ([]*yaml.RNode, error) + // ToRNodeSlice returns a copy of the resources as RNodes. + ToRNodeSlice() []*yaml.RNode // ApplySmPatch applies a strategic-merge patch to the // selected set of resources. diff --git a/api/resmap/reswrangler.go b/api/resmap/reswrangler.go index 478b67e1b..a5e0db38c 100644 --- a/api/resmap/reswrangler.go +++ b/api/resmap/reswrangler.go @@ -6,14 +6,12 @@ package resmap import ( "bytes" "fmt" - "strings" "github.com/pkg/errors" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/types" - kyaml_yaml "sigs.k8s.io/kustomize/kyaml/yaml" - "sigs.k8s.io/yaml" + kyaml "sigs.k8s.io/kustomize/kyaml/yaml" ) // resWrangler implements ResMap. @@ -38,6 +36,18 @@ func (m *resWrangler) Clear() { m.rList = nil } +// DropEmpties quickly drops empty resources. +// It doesn't use Append, which checks for Id collisions. +func (m *resWrangler) DropEmpties() { + var rList []*resource.Resource + for _, r := range m.rList { + if !r.IsEmpty() { + rList = append(rList, r) + } + } + m.rList = rList +} + // Size implements ResMap. func (m *resWrangler) Size() int { return len(m.rList) @@ -66,22 +76,27 @@ func (m *resWrangler) Append(res *resource.Resource) error { return fmt.Errorf( "may not add resource with an already registered id: %s", id) } - m.rList = append(m.rList, res) + m.append(res) return nil } +// append appends without performing an Id check +func (m *resWrangler) append(res *resource.Resource) { + m.rList = append(m.rList, res) +} + // Remove implements ResMap. func (m *resWrangler) Remove(adios resid.ResId) error { - tmp := newOne() + var rList []*resource.Resource for _, r := range m.rList { if r.CurId() != adios { - tmp.Append(r) + rList = append(rList, r) } } - if tmp.Size() != m.Size()-1 { + if len(rList) != m.Size()-1 { return fmt.Errorf("id %s not found in removal", adios) } - m.rList = tmp.rList + m.rList = rList return nil } @@ -118,16 +133,7 @@ func (m *resWrangler) Debug(title string) { } else { fmt.Println("---") } - fmt.Printf("# %d %s\n", i, r.OrgId()) - m, err := r.Map() - if err != nil { - panic(err) - } - blob, err := yaml.Marshal(m) - if err != nil { - panic(err) - } - fmt.Println(string(blob)) + fmt.Printf("# %d %s\n%s\n", i, r.OrgId(), r.String()) } } @@ -273,7 +279,7 @@ func (m *resWrangler) AsYaml() ([]byte, error) { firstObj := true var b []byte buf := bytes.NewBuffer(b) - for _, res := range m.Resources() { + for _, res := range m.rList { out, err := res.AsYAML() if err != nil { m, _ := res.Map() @@ -297,7 +303,7 @@ func (m *resWrangler) AsYaml() ([]byte, error) { func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error { m2, ok := other.(*resWrangler) if !ok { - panic("bad cast") + return fmt.Errorf("bad cast to resWrangler 1") } if m.Size() != m2.Size() { return fmt.Errorf( @@ -334,7 +340,7 @@ func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error { func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error { m2, ok := other.(*resWrangler) if !ok { - panic("bad cast") + return fmt.Errorf("bad cast to resWrangler 2") } if m.Size() != m2.Size() { return fmt.Errorf( @@ -388,7 +394,7 @@ func (m *resWrangler) SubsetThatCouldBeReferencedByResource( } result := newOne() roleBindingNamespaces := getNamespacesForRoleBinding(referrer) - for _, possibleTarget := range m.Resources() { + for _, possibleTarget := range m.rList { id := possibleTarget.CurId() if !id.IsNamespaceableKind() { // A cluster-scoped resource can be referred to by anything. @@ -435,16 +441,21 @@ func getNamespacesForRoleBinding(r *resource.Resource) map[string]bool { 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() { + m2, ok := other.(*resWrangler) + if !ok { + return fmt.Errorf("bad cast to resWrangler 3") + } + return m.appendAll(m2.rList) +} + +// appendAll appends all the resources, error on Id collision. +func (m *resWrangler) appendAll(list []*resource.Resource) error { + for _, res := range list { if err := m.Append(res); err != nil { return err } @@ -457,7 +468,11 @@ func (m *resWrangler) AbsorbAll(other ResMap) error { if other == nil { return nil } - for _, r := range other.Resources() { + m2, ok := other.(*resWrangler) + if !ok { + return fmt.Errorf("bad cast to resWrangler 4") + } + for _, r := range m2.rList { err := m.appendReplaceOrMerge(r) if err != nil { return err @@ -522,7 +537,7 @@ func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { if err != nil { return nil, err } - for _, r := range m.Resources() { + for _, r := range m.rList { curId := r.CurId() orgId := r.OrgId() @@ -567,78 +582,39 @@ func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) { return result, nil } -// ToRNodeSlice converts the resources in the resmp -// to a list of RNodes -func (m *resWrangler) ToRNodeSlice() ([]*kyaml_yaml.RNode, error) { - var rnodes []*kyaml_yaml.RNode - for _, r := range m.Resources() { - s, err := r.AsYAML() - if err != nil { - return nil, err - } - rnode, err := kyaml_yaml.Parse(string(s)) - if err != nil { - return nil, err - } - rnodes = append(rnodes, rnode) +// ToRNodeSlice returns a copy of the resources as RNodes. +func (m *resWrangler) ToRNodeSlice() []*kyaml.RNode { + result := make([]*kyaml.RNode, len(m.rList)) + for i := range m.rList { + result[i] = m.rList[i].AsRNode() } - return rnodes, nil + return result } +// ApplySmPatch applies the patch, and errors on Id collisions. func (m *resWrangler) ApplySmPatch( selectedSet *resource.IdSet, patch *resource.Resource) error { - newRm := New() - for _, res := range m.Resources() { - if !selectedSet.Contains(res.CurId()) { - newRm.Append(res) - continue - } - patchCopy := patch.DeepCopy() - patchCopy.CopyMergeMetaDataFieldsFrom(patch) - patchCopy.SetGvk(res.GetGvk()) - patchCopy.SetKind(patch.GetKind()) - err := res.ApplySmPatch(patchCopy) - if err != nil { - // Check for an error string from UnmarshalJSON that's indicative - // of an object that's missing basic KRM fields, and thus may have been - // entirely deleted (an acceptable outcome). This error handling should - // be deleted along with use of ResMap and apimachinery functions like - // UnmarshalJSON. - if !strings.Contains(err.Error(), "Object 'Kind' is missing") { - // Some unknown error, let it through. + var list []*resource.Resource + for _, res := range m.rList { + if selectedSet.Contains(res.CurId()) { + patchCopy := patch.DeepCopy() + patchCopy.CopyMergeMetaDataFieldsFrom(patch) + patchCopy.SetGvk(res.GetGvk()) + patchCopy.SetKind(patch.GetKind()) + if err := res.ApplySmPatch(patchCopy); err != nil { return err } - empty, err := res.IsEmpty() - if err != nil { - return err - } - if !empty { - m, _ := res.Map() - return errors.Wrapf( - err, "with unexpectedly non-empty object map of size %d", - len(m)) - } - // Fall through to handle deleted object. } - empty, err := res.IsEmpty() - if err != nil { - return err - } - if !empty { - // IsEmpty means all fields have been removed from the object. - // This can happen if a patch required deletion of the - // entire resource (not just a part of it). This means - // the overall resmap must shrink by one. - newRm.Append(res) + if !res.IsEmpty() { + list = append(list, res) } } m.Clear() - m.AppendAll(newRm) - return nil + return m.appendAll(list) } func (m *resWrangler) RemoveBuildAnnotations() { - for _, r := range m.Resources() { + for _, r := range m.rList { r.RemoveBuildAnnotations() } } diff --git a/api/resmap/reswrangler_test.go b/api/resmap/reswrangler_test.go index e6bce67a8..3c3963b42 100644 --- a/api/resmap/reswrangler_test.go +++ b/api/resmap/reswrangler_test.go @@ -877,13 +877,8 @@ rules: if err != nil { t.Fatalf("unexpected error: %v", err) } - rnodes, err := rm.ToRNodeSlice() - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - b := bytes.NewBufferString("") - for i, n := range rnodes { + for i, n := range rm.ToRNodeSlice() { if i != 0 { b.WriteString("---\n") } diff --git a/api/resource/factory.go b/api/resource/factory.go index b04b3ef3b..b8730b489 100644 --- a/api/resource/factory.go +++ b/api/resource/factory.go @@ -10,28 +10,33 @@ import ( "strings" "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/api/internal/generators" "sigs.k8s.io/kustomize/api/internal/kusterr" + "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/types" + "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" ) // Factory makes instances of Resource. type Factory struct { - kf ifc.KunstructuredFactory + hasher ifc.KunstructuredHasher } // NewFactory makes an instance of Factory. -func NewFactory(kf ifc.KunstructuredFactory) *Factory { - return &Factory{kf: kf} +func NewFactory(h ifc.KunstructuredHasher) *Factory { + return &Factory{hasher: h} } +// Hasher returns an ifc.KunstructuredHasher func (rf *Factory) Hasher() ifc.KunstructuredHasher { - return rf.kf.Hasher() + return rf.hasher } // FromMap returns a new instance of Resource. func (rf *Factory) FromMap(m map[string]interface{}) *Resource { - return rf.makeOne(rf.kf.FromMap(m), nil) + return rf.FromMapAndOption(m, nil) } // FromMapWithName returns a new instance with the given "original" name. @@ -41,35 +46,30 @@ func (rf *Factory) FromMapWithName(n string, m map[string]interface{}) *Resource // FromMapWithNamespaceAndName returns a new instance with the given "original" namespace. func (rf *Factory) FromMapWithNamespaceAndName(ns string, n string, m map[string]interface{}) *Resource { - r := rf.makeOne(rf.kf.FromMap(m), nil) + r := rf.FromMapAndOption(m, nil) return r.setPreviousId(ns, n, r.GetKind()) } // FromMapAndOption returns a new instance of Resource with given options. func (rf *Factory) FromMapAndOption( m map[string]interface{}, args *types.GeneratorArgs) *Resource { - return rf.makeOne(rf.kf.FromMap(m), types.NewGenArgs(args)) -} - -// FromKunstructured returns a new instance of Resource. -func (rf *Factory) FromKunstructured(u ifc.Kunstructured) *Resource { - return rf.makeOne(u, nil) + n, err := yaml.FromMap(m) + if err != nil { + // TODO: return err instead of log. + log.Fatal(err) + } + return rf.makeOne(n, types.NewGenArgs(args)) } // makeOne returns a new instance of Resource. -func (rf *Factory) makeOne( - u ifc.Kunstructured, o *types.GenArgs) *Resource { - if u == nil { - log.Fatal("unstruct ifc must not be null") +func (rf *Factory) makeOne(rn *yaml.RNode, o *types.GenArgs) *Resource { + if rn == nil { + log.Fatal("RNode must not be null") } if o == nil { o = types.NewGenArgs(nil) } - r := &Resource{ - kunStr: u, - options: o, - } - return r + return &Resource{kunStr: rn, options: o} } // SliceFromPatches returns a slice of resources given a patch path @@ -106,47 +106,135 @@ func (rf *Factory) FromBytes(in []byte) (*Resource, error) { // SliceFromBytes unmarshals bytes into a Resource slice. func (rf *Factory) SliceFromBytes(in []byte) ([]*Resource, error) { - kunStructs, err := rf.kf.SliceFromBytes(in) + nodes, err := rf.RNodesFromBytes(in) if err != nil { return nil, err } - var result []*Resource - for len(kunStructs) > 0 { - u := kunStructs[0] - kunStructs = kunStructs[1:] - if strings.HasSuffix(u.GetKind(), "List") { - m, err := u.Map() - if err != nil { - return nil, err + return rf.resourcesFromRNodes(nodes), nil +} + +// ResourcesFromRNodes converts RNodes to Resources. +func (rf *Factory) ResourcesFromRNodes( + nodes []*yaml.RNode) (result []*Resource, err error) { + nodes, err = rf.dropBadNodes(nodes) + if err != nil { + return nil, err + } + return rf.resourcesFromRNodes(nodes), nil +} + +// resourcesFromRNode assumes all nodes are good. +func (rf *Factory) resourcesFromRNodes( + nodes []*yaml.RNode) (result []*Resource) { + for _, n := range nodes { + result = append(result, rf.makeOne(n, nil)) + } + return +} + +func (rf *Factory) RNodesFromBytes(b []byte) (result []*yaml.RNode, err error) { + nodes, err := kio.FromBytes(b) + if err != nil { + return nil, err + } + nodes, err = rf.dropBadNodes(nodes) + if err != nil { + return nil, err + } + for len(nodes) > 0 { + n0 := nodes[0] + nodes = nodes[1:] + kind := n0.GetKind() + if !strings.HasSuffix(kind, "List") { + result = append(result, n0) + continue + } + // Convert a FooList into a slice of Foo. + var m map[string]interface{} + m, err = n0.Map() + if err != nil { + return nil, err + } + items, ok := m["items"] + if !ok { + // treat as an empty list + continue + } + slice, ok := items.([]interface{}) + if !ok { + if items == nil { + // an empty list + continue } - items := m["items"] - itemsSlice, ok := items.([]interface{}) - if !ok { - if items == nil { - // an empty list - continue - } - return nil, fmt.Errorf("items in List is type %T, expected array", items) - } - for _, item := range itemsSlice { - itemJSON, err := json.Marshal(item) - if err != nil { - return nil, err - } - innerU, err := rf.kf.SliceFromBytes(itemJSON) - if err != nil { - return nil, err - } - // append innerU to kunStructs so nested Lists can be handled - kunStructs = append(kunStructs, innerU...) - } - } else { - result = append(result, rf.FromKunstructured(u)) + return nil, fmt.Errorf( + "expected array in %s/items, but found %T", kind, items) + } + innerNodes, err := rf.convertObjectSliceToNodeSlice(slice) + if err != nil { + return nil, err + } + nodes = append(nodes, innerNodes...) + } + return result, nil +} + +// convertObjectSlice converts a list of objects to a list of RNode. +func (rf *Factory) convertObjectSliceToNodeSlice( + objects []interface{}) (result []*yaml.RNode, err error) { + var bytes []byte + var nodes []*yaml.RNode + for _, obj := range objects { + bytes, err = json.Marshal(obj) + if err != nil { + return + } + nodes, err = kio.FromBytes(bytes) + if err != nil { + return + } + nodes, err = rf.dropBadNodes(nodes) + if err != nil { + return + } + result = append(result, nodes...) + } + return +} + +// dropBadNodes may drop some nodes from its input argument. +func (rf *Factory) dropBadNodes(nodes []*yaml.RNode) ([]*yaml.RNode, error) { + var result []*yaml.RNode + for _, n := range nodes { + ignore, err := rf.shouldIgnore(n) + if err != nil { + return nil, err + } + if !ignore { + result = append(result, n) } } return result, nil } +// shouldIgnore returns true if there's some reason to ignore the node. +func (rf *Factory) shouldIgnore(n *yaml.RNode) (bool, error) { + if n.IsNilOrEmpty() { + return true, nil + } + md, err := n.GetValidatedMetadata() + if err != nil { + return true, err + } + _, ignore := md.ObjectMeta.Annotations[konfig.IgnoredByKustomizeAnnotation] + if ignore { + return true, nil + } + if foundNil, path := n.HasNilEntryInList(); foundNil { + return true, fmt.Errorf("empty item at %v in object %v", path, n) + } + return false, nil +} + // SliceFromBytesWithNames unmarshals bytes into a Resource slice with specified original // name. func (rf *Factory) SliceFromBytesWithNames(names []string, in []byte) ([]*Resource, error) { @@ -165,18 +253,18 @@ func (rf *Factory) SliceFromBytesWithNames(names []string, in []byte) ([]*Resour // MakeConfigMap makes an instance of Resource for ConfigMap func (rf *Factory) MakeConfigMap(kvLdr ifc.KvLoader, args *types.ConfigMapArgs) (*Resource, error) { - u, err := rf.kf.MakeConfigMap(kvLdr, args) + rn, err := generators.MakeConfigMap(kvLdr, args) if err != nil { return nil, err } - return rf.makeOne(u, types.NewGenArgs(&args.GeneratorArgs)), nil + return rf.makeOne(rn, types.NewGenArgs(&args.GeneratorArgs)), nil } // MakeSecret makes an instance of Resource for Secret func (rf *Factory) MakeSecret(kvLdr ifc.KvLoader, args *types.SecretArgs) (*Resource, error) { - u, err := rf.kf.MakeSecret(kvLdr, args) + rn, err := generators.MakeSecret(kvLdr, args) if err != nil { return nil, err } - return rf.makeOne(u, types.NewGenArgs(&args.GeneratorArgs)), nil + return rf.makeOne(rn, types.NewGenArgs(&args.GeneratorArgs)), nil } diff --git a/api/resource/factory_test.go b/api/resource/factory_test.go index e205c138c..e9267e3ce 100644 --- a/api/resource/factory_test.go +++ b/api/resource/factory_test.go @@ -5,6 +5,7 @@ package resource_test import ( "fmt" + "reflect" "testing" "github.com/stretchr/testify/assert" @@ -151,35 +152,33 @@ spec: for name := range testCases { tc := testCases[name] t.Run(name, func(t *testing.T) { - result, err := factory.SliceFromBytes([]byte(tc.input)) + result, err := factory.RNodesFromBytes([]byte(tc.input)) if err != nil { t.Fatalf("%v: fails with err: %v", name, err) } if len(result) != len(tc.expected) { for i := range result { - bytes, err := result[i].AsYAML() + str, err := result[i].String() if err != nil { t.Fatalf("%v: result to YAML fails with err: %v", name, err) } - tmp := string(bytes) - t.Logf("--- %d:\n%s", i, tmp) + t.Logf("--- %d:\n%s", i, str) } t.Fatalf( "%v: actual len %d != expected len %d", name, len(result), len(tc.expected)) } for i := range tc.expected { - bytes, err := result[i].AsYAML() + str, err := result[i].String() if err != nil { t.Fatalf("%v: result to YAML fails with err: %v", name, err) } - tmp := string(bytes) - if tmp != tc.expected[i] { + if str != tc.expected[i] { t.Fatalf( "%v: string mismatch in item %d\n"+ "actual:\n-----\n%s\n-----\n"+ "expected:\n-----\n%s\n-----\n", - name, i, tmp, tc.expected[i]) + name, i, str, tc.expected[i]) } } }) @@ -307,38 +306,32 @@ kind: List t.Fatal(err) } - tests := []struct { - name string + tests := map[string]struct { input []types.PatchStrategicMerge expectedOut []*Resource expectedErr bool }{ - { - name: "happy", + "happy": { input: []types.PatchStrategicMerge{patchGood1, patchGood2}, expectedOut: []*Resource{testDeployment, testConfigMap}, expectedErr: false, }, - { - name: "badFileName", + "badFileName": { input: []types.PatchStrategicMerge{patchGood1, "doesNotExist"}, expectedOut: []*Resource{}, expectedErr: true, }, - { - name: "badData", + "badData": { input: []types.PatchStrategicMerge{patchGood1, patchBad}, expectedOut: []*Resource{}, expectedErr: true, }, - { - name: "listOfPatches", + "listOfPatches": { input: []types.PatchStrategicMerge{patchList}, expectedOut: []*Resource{testDeployment, testConfigMap}, expectedErr: false, }, - { - name: "listWithAnchorReference", + "listWithAnchorReference": { input: []types.PatchStrategicMerge{patchList2}, expectedOut: []*Resource{testDeploymentA, testDeploymentB}, // The error using kyaml is: @@ -349,34 +342,340 @@ kind: List // TODO(#3271) This shouldn't have an error, but does when kyaml is used. expectedErr: true, }, - { - name: "listWithNoEntries", + "listWithNoEntries": { input: []types.PatchStrategicMerge{patchList3}, expectedOut: []*Resource{}, expectedErr: false, }, - { - name: "listWithNoItems", + "listWithNoItems": { input: []types.PatchStrategicMerge{patchList4}, expectedOut: []*Resource{}, expectedErr: false, }, } - for _, test := range tests { - rs, err := factory.SliceFromPatches(ldr, test.input) - if err != nil { - assert.True(t, test.expectedErr, - fmt.Sprintf("in test %s, got unexpected error: %v", test.name, err)) - continue - } - assert.False(t, test.expectedErr, "expected no error in "+test.name) - assert.Equal(t, len(test.expectedOut), len(rs)) - for i := range rs { - expYaml, err := test.expectedOut[i].AsYAML() - assert.NoError(t, err) - actYaml, err := rs[i].AsYAML() - assert.NoError(t, err) - assert.Equal(t, expYaml, actYaml) - } + for n, test := range tests { + t.Run(n, func(t *testing.T) { + rs, err := factory.SliceFromPatches(ldr, test.input) + if err != nil { + assert.True(t, test.expectedErr, + fmt.Sprintf("in test %s, got unexpected error: %v", n, err)) + return + } + assert.False(t, test.expectedErr, "expected no error in "+n) + assert.Equal(t, len(test.expectedOut), len(rs)) + for i := range rs { + expYaml, err := test.expectedOut[i].AsYAML() + assert.NoError(t, err) + actYaml, err := rs[i].AsYAML() + assert.NoError(t, err) + assert.Equal(t, expYaml, actYaml) + } + }) + } +} + +func TestHash(t *testing.T) { + input := ` +apiVersion: v1 +kind: ConfigMap +metadata: + name: foo +data: + one: "" +binaryData: + two: "" +` + expect := "698h7c7t9m" + k, err := factory.SliceFromBytes([]byte(input)) + if err != nil { + t.Fatal(err) + } + result, err := k[0].Hash(factory.Hasher()) + if err != nil { + t.Fatal(err) + } + if result != expect { + t.Fatalf("expect %s but got %s", expect, result) + } +} + +func TestSliceFromBytesMore(t *testing.T) { + testConfigMap := + map[string]interface{}{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": map[string]interface{}{ + "name": "winnie", + }, + } + testDeploymentSpec := map[string]interface{}{ + "template": map[string]interface{}{ + "spec": map[string]interface{}{ + "hostAliases": []interface{}{ + map[string]interface{}{ + "hostnames": []interface{}{ + "a.example.com", + }, + "ip": "8.8.8.8", + }, + }, + }, + }, + } + testDeploymentA := map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deployment-a", + }, + "spec": testDeploymentSpec, + } + testDeploymentB := map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deployment-b", + }, + "spec": testDeploymentSpec, + } + testDeploymentList := + map[string]interface{}{ + "apiVersion": "v1", + "kind": "DeploymentList", + "items": []interface{}{ + testDeploymentA, + testDeploymentB, + }, + } + + type expected struct { + out []map[string]interface{} + isErr bool + } + + testCases := map[string]struct { + input []byte + exp expected + }{ + "garbage": { + input: []byte("garbageIn: garbageOut"), + exp: expected{ + isErr: true, + }, + }, + "noBytes": { + input: []byte{}, + exp: expected{ + out: []map[string]interface{}{}, + }, + }, + "goodJson": { + input: []byte(` +{"apiVersion":"v1","kind":"ConfigMap","metadata":{"name":"winnie"}} +`), + exp: expected{ + out: []map[string]interface{}{testConfigMap}, + }, + }, + "goodYaml1": { + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +`), + exp: expected{ + out: []map[string]interface{}{testConfigMap}, + }, + }, + "goodYaml2": { + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +`), + exp: expected{ + out: []map[string]interface{}{testConfigMap, testConfigMap}, + }, + }, + "localConfigYaml": { + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie-skip + annotations: + # this annotation causes the Resource to be ignored by kustomize + config.kubernetes.io/local-config: "" +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +`), + exp: expected{ + out: []map[string]interface{}{testConfigMap}, + }, + }, + "garbageInOneOfTwoObjects": { + input: []byte(` +apiVersion: v1 +kind: ConfigMap +metadata: + name: winnie +--- +WOOOOOOOOOOOOOOOOOOOOOOOOT: woot +`), + exp: expected{ + isErr: true, + }, + }, + "emptyObjects": { + input: []byte(` +--- +#a comment + +--- + +`), + exp: expected{ + out: []map[string]interface{}{}, + }, + }, + "Missing .metadata.name in object": { + input: []byte(` +apiVersion: v1 +kind: Namespace +metadata: + annotations: + foo: bar +`), + exp: expected{ + isErr: true, + }, + }, + "nil value in list": { + input: []byte(` +apiVersion: builtin +kind: ConfigMapGenerator +metadata: + name: kube100-site + labels: + app: web +testList: +- testA +- +`), + exp: expected{ + isErr: true, + }, + }, + "List": { + input: []byte(` +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +`), + exp: expected{ + out: []map[string]interface{}{ + testConfigMap, + testConfigMap}, + }, + }, + "ConfigMapList": { + input: []byte(` +apiVersion: v1 +kind: ConfigMapList +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +`), + exp: expected{ + out: []map[string]interface{}{ + testConfigMap, + testConfigMap, + }, + }, + }, + "listWithAnchors": { + input: []byte(` +apiVersion: v1 +kind: DeploymentList +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-a + spec: &hostAliases + template: + spec: + hostAliases: + - hostnames: + - a.example.com + ip: 8.8.8.8 +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: deployment-b + spec: + <<: *hostAliases +`), + exp: expected{ + // TODO(3271): This should work. + // https://github.com/kubernetes-sigs/kustomize/issues/3271 + // json.Marshal(obj) fails on the 2nd list item. + // The value of the 1st list item's first spec field is + // map[string]interface{} + // The value of the 2nd list item's first spec field is + // map[interface{}]interface{} + // which causes a encoding/json.UnsupportedTypeError. + isErr: true, + out: []map[string]interface{}{testDeploymentList}, + }, + }, + } + + for n := range testCases { + tc := testCases[n] + t.Run(n, func(t *testing.T) { + rs, err := factory.RNodesFromBytes(tc.input) + if err != nil { + assert.True(t, tc.exp.isErr) + return + } + assert.False(t, tc.exp.isErr) + assert.Equal(t, len(tc.exp.out), len(rs)) + for i := range rs { + rsMap, err := rs[i].Map() + assert.NoError(t, err) + assert.Equal( + t, fmt.Sprintf("%v", tc.exp.out[i]), fmt.Sprintf("%v", rsMap)) + m, _ := rs[i].Map() + if !reflect.DeepEqual(tc.exp.out[i], m) { + t.Fatalf("%s:\nexpected: %v\n actual: %v", + n, tc.exp.out[i], m) + } + } + }) } } diff --git a/api/resource/resource.go b/api/resource/resource.go index 71aafd7a2..bd7da5706 100644 --- a/api/resource/resource.go +++ b/api/resource/resource.go @@ -12,7 +12,6 @@ import ( "sigs.k8s.io/kustomize/api/filters/patchstrategicmerge" "sigs.k8s.io/kustomize/api/ifc" - "sigs.k8s.io/kustomize/api/internal/wrappy" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/types" @@ -26,7 +25,7 @@ import ( // paired with metadata used by kustomize. // For more history, see sigs.k8s.io/kustomize/api/ifc.Unstructured type Resource struct { - kunStr ifc.Kunstructured + kunStr *kyaml.RNode options *types.GenArgs refBy []resid.ResId refVarNames []string @@ -55,23 +54,24 @@ var buildAnnotations = []string{ buildAnnotationAllowKindChange, } +func (r *Resource) AsRNode() *kyaml.RNode { + return r.kunStr.Copy() +} + func (r *Resource) ResetPrimaryData(incoming *Resource) { - r.kunStr = incoming.Copy() + r.kunStr = incoming.kunStr.Copy() } func (r *Resource) GetAnnotations() map[string]string { - annotations := r.kunStr.GetAnnotations() - if annotations == nil { + annotations, err := r.kunStr.GetAnnotations() + if err != nil || annotations == nil { return make(map[string]string) } return annotations } -func (r *Resource) Copy() ifc.Kunstructured { - return r.kunStr.Copy() -} - func (r *Resource) GetFieldValue(f string) (interface{}, error) { + //nolint:staticcheck return r.kunStr.GetFieldValue(f) } @@ -84,7 +84,16 @@ func (r *Resource) GetBinaryDataMap() map[string]string { } func (r *Resource) GetGvk() resid.Gvk { - return r.kunStr.GetGvk() + meta, err := r.kunStr.GetMeta() + if err != nil { + return resid.GvkFromString("") + } + g, v := resid.ParseGroupVersion(meta.APIVersion) + return resid.Gvk{Group: g, Version: v, Kind: meta.Kind} +} + +func (r *Resource) Hash(h ifc.KunstructuredHasher) (string, error) { + return h.Hash(r.kunStr) } func (r *Resource) GetKind() string { @@ -92,7 +101,11 @@ func (r *Resource) GetKind() string { } func (r *Resource) GetLabels() map[string]string { - return r.kunStr.GetLabels() + l, err := r.kunStr.GetLabels() + if err != nil { + return map[string]string{} + } + return l } func (r *Resource) GetName() string { @@ -100,16 +113,17 @@ func (r *Resource) GetName() string { } func (r *Resource) GetSlice(p string) ([]interface{}, error) { + //nolint:staticcheck return r.kunStr.GetSlice(p) } func (r *Resource) GetString(p string) (string, error) { + //nolint:staticcheck return r.kunStr.GetString(p) } -func (r *Resource) IsEmpty() (bool, error) { - m, err := r.kunStr.Map() - return len(m) == 0, err +func (r *Resource) IsEmpty() bool { + return r.kunStr.IsNilOrEmpty() } func (r *Resource) Map() (map[string]interface{}, error) { @@ -146,7 +160,10 @@ func (r *Resource) SetBinaryDataMap(m map[string]string) { } func (r *Resource) SetGvk(gvk resid.Gvk) { - r.kunStr.SetGvk(gvk) + r.kunStr.SetMapField( + kyaml.NewScalarRNode(gvk.Kind), kyaml.KindField) + r.kunStr.SetMapField( + kyaml.NewScalarRNode(gvk.ApiVersion()), kyaml.APIVersionField) } func (r *Resource) SetLabels(m map[string]string) { @@ -193,7 +210,7 @@ type ResCtxMatcher func(ResCtx) bool // DeepCopy returns a new copy of resource func (r *Resource) DeepCopy() *Resource { rc := &Resource{ - kunStr: r.Copy(), + kunStr: r.kunStr.Copy(), } rc.copyOtherFields(r) return rc @@ -541,11 +558,7 @@ func (r *Resource) ApplySmPatch(patch *Resource) error { if err != nil { return err } - empty, err := r.IsEmpty() - if err != nil { - return err - } - if empty { + if r.IsEmpty() { return nil } if !patch.KindChangeAllowed() { @@ -559,15 +572,12 @@ func (r *Resource) ApplySmPatch(patch *Resource) error { } func (r *Resource) ApplyFilter(f kio.Filter) error { - if wn, ok := r.kunStr.(*wrappy.WNode); ok { - l, err := f.Filter([]*kyaml.RNode{wn.AsRNode()}) - if len(l) == 0 { - // Hack to deal with deletion. - r.kunStr = wrappy.NewWNode() - } - return err + l, err := f.Filter([]*kyaml.RNode{r.kunStr}) + if len(l) == 0 { + // The node was deleted. The following makes r.IsEmpty() true. + r.kunStr = nil } - return filtersutil.ApplyToJSON(f, r) + return err } func mergeStringMaps(maps ...map[string]string) map[string]string { diff --git a/api/resource/resource_test.go b/api/resource/resource_test.go index 660cc6245..a946859ae 100644 --- a/api/resource/resource_test.go +++ b/api/resource/resource_test.go @@ -695,7 +695,7 @@ spec: } } -func TestResource_StorePreviousId(t *testing.T) { +func TestResourceStorePreviousId(t *testing.T) { tests := map[string]struct { input string newName string @@ -1079,3 +1079,54 @@ func TestSameEndingSubarray(t *testing.T) { }) } } + +func TestGetGvk(t *testing.T) { + r, err := factory.FromBytes([]byte(` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: clown +spec: + numReplicas: 1 +`)) + assert.NoError(t, err) + + gvk := r.GetGvk() + expected := "apps" + actual := gvk.Group + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + expected = "v1" + actual = gvk.Version + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + expected = "Deployment" + actual = gvk.Kind + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} +func TestSetGvk(t *testing.T) { + r, err := factory.FromBytes([]byte(` +apiVersion: v1 +kind: Deployment +metadata: + name: clown +spec: + numReplicas: 1 +`)) + assert.NoError(t, err) + r.SetGvk(resid.GvkFromString("grp_ver_knd")) + gvk := r.GetGvk() + if expected, actual := "grp", gvk.Group; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "ver", gvk.Version; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "knd", gvk.Kind; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} diff --git a/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go b/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go index ffacc9ab2..9c4af520b 100644 --- a/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go +++ b/cmd/pluginator/internal/krmfunction/funcwrappersrc/main.go @@ -46,10 +46,7 @@ func main() { } } - resourceList.Items, err = resMap.ToRNodeSlice() - if err != nil { - return err - } + resourceList.Items = resMap.ToRNodeSlice() return nil }) if err := framework.Execute(&processor, nil); err != nil { diff --git a/kustomize/commands/commands.go b/kustomize/commands/commands.go index c4082ff9b..ab91aa355 100644 --- a/kustomize/commands/commands.go +++ b/kustomize/commands/commands.go @@ -49,8 +49,8 @@ See https://sigs.k8s.io/kustomize completion.NewCommand(), makeBuildCommand(fSys, stdOut), edit.NewCmdEdit( - fSys, pvd.GetFieldValidator(), pvd.GetKunstructuredFactory()), - create.NewCmdCreate(fSys, pvd.GetKunstructuredFactory()), + fSys, pvd.GetFieldValidator(), pvd.GetResourceFactory()), + create.NewCmdCreate(fSys, pvd.GetResourceFactory()), version.NewCmdVersion(stdOut), openapi.NewCmdOpenAPI(stdOut), ) diff --git a/kustomize/commands/create/create.go b/kustomize/commands/create/create.go index 2a6969e0b..83bffbafb 100644 --- a/kustomize/commands/create/create.go +++ b/kustomize/commands/create/create.go @@ -11,9 +11,9 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kustomize/api/filesys" - "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/loader" + "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/util" ) @@ -31,7 +31,7 @@ type createFlags struct { } // NewCmdCreate returns an instance of 'create' subcommand. -func NewCmdCreate(fSys filesys.FileSystem, uf ifc.KunstructuredFactory) *cobra.Command { +func NewCmdCreate(fSys filesys.FileSystem, rf *resource.Factory) *cobra.Command { opts := createFlags{path: filesys.SelfDir} c := &cobra.Command{ Use: "create", @@ -49,7 +49,7 @@ func NewCmdCreate(fSys filesys.FileSystem, uf ifc.KunstructuredFactory) *cobra.C kustomize create --resources deployment.yaml,service.yaml,../base --namespace staging --nameprefix acme- `, RunE: func(cmd *cobra.Command, args []string) error { - return runCreate(opts, fSys, uf) + return runCreate(opts, fSys, rf) }, } c.Flags().StringVar( @@ -95,7 +95,7 @@ func NewCmdCreate(fSys filesys.FileSystem, uf ifc.KunstructuredFactory) *cobra.C return c } -func runCreate(opts createFlags, fSys filesys.FileSystem, uf ifc.KunstructuredFactory) error { +func runCreate(opts createFlags, fSys filesys.FileSystem, rf *resource.Factory) error { var resources []string var err error if opts.resources != "" { @@ -108,7 +108,7 @@ func runCreate(opts createFlags, fSys filesys.FileSystem, uf ifc.KunstructuredFa return fmt.Errorf("kustomization file already exists") } if opts.detectResources { - detected, err := detectResources(fSys, uf, opts.path, opts.detectRecursive) + detected, err := detectResources(fSys, rf, opts.path, opts.detectRecursive) if err != nil { return err } @@ -149,7 +149,7 @@ func runCreate(opts createFlags, fSys filesys.FileSystem, uf ifc.KunstructuredFa return mf.Write(m) } -func detectResources(fSys filesys.FileSystem, uf ifc.KunstructuredFactory, base string, recursive bool) ([]string, error) { +func detectResources(fSys filesys.FileSystem, rf *resource.Factory, base string, recursive bool) ([]string, error) { var paths []string err := fSys.Walk(base, func(path string, info os.FileInfo, err error) error { if err != nil { @@ -176,7 +176,7 @@ func detectResources(fSys filesys.FileSystem, uf ifc.KunstructuredFactory, base if err != nil { return err } - if _, err := uf.SliceFromBytes(fContents); err != nil { + if _, err := rf.SliceFromBytes(fContents); err != nil { return nil } paths = append(paths, path) diff --git a/kustomize/commands/create/create_test.go b/kustomize/commands/create/create_test.go index 44af5592e..728125e1b 100644 --- a/kustomize/commands/create/create_test.go +++ b/kustomize/commands/create/create_test.go @@ -13,7 +13,7 @@ import ( "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" ) -var factory = provider.NewDefaultDepProvider().GetKunstructuredFactory() +var factory = provider.NewDefaultDepProvider().GetResourceFactory() func readKustomizationFS(t *testing.T, fSys filesys.FileSystem) *types.Kustomization { kf, err := kustfile.NewKustomizationFile(fSys) diff --git a/kustomize/commands/edit/add/all.go b/kustomize/commands/edit/add/all.go index 1e34f7160..89a55e850 100644 --- a/kustomize/commands/edit/add/all.go +++ b/kustomize/commands/edit/add/all.go @@ -7,13 +7,14 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/api/resource" ) // NewCmdAdd returns an instance of 'add' subcommand. func NewCmdAdd( fSys filesys.FileSystem, ldr ifc.KvLoader, - kf ifc.KunstructuredFactory) *cobra.Command { + rf *resource.Factory) *cobra.Command { c := &cobra.Command{ Use: "add", Short: "Adds an item to the kustomization file.", @@ -53,8 +54,8 @@ func NewCmdAdd( newCmdAddResource(fSys), newCmdAddPatch(fSys), newCmdAddComponent(fSys), - newCmdAddSecret(fSys, ldr, kf), - newCmdAddConfigMap(fSys, ldr, kf), + newCmdAddSecret(fSys, ldr, rf), + newCmdAddConfigMap(fSys, ldr, rf), newCmdAddBase(fSys), newCmdAddLabel(fSys, ldr.Validator().MakeLabelValidator()), newCmdAddAnnotation(fSys, ldr.Validator().MakeAnnotationValidator()), diff --git a/kustomize/commands/edit/add/configmap.go b/kustomize/commands/edit/add/configmap.go index fa539ccbe..f8c7a6b8f 100644 --- a/kustomize/commands/edit/add/configmap.go +++ b/kustomize/commands/edit/add/configmap.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" ) @@ -15,7 +16,7 @@ import ( func newCmdAddConfigMap( fSys filesys.FileSystem, ldr ifc.KvLoader, - kf ifc.KunstructuredFactory) *cobra.Command { + rf *resource.Factory) *cobra.Command { var flags flagsAndArgs cmd := &cobra.Command{ Use: "configmap NAME [--behavior={create|merge|replace}] [--from-file=[key=]source] [--from-literal=key1=value1]", @@ -57,7 +58,7 @@ func newCmdAddConfigMap( } // Add the flagsAndArgs map to the kustomization file. - err = addConfigMap(ldr, kustomization, flags, kf) + err = addConfigMap(ldr, kustomization, flags, rf) if err != nil { return err } @@ -106,13 +107,13 @@ func newCmdAddConfigMap( func addConfigMap( ldr ifc.KvLoader, k *types.Kustomization, - flags flagsAndArgs, kf ifc.KunstructuredFactory) error { + flags flagsAndArgs, rf *resource.Factory) error { args := findOrMakeConfigMapArgs(k, flags.Name) mergeFlagsIntoCmArgs(args, flags) // Validate by trying to create corev1.configmap. args.Options = types.MergeGlobalOptionsIntoLocal( args.Options, k.GeneratorOptions) - _, err := kf.MakeConfigMap(ldr, args) + _, err := rf.MakeConfigMap(ldr, args) return err } diff --git a/kustomize/commands/edit/add/secret.go b/kustomize/commands/edit/add/secret.go index 1e4b41a5d..7c24193f2 100644 --- a/kustomize/commands/edit/add/secret.go +++ b/kustomize/commands/edit/add/secret.go @@ -7,6 +7,7 @@ import ( "github.com/spf13/cobra" "sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" ) @@ -15,7 +16,7 @@ import ( func newCmdAddSecret( fSys filesys.FileSystem, ldr ifc.KvLoader, - kf ifc.KunstructuredFactory) *cobra.Command { + rf *resource.Factory) *cobra.Command { var flags flagsAndArgs cmd := &cobra.Command{ Use: "secret NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--type=Opaque|kubernetes.io/tls]", @@ -54,7 +55,7 @@ func newCmdAddSecret( } // Add the flagsAndArgs map to the kustomization file. - err = addSecret(ldr, kustomization, flags, kf) + err = addSecret(ldr, kustomization, flags, rf) if err != nil { return err } @@ -106,13 +107,13 @@ func newCmdAddSecret( func addSecret( ldr ifc.KvLoader, k *types.Kustomization, - flags flagsAndArgs, kf ifc.KunstructuredFactory) error { + flags flagsAndArgs, rf *resource.Factory) error { args := findOrMakeSecretArgs(k, flags.Name, flags.Namespace, flags.Type) mergeFlagsIntoGeneratorArgs(&args.GeneratorArgs, flags) // Validate by trying to create corev1.secret. args.Options = types.MergeGlobalOptionsIntoLocal( args.Options, k.GeneratorOptions) - _, err := kf.MakeSecret(ldr, args) + _, err := rf.MakeSecret(ldr, args) return err } diff --git a/kustomize/commands/edit/all.go b/kustomize/commands/edit/all.go index 2f90a9272..8709a5775 100644 --- a/kustomize/commands/edit/all.go +++ b/kustomize/commands/edit/all.go @@ -9,6 +9,7 @@ import ( "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/kv" "sigs.k8s.io/kustomize/api/loader" + "sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/kustomize/v4/commands/edit/add" "sigs.k8s.io/kustomize/kustomize/v4/commands/edit/fix" "sigs.k8s.io/kustomize/kustomize/v4/commands/edit/listbuiltin" @@ -18,7 +19,7 @@ import ( // NewCmdEdit returns an instance of 'edit' subcommand. func NewCmdEdit( - fSys filesys.FileSystem, v ifc.Validator, kf ifc.KunstructuredFactory) *cobra.Command { + fSys filesys.FileSystem, v ifc.Validator, rf *resource.Factory) *cobra.Command { c := &cobra.Command{ Use: "edit", Short: "Edits a kustomization file", @@ -40,7 +41,7 @@ func NewCmdEdit( add.NewCmdAdd( fSys, kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v), - kf), + rf), set.NewCmdSet( fSys, kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v), diff --git a/kyaml/yaml/rnode.go b/kyaml/yaml/rnode.go index 22e5d326e..9f6ed3984 100644 --- a/kyaml/yaml/rnode.go +++ b/kyaml/yaml/rnode.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "log" + "regexp" "strconv" "strings" @@ -336,6 +337,33 @@ func (rn *RNode) SetYNode(node *yaml.Node) { *rn.value = *node } +// GetKind returns the kind. +func (rn *RNode) GetKind() string { + node, err := rn.Pipe(FieldMatcher{Name: KindField}) + if err != nil { + return "" + } + return GetValue(node) +} + +// GetName returns the name. +func (rn *RNode) GetName() string { + f := rn.Field(MetadataField) + if f.IsNilOrEmpty() { + return "" + } + f = f.Value.Field(NameField) + if f.IsNilOrEmpty() { + return "" + } + return f.Value.YNode().Value +} + +// SetName sets the metadata name field. +func (rn *RNode) SetName(name string) error { + return rn.SetMapField(NewScalarRNode(name), MetadataField, NameField) +} + // GetNamespace gets the metadata namespace field. func (rn *RNode) GetNamespace() (string, error) { meta, err := rn.GetMeta() @@ -752,7 +780,7 @@ func (rn *RNode) GetValidatedMetadata() (ResourceMeta, error) { return m, nil } -// MatchesAnnotationSelector implements ifc.Kunstructured. +// MatchesAnnotationSelector returns true on a selector match to annotations. func (rn *RNode) MatchesAnnotationSelector(selector string) (bool, error) { s, err := labels.Parse(selector) if err != nil { @@ -765,7 +793,7 @@ func (rn *RNode) MatchesAnnotationSelector(selector string) (bool, error) { return s.Matches(labels.Set(slice)), nil } -// MatchesLabelSelector implements ifc.Kunstructured. +// MatchesLabelSelector returns true on a selector match to labels. func (rn *RNode) MatchesLabelSelector(selector string) (bool, error) { s, err := labels.Parse(selector) if err != nil { @@ -859,3 +887,123 @@ func checkKey(key string, elems []*Node) bool { } return count == len(elems) } + +// Deprecated: use pipes instead. +// GetSlice returns the contents of the slice field at the given path. +func (rn *RNode) GetSlice(path string) ([]interface{}, error) { + value, err := rn.GetFieldValue(path) + if err != nil { + return nil, err + } + if sliceValue, ok := value.([]interface{}); ok { + return sliceValue, nil + } + return nil, fmt.Errorf("node %s is not a slice", path) +} + +// Deprecated: use pipes instead. +// GetString returns the contents of the string field at the given path. +func (rn *RNode) GetString(path string) (string, error) { + value, err := rn.GetFieldValue(path) + if err != nil { + return "", err + } + if v, ok := value.(string); ok { + return v, nil + } + return "", fmt.Errorf("node %s is not a string: %v", path, value) +} + +// Deprecated: use slash paths instead. +// GetFieldValue finds period delimited fields. +// TODO: When doing kustomize var replacement, which is likely a +// a primary use of this function and the reason it returns interface{} +// rather than string, we do conversion from Nodes to Go types and back +// to nodes. We should figure out how to do replacement using raw nodes, +// assuming we keep the var feature in kustomize. +// The other end of this is: refvar.go:updateNodeValue. +func (rn *RNode) GetFieldValue(path string) (interface{}, error) { + fields := convertSliceIndex(strings.Split(path, ".")) + rn, err := rn.Pipe(Lookup(fields...)) + if err != nil { + return nil, err + } + if rn == nil { + return nil, NoFieldError{path} + } + yn := rn.YNode() + + // If this is an alias node, resolve it + if yn.Kind == yaml.AliasNode { + yn = yn.Alias + } + + // Return value as map for DocumentNode and MappingNode kinds + if yn.Kind == yaml.DocumentNode || yn.Kind == yaml.MappingNode { + var result map[string]interface{} + if err := yn.Decode(&result); err != nil { + return nil, err + } + return result, err + } + + // Return value as slice for SequenceNode kind + if yn.Kind == yaml.SequenceNode { + var result []interface{} + if err := yn.Decode(&result); err != nil { + return nil, err + } + return result, nil + } + if yn.Kind != yaml.ScalarNode { + return nil, fmt.Errorf("expected ScalarNode, got Kind=%d", yn.Kind) + } + + switch yn.Tag { + case NodeTagString: + return yn.Value, nil + case NodeTagInt: + return strconv.Atoi(yn.Value) + case NodeTagFloat: + return strconv.ParseFloat(yn.Value, 64) + case NodeTagBool: + return strconv.ParseBool(yn.Value) + default: + // Possibly this should be an error or log. + return yn.Value, nil + } +} + +// convertSliceIndex traverses the items in `fields` and find +// if there is a slice index in the item and change it to a +// valid Lookup field path. For example, 'ports[0]' will be +// converted to 'ports' and '0'. +func convertSliceIndex(fields []string) []string { + var res []string + for _, s := range fields { + if !strings.HasSuffix(s, "]") { + res = append(res, s) + continue + } + re := regexp.MustCompile(`^(.*)\[(\d+)\]$`) + groups := re.FindStringSubmatch(s) + if len(groups) == 0 { + // no match, add to result + res = append(res, s) + continue + } + if groups[1] != "" { + res = append(res, groups[1]) + } + res = append(res, groups[2]) + } + return res +} + +type NoFieldError struct { + Field string +} + +func (e NoFieldError) Error() string { + return fmt.Sprintf("no field named '%s'", e.Field) +} diff --git a/kyaml/yaml/rnode_test.go b/kyaml/yaml/rnode_test.go index c14e61939..1562eafc9 100644 --- a/kyaml/yaml/rnode_test.go +++ b/kyaml/yaml/rnode_test.go @@ -10,6 +10,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v3" ) func TestRNodeHasNilEntryInList(t *testing.T) { @@ -1130,3 +1131,627 @@ func TestRNodeMatchesLabelSelector(t *testing.T) { } } } + +const ( + deploymentLittleJson = `{"apiVersion":"apps/v1","kind":"Deployment",` + + `"metadata":{"name":"homer","namespace":"simpsons"}}` + + deploymentBiggerJson = ` +{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": { + "name": "homer", + "namespace": "simpsons", + "labels": { + "fruit": "apple", + "veggie": "carrot" + }, + "annotations": { + "area": "51", + "greeting": "Take me to your leader." + } + }, + "spec": { + "template": { + "spec": { + "containers": [ + { + "env": [ + { + "name": "CM_FOO", + "valueFrom": { + "configMapKeyRef": { + "key": "somekey", + "name": "myCm" + } + } + }, + { + "name": "SECRET_FOO", + "valueFrom": { + "secretKeyRef": { + "key": "someKey", + "name": "mySecret" + } + } + } + ], + "image": "nginx:1.7.9", + "name": "nginx" + } + ] + } + } + } +} +` + bigMapYaml = `Kind: Service +complextree: + - field1: + - boolfield: true + floatsubfield: 1.01 + intsubfield: 1010 + stringsubfield: idx1010 + - boolfield: false + floatsubfield: 1.011 + intsubfield: 1011 + stringsubfield: idx1011 + field2: + - boolfield: true + floatsubfield: 1.02 + intsubfield: 1020 + stringsubfield: idx1020 + - boolfield: false + floatsubfield: 1.021 + intsubfield: 1021 + stringsubfield: idx1021 + - field1: + - boolfield: true + floatsubfield: 1.11 + intsubfield: 1110 + stringsubfield: idx1110 + - boolfield: false + floatsubfield: 1.111 + intsubfield: 1111 + stringsubfield: idx1111 + field2: + - boolfield: true + floatsubfield: 1.112 + intsubfield: 1120 + stringsubfield: idx1120 + - boolfield: false + floatsubfield: 1.1121 + intsubfield: 1121 + stringsubfield: idx1121 +metadata: + labels: + app: application-name + name: service-name +spec: + ports: + port: 80 +that: + - idx0 + - idx1 + - idx2 + - idx3 +these: + - field1: + - idx010 + - idx011 + field2: + - idx020 + - idx021 + - field1: + - idx110 + - idx111 + field2: + - idx120 + - idx121 + - field1: + - idx210 + - idx211 + field2: + - idx220 + - idx221 +this: + is: + aBool: true + aFloat: 1.001 + aNilValue: null + aNumber: 1000 + anEmptyMap: {} + anEmptySlice: [] +those: + - field1: idx0foo + field2: idx0bar + - field1: idx1foo + field2: idx1bar + - field1: idx2foo + field2: idx2bar +` +) + +func TestGetFieldValueReturnsMap(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := map[string]interface{}{ + "fruit": "apple", + "veggie": "carrot", + } + actual, err := rn.GetFieldValue("metadata.labels") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual map does not deep equal expected map:\n%v", diff) + } +} + +func TestGetFieldValueReturnsStuff(t *testing.T) { + wn := NewRNode(nil) + if err := wn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := []interface{}{ + map[string]interface{}{ + "env": []interface{}{ + map[string]interface{}{ + "name": "CM_FOO", + "valueFrom": map[string]interface{}{ + "configMapKeyRef": map[string]interface{}{ + "key": "somekey", + "name": "myCm", + }, + }, + }, + map[string]interface{}{ + "name": "SECRET_FOO", + "valueFrom": map[string]interface{}{ + "secretKeyRef": map[string]interface{}{ + "key": "someKey", + "name": "mySecret", + }, + }, + }, + }, + "image": string("nginx:1.7.9"), + "name": string("nginx"), + }, + } + actual, err := wn.GetFieldValue("spec.template.spec.containers") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual map does not deep equal expected map:\n%v", diff) + } + // Cannot go deeper yet. + _, err = wn.GetFieldValue("spec.template.spec.containers.env") + if err == nil { + t.Fatalf("expected err %v", err) + } +} + +func makeBigMap() map[string]interface{} { + return map[string]interface{}{ + "Kind": "Service", + "metadata": map[string]interface{}{ + "labels": map[string]interface{}{ + "app": "application-name", + }, + "name": "service-name", + }, + "spec": map[string]interface{}{ + "ports": map[string]interface{}{ + "port": int64(80), + }, + }, + "this": map[string]interface{}{ + "is": map[string]interface{}{ + "aNumber": int64(1000), + "aFloat": float64(1.001), + "aNilValue": nil, + "aBool": true, + "anEmptyMap": map[string]interface{}{}, + "anEmptySlice": []interface{}{}, + /* + TODO: test for unrecognizable (e.g. a function) + "unrecognizable": testing.InternalExample{ + Name: "fooBar", + }, + */ + }, + }, + "that": []interface{}{ + "idx0", + "idx1", + "idx2", + "idx3", + }, + "those": []interface{}{ + map[string]interface{}{ + "field1": "idx0foo", + "field2": "idx0bar", + }, + map[string]interface{}{ + "field1": "idx1foo", + "field2": "idx1bar", + }, + map[string]interface{}{ + "field1": "idx2foo", + "field2": "idx2bar", + }, + }, + "these": []interface{}{ + map[string]interface{}{ + "field1": []interface{}{"idx010", "idx011"}, + "field2": []interface{}{"idx020", "idx021"}, + }, + map[string]interface{}{ + "field1": []interface{}{"idx110", "idx111"}, + "field2": []interface{}{"idx120", "idx121"}, + }, + map[string]interface{}{ + "field1": []interface{}{"idx210", "idx211"}, + "field2": []interface{}{"idx220", "idx221"}, + }, + }, + "complextree": []interface{}{ + map[string]interface{}{ + "field1": []interface{}{ + map[string]interface{}{ + "stringsubfield": "idx1010", + "intsubfield": int64(1010), + "floatsubfield": float64(1.010), + "boolfield": true, + }, + map[string]interface{}{ + "stringsubfield": "idx1011", + "intsubfield": int64(1011), + "floatsubfield": float64(1.011), + "boolfield": false, + }, + }, + "field2": []interface{}{ + map[string]interface{}{ + "stringsubfield": "idx1020", + "intsubfield": int64(1020), + "floatsubfield": float64(1.020), + "boolfield": true, + }, + map[string]interface{}{ + "stringsubfield": "idx1021", + "intsubfield": int64(1021), + "floatsubfield": float64(1.021), + "boolfield": false, + }, + }, + }, + map[string]interface{}{ + "field1": []interface{}{ + map[string]interface{}{ + "stringsubfield": "idx1110", + "intsubfield": int64(1110), + "floatsubfield": float64(1.110), + "boolfield": true, + }, + map[string]interface{}{ + "stringsubfield": "idx1111", + "intsubfield": int64(1111), + "floatsubfield": float64(1.111), + "boolfield": false, + }, + }, + "field2": []interface{}{ + map[string]interface{}{ + "stringsubfield": "idx1120", + "intsubfield": int64(1120), + "floatsubfield": float64(1.1120), + "boolfield": true, + }, + map[string]interface{}{ + "stringsubfield": "idx1121", + "intsubfield": int64(1121), + "floatsubfield": float64(1.1121), + "boolfield": false, + }, + }, + }, + }, + } +} +func TestBasicYamlOperationFromMap(t *testing.T) { + bytes, err := Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + if string(bytes) != bigMapYaml { + t.Fatalf("unexpected string equality") + } + rNode, err := Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rNodeString := rNode.MustString() + // The result from MustString has more indentation + // than bigMapYaml. + rNodeStrings := strings.Split(rNodeString, "\n") + bigMapStrings := strings.Split(bigMapYaml, "\n") + if len(rNodeStrings) != len(bigMapStrings) { + t.Fatalf("line count mismatch") + } + for i := range rNodeStrings { + s1 := strings.TrimSpace(rNodeStrings[i]) + s2 := strings.TrimSpace(bigMapStrings[i]) + if s1 != s2 { + t.Fatalf("expected '%s'=='%s'", s1, s2) + } + } +} + +func TestGetFieldValueReturnsSlice(t *testing.T) { + bytes, err := yaml.Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rNode, err := Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} + actual, err := rNode.GetFieldValue("that") + if err != nil { + t.Fatalf("error getting slice: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) + } +} + +func TestGetFieldValueReturnsSliceOfMappings(t *testing.T) { + bytes, err := yaml.Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rn, err := Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + expected := []interface{}{ + map[string]interface{}{ + "field1": "idx0foo", + "field2": "idx0bar", + }, + map[string]interface{}{ + "field1": "idx1foo", + "field2": "idx1bar", + }, + map[string]interface{}{ + "field1": "idx2foo", + "field2": "idx2bar", + }, + } + actual, err := rn.GetFieldValue("those") + if err != nil { + t.Fatalf("error getting slice: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) + } +} + +func TestGetFieldValueReturnsString(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + actual, err := rn.GetFieldValue("metadata.labels.fruit") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + v, ok := actual.(string) + if !ok || v != "apple" { + t.Fatalf("unexpected value '%v'", actual) + } +} + +func TestGetFieldValueResolvesAlias(t *testing.T) { + yamlWithAlias := ` +foo: &a theValue +bar: *a +` + rn, err := Parse(yamlWithAlias) + if err != nil { + t.Fatalf("unexpected yaml parse error: %v", err) + } + actual, err := rn.GetFieldValue("bar") + if err != nil { + t.Fatalf("error getting field value: %v", err) + } + v, ok := actual.(string) + if !ok || v != "theValue" { + t.Fatalf("unexpected value '%v'", actual) + } +} + +func TestGetString(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := "carrot" + actual, err := rn.GetString("metadata.labels.veggie") + if err != nil { + t.Fatalf("error getting string: %v", err) + } + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestGetSlice(t *testing.T) { + bytes, err := yaml.Marshal(makeBigMap()) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + rn, err := Parse(string(bytes)) + if err != nil { + t.Fatalf("unexpected yaml.Marshal err: %v", err) + } + expected := []interface{}{"idx0", "idx1", "idx2", "idx3"} + actual, err := rn.GetSlice("that") + if err != nil { + t.Fatalf("error getting slice: %v", err) + } + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual slice does not deep equal expected slice:\n%v", diff) + } +} + +func TestRoundTripJSON(t *testing.T) { + rn := NewRNode(nil) + err := rn.UnmarshalJSON([]byte(deploymentLittleJson)) + if err != nil { + t.Fatalf("unexpected UnmarshalJSON err: %v", err) + } + data, err := rn.MarshalJSON() + if err != nil { + t.Fatalf("unexpected MarshalJSON err: %v", err) + } + actual := string(data) + if actual != deploymentLittleJson { + t.Fatalf("expected %s, got %s", deploymentLittleJson, actual) + } +} + +func TestGettingFields(t *testing.T) { + rn := NewRNode(nil) + err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)) + if err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + expected := "Deployment" + actual := rn.GetKind() + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + expected = "homer" + actual = rn.GetName() + if expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + actualMap, err := rn.GetLabels() + if err != nil { + t.Fatalf("unexpected err '%v'", err) + } + v, ok := actualMap["fruit"] + if !ok || v != "apple" { + t.Fatalf("unexpected labels '%v'", actualMap) + } + actualMap, err = rn.GetAnnotations() + if err != nil { + t.Fatalf("unexpected err '%v'", err) + } + v, ok = actualMap["greeting"] + if !ok || v != "Take me to your leader." { + t.Fatalf("unexpected annotations '%v'", actualMap) + } +} + +func TestMapEmpty(t *testing.T) { + newNodeMap, err := NewRNode(nil).Map() + assert.NoError(t, err) + assert.Equal(t, 0, len(newNodeMap)) +} + +func TestMap(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentLittleJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + + expected := map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "homer", + "namespace": "simpsons", + }, + } + + actual, err := rn.Map() + assert.NoError(t, err) + if diff := cmp.Diff(expected, actual); diff != "" { + t.Fatalf("actual map does not deep equal expected map:\n%v", diff) + } +} + +func TestSetName(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + rn.SetName("marge") + if expected, actual := "marge", rn.GetName(); expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestSetNamespace(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + rn.SetNamespace("flanders") + meta, _ := rn.GetMeta() + if expected, actual := "flanders", meta.Namespace; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} +func TestSetLabels(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + rn.SetLabels(map[string]string{ + "label1": "foo", + "label2": "bar", + }) + labels, err := rn.GetLabels() + assert.NoError(t, err) + if expected, actual := 2, len(labels); expected != actual { + t.Fatalf("expected '%d', got '%d'", expected, actual) + } + if expected, actual := "foo", labels["label1"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "bar", labels["label2"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} + +func TestGetAnnotations(t *testing.T) { + rn := NewRNode(nil) + if err := rn.UnmarshalJSON([]byte(deploymentBiggerJson)); err != nil { + t.Fatalf("unexpected unmarshaljson err: %v", err) + } + rn.SetAnnotations(map[string]string{ + "annotation1": "foo", + "annotation2": "bar", + }) + annotations, err := rn.GetAnnotations() + assert.NoError(t, err) + if expected, actual := 2, len(annotations); expected != actual { + t.Fatalf("expected '%d', got '%d'", expected, actual) + } + if expected, actual := "foo", annotations["annotation1"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } + if expected, actual := "bar", annotations["annotation2"]; expected != actual { + t.Fatalf("expected '%s', got '%s'", expected, actual) + } +} diff --git a/plugin/builtin/hashtransformer/HashTransformer.go b/plugin/builtin/hashtransformer/HashTransformer.go index 1b86f541a..777c5fefa 100644 --- a/plugin/builtin/hashtransformer/HashTransformer.go +++ b/plugin/builtin/hashtransformer/HashTransformer.go @@ -28,7 +28,7 @@ func (p *plugin) Config( func (p *plugin) Transform(m resmap.ResMap) error { for _, res := range m.Resources() { if res.NeedHashSuffix() { - h, err := p.hasher.Hash(res) + h, err := res.Hash(p.hasher) if err != nil { return err } diff --git a/plugin/builtin/namespacetransformer/NamespaceTransformer.go b/plugin/builtin/namespacetransformer/NamespaceTransformer.go index 6d42db29d..8fda2d640 100644 --- a/plugin/builtin/namespacetransformer/NamespaceTransformer.go +++ b/plugin/builtin/namespacetransformer/NamespaceTransformer.go @@ -34,20 +34,15 @@ func (p *plugin) Transform(m resmap.ResMap) error { return nil } for _, r := range m.Resources() { - empty, err := r.IsEmpty() - if err != nil { - return err - } - if empty { + if r.IsEmpty() { // Don't mutate empty objects? continue } r.StorePreviousId() - err = r.ApplyFilter(namespace.Filter{ + if err := r.ApplyFilter(namespace.Filter{ Namespace: p.Namespace, FsSlice: p.FieldSpecs, - }) - if err != nil { + }); err != nil { return err } matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals)