Merge pull request #3721 from monopole/removeWrappyLayer

Remove the wrappy package in service of 3588
This commit is contained in:
Kubernetes Prow Robot
2021-03-15 16:54:34 -07:00
committed by GitHub
39 changed files with 1601 additions and 2002 deletions

View File

@@ -11,7 +11,7 @@ import (
) )
type HashTransformerPlugin struct { type HashTransformerPlugin struct {
hasher ifc.KunstructuredHasher hasher ifc.KustHasher
} }
func (p *HashTransformerPlugin) Config( func (p *HashTransformerPlugin) Config(
@@ -24,7 +24,7 @@ func (p *HashTransformerPlugin) Config(
func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error { func (p *HashTransformerPlugin) Transform(m resmap.ResMap) error {
for _, res := range m.Resources() { for _, res := range m.Resources() {
if res.NeedHashSuffix() { if res.NeedHashSuffix() {
h, err := p.hasher.Hash(res) h, err := res.Hash(p.hasher)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -30,20 +30,15 @@ func (p *NamespaceTransformerPlugin) Transform(m resmap.ResMap) error {
return nil return nil
} }
for _, r := range m.Resources() { for _, r := range m.Resources() {
empty, err := r.IsEmpty() if r.IsEmpty() {
if err != nil {
return err
}
if empty {
// Don't mutate empty objects? // Don't mutate empty objects?
continue continue
} }
r.StorePreviousId() r.StorePreviousId()
err = r.ApplyFilter(namespace.Filter{ if err := r.ApplyFilter(namespace.Filter{
Namespace: p.Namespace, Namespace: p.Namespace,
FsSlice: p.FieldSpecs, FsSlice: p.FieldSpecs,
}) }); err != nil {
if err != nil {
return err return err
} }
matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals) matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals)

View File

@@ -118,7 +118,6 @@ func (ns Filter) roleBindingHack(obj *yaml.RNode, meta yaml.ResourceMeta) error
// add the namespace to each "subject" with name: default // add the namespace to each "subject" with name: default
err = obj.VisitElements(func(o *yaml.RNode) error { err = obj.VisitElements(func(o *yaml.RNode) error {
// copied from kunstruct based kustomize NamespaceTransformer plugin
// The only case we need to force the namespace // The only case we need to force the namespace
// if for the "service account". "default" is // if for the "service account". "default" is
// kind of hardcoded here for right now. // kind of hardcoded here for right now.

View File

@@ -6,13 +6,11 @@ require (
github.com/evanphx/json-patch v4.5.0+incompatible github.com/evanphx/json-patch v4.5.0+incompatible
github.com/go-errors/errors v1.0.1 github.com/go-errors/errors v1.0.1
github.com/go-openapi/spec v0.19.5 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/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/imdario/mergo v0.3.5 github.com/imdario/mergo v0.3.5
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/stretchr/testify v1.4.0 github.com/stretchr/testify v1.4.0
gopkg.in/yaml.v2 v2.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/kustomize/kyaml v0.10.15
sigs.k8s.io/yaml v1.2.0 sigs.k8s.io/yaml v1.2.0
) )

View File

@@ -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-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-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/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= 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/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@@ -20,12 +20,12 @@ func SortArrayAndComputeHash(s []string) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
return Encode(Hash(string(data))) return encode(hex256(string(data)))
} }
// Copied from https://github.com/kubernetes/kubernetes // Copied from https://github.com/kubernetes/kubernetes
// /blob/master/pkg/kubectl/util/hash/hash.go // /blob/master/pkg/kubectl/util/hash/hash.go
func Encode(hex string) (string, error) { func encode(hex string) (string, error) {
if len(hex) < 10 { if len(hex) < 10 {
return "", fmt.Errorf( return "", fmt.Errorf(
"input length must be at least 10") "input length must be at least 10")
@@ -48,23 +48,18 @@ func Encode(hex string) (string, error) {
return string(enc), nil return string(enc), nil
} }
// Hash returns the hex form of the sha256 of the argument. // hex256 returns the hex form of the sha256 of the argument.
func Hash(data string) string { func hex256(data string) string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(data))) return fmt.Sprintf("%x", sha256.Sum256([]byte(data)))
} }
// HashRNode returns the hash value of input RNode // Hasher computes the hash of an RNode.
func HashRNode(node *yaml.RNode) (string, error) { type Hasher struct{}
// get node kind
kindNode, err := node.Pipe(yaml.FieldMatcher{Name: "kind"})
if err != nil {
return "", err
}
kind := kindNode.YNode().Value
// calculate hash for different kinds // Hash returns a hash of the argument.
encoded := "" func (h *Hasher) Hash(node *yaml.RNode) (r string, err error) {
switch kind { var encoded string
switch node.GetKind() {
case "ConfigMap": case "ConfigMap":
encoded, err = encodeConfigMap(node) encoded, err = encodeConfigMap(node)
case "Secret": case "Secret":
@@ -77,10 +72,11 @@ func HashRNode(node *yaml.RNode) (string, error) {
if err != nil { if err != nil {
return "", err 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{}) values := make(map[string]interface{})
for _, p := range paths { for _, p := range paths {
vn, err := node.Pipe(yaml.Lookup(p)) vn, err := node.Pipe(yaml.Lookup(p))
@@ -117,8 +113,11 @@ func encodeConfigMap(node *yaml.RNode) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
m := map[string]interface{}{"kind": "ConfigMap", "name": values["metadata/name"], m := map[string]interface{}{
"data": values["data"]} "kind": "ConfigMap",
"name": values["metadata/name"],
"data": values["data"],
}
if _, ok := values["binaryData"].(map[string]interface{}); ok { if _, ok := values["binaryData"].(map[string]interface{}); ok {
m["binaryData"] = values["binaryData"] m["binaryData"] = values["binaryData"]
} }

View File

@@ -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 // hash the empty string to be sure that sha256 is being used
expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" expect := "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
sum := Hash("") sum := hex256("")
if expect != sum { if expect != sum {
t.Errorf("expected hash %q but got %q", expect, sum) t.Errorf("expected hash %q but got %q", expect, sum)
} }
@@ -93,17 +93,17 @@ data:
binaryData: binaryData:
two: ""`, "698h7c7t9m", ""}, two: ""`, "698h7c7t9m", ""},
} }
h := &Hasher{}
for _, c := range cases { for _, c := range cases {
node, err := yaml.Parse(c.cmYaml) node, err := yaml.Parse(c.cmYaml)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
h, err := HashRNode(node) hashed, err := h.Hash(node)
if SkipRest(t, c.desc, err, c.err) { if SkipRest(t, c.desc, err, c.err) {
continue continue
} }
if c.hash != h { if c.hash != hashed {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
} }
} }
@@ -154,35 +154,34 @@ type: my-type
data: data:
one: ""`, "74bd68bm66", ""}, one: ""`, "74bd68bm66", ""},
} }
h := &Hasher{}
for _, c := range cases { for _, c := range cases {
node, err := yaml.Parse(c.secretYaml) node, err := yaml.Parse(c.secretYaml)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
h, err := HashRNode(node) hashed, err := h.Hash(node)
if SkipRest(t, c.desc, err, c.err) { if SkipRest(t, c.desc, err, c.err) {
continue continue
} }
if c.hash != h { if c.hash != hashed {
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h)
} }
} }
} }
func TestUnstructuredHash(t *testing.T) { func TestBasicHash(t *testing.T) {
cases := []struct { cases := map[string]struct {
desc string res string
unstructured string hash string
hash string err string
err string
}{ }{
{"minimal", ` "minimal": {`
apiVersion: test/v1 apiVersion: test/v1
kind: TestResource kind: TestResource
metadata: metadata:
name: my-resource`, "244782mkb7", ""}, name: my-resource`, "244782mkb7", ""},
{"with spec", ` "with spec": {`
apiVersion: test/v1 apiVersion: test/v1
kind: TestResource kind: TestResource
metadata: metadata:
@@ -191,19 +190,22 @@ spec:
foo: 1 foo: 1
bar: abc`, "59m2mdccg4", ""}, bar: abc`, "59m2mdccg4", ""},
} }
h := &Hasher{}
for _, c := range cases { for n := range cases {
node, err := yaml.Parse(c.unstructured) c := cases[n]
if err != nil { t.Run(n, func(t *testing.T) {
t.Fatal(err) node, err := yaml.Parse(c.res)
} if err != nil {
h, err := HashRNode(node) t.Fatal(err)
if SkipRest(t, c.desc, err, c.err) { }
continue hashed, err := h.Hash(node)
} if SkipRest(t, n, err, c.err) {
if c.hash != h { return
t.Errorf("case %q, expect hash %q but got %q", c.desc, c.hash, h) }
} if c.hash != hashed {
t.Errorf("case %q, expect hash %q but got %q", n, c.hash, h)
}
})
} }
} }
@@ -334,9 +336,10 @@ data:
} }
} }
// SkipRest returns true if there was a non-nil error or if we expected an error that didn't happen, // SkipRest returns true if there was a non-nil error or if we expected an
// and logs the appropriate error on the test object. // 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. // 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 { func SkipRest(t *testing.T, desc string, err error, contains string) bool {
if err != nil { if err != nil {
if len(contains) == 0 { if len(contains) == 0 {

View File

@@ -5,8 +5,8 @@
package ifc package ifc
import ( import (
"sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
// Validator provides functions to validate annotations and labels // Validator provides functions to validate annotations and labels
@@ -38,92 +38,10 @@ type Loader interface {
Cleanup() error Cleanup() error
} }
// Kunstructured represents a Kubernetes Resource Model object. // KustHasher returns a hash of the argument
type Kunstructured interface {
// Several uses.
Copy() Kunstructured
// GetAnnotations returns the k8s annotations.
GetAnnotations() map[string]string
// GetData returns a top-level "data" field, as in a ConfigMap.
GetDataMap() map[string]string
// GetData returns a top-level "binaryData" field, as in a ConfigMap.
GetBinaryDataMap() map[string]string
// Used by ResAccumulator and ReplacementTransformer.
GetFieldValue(string) (interface{}, error)
// Used by Resource.OrgId
GetGvk() resid.Gvk
// Used by resource.Factory.SliceFromBytes
GetKind() string
// GetLabels returns the k8s labels.
GetLabels() map[string]string
// Used by Resource.CurId and resource factory.
GetName() string
// Used by special case code in
// ResMap.SubsetThatCouldBeReferencedByResource
GetSlice(path string) ([]interface{}, error)
// GetString returns the value of a string field.
// Used by Resource.GetNamespace
GetString(string) (string, error)
// Several uses.
Map() (map[string]interface{}, error)
// Used by Resource.AsYAML and Resource.String
MarshalJSON() ([]byte, error)
// Used by resWrangler.Select
MatchesAnnotationSelector(selector string) (bool, error)
// Used by resWrangler.Select
MatchesLabelSelector(selector string) (bool, error)
// SetAnnotations replaces the k8s annotations.
SetAnnotations(map[string]string)
// SetDataMap sets a top-level "data" field, as in a ConfigMap.
SetDataMap(map[string]string)
// SetDataMap sets a top-level "binaryData" field, as in a ConfigMap.
SetBinaryDataMap(map[string]string)
// Used by PatchStrategicMergeTransformer.
SetGvk(resid.Gvk)
// SetLabels replaces the k8s labels.
SetLabels(map[string]string)
// SetName changes the name.
SetName(string)
// SetNamespace changes the namespace.
SetNamespace(string)
// Needed, for now, by kyaml/filtersutil.ApplyToJSON.
UnmarshalJSON([]byte) error
}
// KunstructuredFactory makes instances of Kunstructured.
type KunstructuredFactory interface {
SliceFromBytes([]byte) ([]Kunstructured, error)
FromMap(m map[string]interface{}) Kunstructured
Hasher() KunstructuredHasher
MakeConfigMap(kvLdr KvLoader, args *types.ConfigMapArgs) (Kunstructured, error)
MakeSecret(kvLdr KvLoader, args *types.SecretArgs) (Kunstructured, error)
}
// KunstructuredHasher returns a hash of the argument
// or an error. // or an error.
type KunstructuredHasher interface { type KustHasher interface {
Hash(Kunstructured) (string, error) Hash(*yaml.RNode) (string, error)
} }
// See core.v1.SecretTypeOpaque // See core.v1.SecretTypeOpaque

View File

@@ -37,22 +37,10 @@ func (o *multiTransformer) Transform(m resmap.ResMap) error {
func (o *multiTransformer) transform(m resmap.ResMap) error { func (o *multiTransformer) transform(m resmap.ResMap) error {
for _, t := range o.transformers { for _, t := range o.transformers {
err := t.Transform(m) if err := t.Transform(m); err != nil {
if err != nil {
return err return err
} }
} m.DropEmpties()
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
}
}
} }
return nil return nil
} }

View File

@@ -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
}

View File

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

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package krusty_test
import ( import (
"testing" "testing"
"github.com/stretchr/testify/assert"
kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest" kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
) )
@@ -35,5 +36,8 @@ spec:
- name: PODINFO_UI_COLOR - name: PODINFO_UI_COLOR
value: "#34577c" 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")
} }

View File

@@ -4,103 +4,21 @@
package provider package provider
import ( import (
"sigs.k8s.io/kustomize/api/hasher"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/conflict" "sigs.k8s.io/kustomize/api/internal/conflict"
"sigs.k8s.io/kustomize/api/internal/validate" "sigs.k8s.io/kustomize/api/internal/validate"
"sigs.k8s.io/kustomize/api/internal/wrappy"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
) )
// DepProvider is a dependency provider. // DepProvider is a dependency provider, injecting different
// implementations depending on the context.
// //
// The instances it returns are either // Notes on interfaces:
// - old implementations backed by k8sdeps code,
// - new implementations backed by kyaml code.
// //
// History: // - resource.ConflictDetector
// //
// kubectl depends on k8s.io code, and at the time of writing, so // implemented by api/internal/conflict.smPatchMergeOnlyDetector
// does kustomize. Code that imports k8s.io/api* cannot be imported
// back into k8s.io/*, yet kustomize appears inside k8s.io/kubectl.
//
// To allow kustomize to appear inside kubectl, yet still be developed
// outside kubectl, the kustomize code was divided into the following
// packages
//
// api/
// k8sdeps/ (and internal/ks8deps/)
// ifc/
// krusty/
// everythingElse/
//
// with the following rules:
//
// - Only k8sdeps/ may import k8s.io/api*.
//
// - Only krusty/ (and its internals) may import k8sdeps/.
// I.e., ifc/ and everythingElse/ must not
// import k8sdeps/ or k8s.io/api*.
//
// - Code in krusty/ may use code in k8sdeps/ to create
// objects then inject said objects into
// everythingElse/ behind dependency neutral interfaces.
//
// The idea was to periodically copy, not import, the large k8sdeps/
// tree (plus a snippet from krusty/kustomizer.go) into the kubectl
// codebase via a large PR, and have kubectl depend on the rest via
// normal importing.
//
// Over 2019, however, kubectl underwent large changes including
// a switch to Go modules, and a concerted attempt to extract kubectl
// from the k8s repo. This made large kustomize integration PRs too
// intrusive to review.
//
// In 2020, kubectl is based on Go modules, and almost entirely
// extracted from the k8s.io repositories, and further the kyaml
// library has a appeared as a viable replacement to k8s.io/api*
// KRM manipulation code.
//
// The new plan is to eliminate k8sdeps/ entirely, along with its
// k8s.io/api* dependence, allowing kustomize code to be imported
// into kubectl via normal Go module imports. Then the kustomize API
// code can then move into the github.com/kubernetes-sigs/cli-utils
// repo. The kustomize CLI in github.com/kubernetes-sigs/kustomize
// and the kubectl CLI can then both depend on the kustomize API.
//
// So, all code that depends on k8sdeps must go behind interfaces,
// and kustomize must be factored to choose the implementation.
//
// That problem has been reduced to three interfaces, each having
// two implementations. (1) is k8sdeps-based, (2) is kyaml-based.
//
// - ifc.Kunstructured
//
// 1) api/k8sdeps/kunstruct.UnstructAdapter
//
// This adapts structs in
// k8s.io/apimachinery/pkg/apis/meta/v1/unstructured
// to ifc.Kunstructured.
//
// 2) api/wrappy.WNode
//
// This adapts sigs.k8s.io/kustomize/kyaml/yaml.RNode
// to ifc.Unstructured.
//
// At time of writing, implementation started.
// Further reducing the size of ifc.Kunstructed
// would really reduce the work
// (e.g. drop Vars, drop ReplacementTranformer).
//
// - resource.ConflictDetector
//
// 1) api/internal/k8sdeps/conflict.conflictDetectorJson
// api/internal/k8sdeps/conflict.conflictDetectorSm
//
// Uses k8s.io/apimachinery/pkg/util/strategicpatch,
// apimachinery/pkg/util/mergepatch, etc. to merge
// resource.Resource instances.
//
// 2) api/internal/conflict.smPatchMergeOnlyDetector
// //
// At time of writing, this doesn't report conflicts, // At time of writing, this doesn't report conflicts,
// but it does know how to merge patches. Conflict // but it does know how to merge patches. Conflict
@@ -113,49 +31,23 @@ import (
// is plainly visible and usable in the output, even if // is plainly visible and usable in the output, even if
// a conflict happened but wasn't reported as an error. // a conflict happened but wasn't reported as an error.
// //
// - ifc.Validator // - ifc.Validator
// //
// 1) api/k8sdeps/validator.KustValidator // implemented by api/internal/validate.FieldValidator
//
// Uses k8s.io/apimachinery/pkg/api/validation and
// friends to validate strings.
//
// 2) api/internal/validate.FieldValidator
// //
// See TODO inside the validator for status. // See TODO inside the validator for status.
// At time of writing, this is a do-nothing // At time of writing, this is a do-nothing
// validator as it's not critical to kustomize function. // validator as it's not critical to kustomize function.
// //
// Proposed plan:
// [x] Ship kustomize with the ability to switch from 1 to 2 via
// an --enable_kyaml flag.
// [x] Make --enable_kyaml true by default.
// [x] When 2 is not noticeably more buggy than 1, delete 1.
// I.e. delete k8sdeps/, transitively deleting all k8s.io/api* deps.
// This DepProvider should be left in place to retain these
// comments, but it will have only one choice.
// [x] The way is now clear to reintegrate into kubectl.
// This should be done ASAP; the last step is cleanup.
// [ ] Cleanup. With only one impl of Kunstructure remaining,
// that interface and WNode can be deleted, along with this
// DepProvider. The other two interfaces could be dropped too.
//
// When the above is done, kustomize will use yaml.RNode and/or
// KRM Config Functions directly and exclusively.
// If you're reading this, plan not done.
//
type DepProvider struct { type DepProvider struct {
kFactory ifc.KunstructuredFactory
resourceFactory *resource.Factory resourceFactory *resource.Factory
conflictDectectorFactory resource.ConflictDetectorFactory conflictDectectorFactory resource.ConflictDetectorFactory
fieldValidator ifc.Validator fieldValidator ifc.Validator
} }
func NewDepProvider() *DepProvider { func NewDepProvider() *DepProvider {
kf := &wrappy.WNodeFactory{} rf := resource.NewFactory(&hasher.Hasher{})
rf := resource.NewFactory(kf)
return &DepProvider{ return &DepProvider{
kFactory: kf,
resourceFactory: rf, resourceFactory: rf,
conflictDectectorFactory: conflict.NewFactory(), conflictDectectorFactory: conflict.NewFactory(),
fieldValidator: validate.NewFieldValidator(), fieldValidator: validate.NewFieldValidator(),
@@ -166,10 +58,6 @@ func NewDefaultDepProvider() *DepProvider {
return NewDepProvider() return NewDepProvider()
} }
func (dp *DepProvider) GetKunstructuredFactory() ifc.KunstructuredFactory {
return dp.kFactory
}
func (dp *DepProvider) GetResourceFactory() *resource.Factory { func (dp *DepProvider) GetResourceFactory() *resource.Factory {
return dp.resourceFactory return dp.resourceFactory
} }

View File

@@ -36,6 +36,14 @@ func ParseGroupVersion(apiVersion string) (group, version string) {
// GvkFromString makes a Gvk from the output of Gvk.String(). // GvkFromString makes a Gvk from the output of Gvk.String().
func GvkFromString(s string) Gvk { func GvkFromString(s string) Gvk {
values := strings.Split(s, fieldSep) 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] g := values[0]
if g == noGroup { if g == noGroup {
g = "" 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 // Implements https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/#not-all-objects-are-in-a-namespace
func (x Gvk) IsNamespaceableKind() bool { func (x Gvk) IsNamespaceableKind() bool {
isNamespaceScoped, found := openapi.IsNamespaceScoped(x.toKyamlTypeMeta()) isNamespaceScoped, found := openapi.IsNamespaceScoped(x.toKyamlTypeMeta())

View File

@@ -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) { func TestApiVersion(t *testing.T) {
for _, hey := range []struct { for _, hey := range []struct {
x Gvk x Gvk

View File

@@ -146,18 +146,10 @@ func newResMapFromResourceSlice(
} }
// NewResMapFromRNodeSlice returns a ResMap from a slice of RNodes // NewResMapFromRNodeSlice returns a ResMap from a slice of RNodes
func (rmF *Factory) NewResMapFromRNodeSlice(rnodes []*yaml.RNode) (ResMap, error) { func (rmF *Factory) NewResMapFromRNodeSlice(s []*yaml.RNode) (ResMap, error) {
var resources []*resource.Resource rs, err := rmF.resF.ResourcesFromRNodes(s)
for _, rnode := range rnodes { if err != nil {
s, err := rnode.String() return nil, err
if err != nil {
return nil, err
}
r, err := rmF.resF.SliceFromBytes([]byte(s))
if err != nil {
return nil, err
}
resources = append(resources, r...)
} }
return newResMapFromResourceSlice(resources) return newResMapFromResourceSlice(rs)
} }

View File

@@ -336,17 +336,18 @@ metadata:
expected: resmaptest_test.NewRmBuilder(t, rf).ResMap(), expected: resmaptest_test.NewRmBuilder(t, rf).ResMap(),
}, },
} }
for name, tc := range testcases { for name := range testcases {
rnodes := []*yaml.RNode{ tc := testcases[name]
yaml.MustParse(tc.input), t.Run(name, func(t *testing.T) {
} rm, err := rmF.NewResMapFromRNodeSlice(
rm, err := rmF.NewResMapFromRNodeSlice(rnodes) []*yaml.RNode{yaml.MustParse(tc.input)})
if err != nil { if err != nil {
t.Fatalf("unexpected error in test case [%s]: %v", name, err) t.Fatalf("unexpected error in test case [%s]: %v", name, err)
} }
if err = tc.expected.ErrorIfNotEqualLists(rm); err != nil { if err = tc.expected.ErrorIfNotEqualLists(rm); err != nil {
t.Fatalf("error in test case [%s]: %s", name, err) t.Fatalf("error in test case [%s]: %s", name, err)
} }
})
} }
} }

View File

@@ -80,6 +80,9 @@ type TransformerPlugin interface {
// resource to transform, try the OrgId first, and if this // resource to transform, try the OrgId first, and if this
// fails or finds too many, it might make sense to then try // fails or finds too many, it might make sense to then try
// the CurrId. Depends on the situation. // 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 { type ResMap interface {
// Size reports the number of resources. // Size reports the number of resources.
Size() int Size() int
@@ -189,6 +192,9 @@ type ResMap interface {
// Clear removes all resources and Ids. // Clear removes all resources and Ids.
Clear() Clear()
// DropEmpties drops empty resources from the ResMap.
DropEmpties()
// SubsetThatCouldBeReferencedByResource returns a ResMap subset // SubsetThatCouldBeReferencedByResource returns a ResMap subset
// of self with resources that could be referenced by the // of self with resources that could be referenced by the
// resource argument. // resource argument.
@@ -231,9 +237,8 @@ type ResMap interface {
// are selected by a Selector // are selected by a Selector
Select(types.Selector) ([]*resource.Resource, error) Select(types.Selector) ([]*resource.Resource, error)
// ToRNodeSlice converts the resources in the resmp // ToRNodeSlice returns a copy of the resources as RNodes.
// to a list of RNodes ToRNodeSlice() []*yaml.RNode
ToRNodeSlice() ([]*yaml.RNode, error)
// ApplySmPatch applies a strategic-merge patch to the // ApplySmPatch applies a strategic-merge patch to the
// selected set of resources. // selected set of resources.

View File

@@ -6,14 +6,12 @@ package resmap
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/resource" "sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
kyaml_yaml "sigs.k8s.io/kustomize/kyaml/yaml" kyaml "sigs.k8s.io/kustomize/kyaml/yaml"
"sigs.k8s.io/yaml"
) )
// resWrangler implements ResMap. // resWrangler implements ResMap.
@@ -38,6 +36,18 @@ func (m *resWrangler) Clear() {
m.rList = nil 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. // Size implements ResMap.
func (m *resWrangler) Size() int { func (m *resWrangler) Size() int {
return len(m.rList) return len(m.rList)
@@ -66,22 +76,27 @@ func (m *resWrangler) Append(res *resource.Resource) error {
return fmt.Errorf( return fmt.Errorf(
"may not add resource with an already registered id: %s", id) "may not add resource with an already registered id: %s", id)
} }
m.rList = append(m.rList, res) m.append(res)
return nil 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. // Remove implements ResMap.
func (m *resWrangler) Remove(adios resid.ResId) error { func (m *resWrangler) Remove(adios resid.ResId) error {
tmp := newOne() var rList []*resource.Resource
for _, r := range m.rList { for _, r := range m.rList {
if r.CurId() != adios { 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) return fmt.Errorf("id %s not found in removal", adios)
} }
m.rList = tmp.rList m.rList = rList
return nil return nil
} }
@@ -118,16 +133,7 @@ func (m *resWrangler) Debug(title string) {
} else { } else {
fmt.Println("---") fmt.Println("---")
} }
fmt.Printf("# %d %s\n", i, r.OrgId()) fmt.Printf("# %d %s\n%s\n", i, r.OrgId(), r.String())
m, err := r.Map()
if err != nil {
panic(err)
}
blob, err := yaml.Marshal(m)
if err != nil {
panic(err)
}
fmt.Println(string(blob))
} }
} }
@@ -273,7 +279,7 @@ func (m *resWrangler) AsYaml() ([]byte, error) {
firstObj := true firstObj := true
var b []byte var b []byte
buf := bytes.NewBuffer(b) buf := bytes.NewBuffer(b)
for _, res := range m.Resources() { for _, res := range m.rList {
out, err := res.AsYAML() out, err := res.AsYAML()
if err != nil { if err != nil {
m, _ := res.Map() m, _ := res.Map()
@@ -297,7 +303,7 @@ func (m *resWrangler) AsYaml() ([]byte, error) {
func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error { func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error {
m2, ok := other.(*resWrangler) m2, ok := other.(*resWrangler)
if !ok { if !ok {
panic("bad cast") return fmt.Errorf("bad cast to resWrangler 1")
} }
if m.Size() != m2.Size() { if m.Size() != m2.Size() {
return fmt.Errorf( return fmt.Errorf(
@@ -317,9 +323,9 @@ func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error {
"id in self matches %d in other; id: %s", len(others), id) "id in self matches %d in other; id: %s", len(others), id)
} }
r2 := others[0] r2 := others[0]
if !r1.KunstructEqual(r2) { if !r1.NodeEqual(r2) {
return fmt.Errorf( return fmt.Errorf(
"kunstruct not equal: \n -- %s,\n -- %s\n\n--\n%#v\n------\n%#v\n", "nodes unequal: \n -- %s,\n -- %s\n\n--\n%#v\n------\n%#v\n",
r1, r2, r1, r2) r1, r2, r1, r2)
} }
seen[m2.indexOfResource(r2)] = true seen[m2.indexOfResource(r2)] = true
@@ -334,7 +340,7 @@ func (m *resWrangler) ErrorIfNotEqualSets(other ResMap) error {
func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error { func (m *resWrangler) ErrorIfNotEqualLists(other ResMap) error {
m2, ok := other.(*resWrangler) m2, ok := other.(*resWrangler)
if !ok { if !ok {
panic("bad cast") return fmt.Errorf("bad cast to resWrangler 2")
} }
if m.Size() != m2.Size() { if m.Size() != m2.Size() {
return fmt.Errorf( return fmt.Errorf(
@@ -388,7 +394,7 @@ func (m *resWrangler) SubsetThatCouldBeReferencedByResource(
} }
result := newOne() result := newOne()
roleBindingNamespaces := getNamespacesForRoleBinding(referrer) roleBindingNamespaces := getNamespacesForRoleBinding(referrer)
for _, possibleTarget := range m.Resources() { for _, possibleTarget := range m.rList {
id := possibleTarget.CurId() id := possibleTarget.CurId()
if !id.IsNamespaceableKind() { if !id.IsNamespaceableKind() {
// A cluster-scoped resource can be referred to by anything. // A cluster-scoped resource can be referred to by anything.
@@ -435,16 +441,21 @@ func getNamespacesForRoleBinding(r *resource.Resource) map[string]bool {
return result return result
} }
func (m *resWrangler) append(res *resource.Resource) {
m.rList = append(m.rList, res)
}
// AppendAll implements ResMap. // AppendAll implements ResMap.
func (m *resWrangler) AppendAll(other ResMap) error { func (m *resWrangler) AppendAll(other ResMap) error {
if other == nil { if other == nil {
return 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 { if err := m.Append(res); err != nil {
return err return err
} }
@@ -457,7 +468,11 @@ func (m *resWrangler) AbsorbAll(other ResMap) error {
if other == nil { if other == nil {
return 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) err := m.appendReplaceOrMerge(r)
if err != nil { if err != nil {
return err return err
@@ -522,7 +537,7 @@ func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
for _, r := range m.Resources() { for _, r := range m.rList {
curId := r.CurId() curId := r.CurId()
orgId := r.OrgId() orgId := r.OrgId()
@@ -567,78 +582,39 @@ func (m *resWrangler) Select(s types.Selector) ([]*resource.Resource, error) {
return result, nil return result, nil
} }
// ToRNodeSlice converts the resources in the resmp // ToRNodeSlice returns a copy of the resources as RNodes.
// to a list of RNodes func (m *resWrangler) ToRNodeSlice() []*kyaml.RNode {
func (m *resWrangler) ToRNodeSlice() ([]*kyaml_yaml.RNode, error) { result := make([]*kyaml.RNode, len(m.rList))
var rnodes []*kyaml_yaml.RNode for i := range m.rList {
for _, r := range m.Resources() { result[i] = m.rList[i].AsRNode()
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)
} }
return rnodes, nil return result
} }
// ApplySmPatch applies the patch, and errors on Id collisions.
func (m *resWrangler) ApplySmPatch( func (m *resWrangler) ApplySmPatch(
selectedSet *resource.IdSet, patch *resource.Resource) error { selectedSet *resource.IdSet, patch *resource.Resource) error {
newRm := New() var list []*resource.Resource
for _, res := range m.Resources() { for _, res := range m.rList {
if !selectedSet.Contains(res.CurId()) { if selectedSet.Contains(res.CurId()) {
newRm.Append(res) patchCopy := patch.DeepCopy()
continue patchCopy.CopyMergeMetaDataFieldsFrom(patch)
} patchCopy.SetGvk(res.GetGvk())
patchCopy := patch.DeepCopy() patchCopy.SetKind(patch.GetKind())
patchCopy.CopyMergeMetaDataFieldsFrom(patch) if err := res.ApplySmPatch(patchCopy); err != nil {
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.
return err 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 !res.IsEmpty() {
if err != nil { list = append(list, res)
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)
} }
} }
m.Clear() m.Clear()
m.AppendAll(newRm) return m.appendAll(list)
return nil
} }
func (m *resWrangler) RemoveBuildAnnotations() { func (m *resWrangler) RemoveBuildAnnotations() {
for _, r := range m.Resources() { for _, r := range m.rList {
r.RemoveBuildAnnotations() r.RemoveBuildAnnotations()
} }
} }

View File

@@ -877,13 +877,8 @@ rules:
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
rnodes, err := rm.ToRNodeSlice()
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
b := bytes.NewBufferString("") b := bytes.NewBufferString("")
for i, n := range rnodes { for i, n := range rm.ToRNodeSlice() {
if i != 0 { if i != 0 {
b.WriteString("---\n") b.WriteString("---\n")
} }

View File

@@ -10,28 +10,33 @@ import (
"strings" "strings"
"sigs.k8s.io/kustomize/api/ifc" "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/internal/kusterr"
"sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
// Factory makes instances of Resource. // Factory makes instances of Resource.
type Factory struct { type Factory struct {
kf ifc.KunstructuredFactory hasher ifc.KustHasher
} }
// NewFactory makes an instance of Factory. // NewFactory makes an instance of Factory.
func NewFactory(kf ifc.KunstructuredFactory) *Factory { func NewFactory(h ifc.KustHasher) *Factory {
return &Factory{kf: kf} return &Factory{hasher: h}
} }
func (rf *Factory) Hasher() ifc.KunstructuredHasher { // Hasher returns an ifc.KustHasher
return rf.kf.Hasher() func (rf *Factory) Hasher() ifc.KustHasher {
return rf.hasher
} }
// FromMap returns a new instance of Resource. // FromMap returns a new instance of Resource.
func (rf *Factory) FromMap(m map[string]interface{}) *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. // 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. // FromMapWithNamespaceAndName returns a new instance with the given "original" namespace.
func (rf *Factory) FromMapWithNamespaceAndName(ns string, n string, m map[string]interface{}) *Resource { 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()) return r.setPreviousId(ns, n, r.GetKind())
} }
// FromMapAndOption returns a new instance of Resource with given options. // FromMapAndOption returns a new instance of Resource with given options.
func (rf *Factory) FromMapAndOption( func (rf *Factory) FromMapAndOption(
m map[string]interface{}, args *types.GeneratorArgs) *Resource { m map[string]interface{}, args *types.GeneratorArgs) *Resource {
return rf.makeOne(rf.kf.FromMap(m), types.NewGenArgs(args)) n, err := yaml.FromMap(m)
} if err != nil {
// TODO: return err instead of log.
// FromKunstructured returns a new instance of Resource. log.Fatal(err)
func (rf *Factory) FromKunstructured(u ifc.Kunstructured) *Resource { }
return rf.makeOne(u, nil) return rf.makeOne(n, types.NewGenArgs(args))
} }
// makeOne returns a new instance of Resource. // makeOne returns a new instance of Resource.
func (rf *Factory) makeOne( func (rf *Factory) makeOne(rn *yaml.RNode, o *types.GenArgs) *Resource {
u ifc.Kunstructured, o *types.GenArgs) *Resource { if rn == nil {
if u == nil { log.Fatal("RNode must not be null")
log.Fatal("unstruct ifc must not be null")
} }
if o == nil { if o == nil {
o = types.NewGenArgs(nil) o = types.NewGenArgs(nil)
} }
r := &Resource{ return &Resource{node: rn, options: o}
kunStr: u,
options: o,
}
return r
} }
// SliceFromPatches returns a slice of resources given a patch path // 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. // SliceFromBytes unmarshals bytes into a Resource slice.
func (rf *Factory) SliceFromBytes(in []byte) ([]*Resource, error) { func (rf *Factory) SliceFromBytes(in []byte) ([]*Resource, error) {
kunStructs, err := rf.kf.SliceFromBytes(in) nodes, err := rf.RNodesFromBytes(in)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var result []*Resource return rf.resourcesFromRNodes(nodes), nil
for len(kunStructs) > 0 { }
u := kunStructs[0]
kunStructs = kunStructs[1:] // ResourcesFromRNodes converts RNodes to Resources.
if strings.HasSuffix(u.GetKind(), "List") { func (rf *Factory) ResourcesFromRNodes(
m, err := u.Map() nodes []*yaml.RNode) (result []*Resource, err error) {
if err != nil { nodes, err = rf.dropBadNodes(nodes)
return nil, err 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"] return nil, fmt.Errorf(
itemsSlice, ok := items.([]interface{}) "expected array in %s/items, but found %T", kind, items)
if !ok { }
if items == nil { innerNodes, err := rf.convertObjectSliceToNodeSlice(slice)
// an empty list if err != nil {
continue return nil, err
} }
return nil, fmt.Errorf("items in List is type %T, expected array", items) nodes = append(nodes, innerNodes...)
} }
for _, item := range itemsSlice { return result, nil
itemJSON, err := json.Marshal(item) }
if err != nil {
return nil, err // convertObjectSlice converts a list of objects to a list of RNode.
} func (rf *Factory) convertObjectSliceToNodeSlice(
innerU, err := rf.kf.SliceFromBytes(itemJSON) objects []interface{}) (result []*yaml.RNode, err error) {
if err != nil { var bytes []byte
return nil, err var nodes []*yaml.RNode
} for _, obj := range objects {
// append innerU to kunStructs so nested Lists can be handled bytes, err = json.Marshal(obj)
kunStructs = append(kunStructs, innerU...) if err != nil {
} return
} else { }
result = append(result, rf.FromKunstructured(u)) 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 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 // SliceFromBytesWithNames unmarshals bytes into a Resource slice with specified original
// name. // name.
func (rf *Factory) SliceFromBytesWithNames(names []string, in []byte) ([]*Resource, error) { 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 // MakeConfigMap makes an instance of Resource for ConfigMap
func (rf *Factory) MakeConfigMap(kvLdr ifc.KvLoader, args *types.ConfigMapArgs) (*Resource, error) { 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 { if err != nil {
return nil, err 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 // MakeSecret makes an instance of Resource for Secret
func (rf *Factory) MakeSecret(kvLdr ifc.KvLoader, args *types.SecretArgs) (*Resource, error) { 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 { if err != nil {
return nil, err return nil, err
} }
return rf.makeOne(u, types.NewGenArgs(&args.GeneratorArgs)), nil return rf.makeOne(rn, types.NewGenArgs(&args.GeneratorArgs)), nil
} }

View File

@@ -5,6 +5,7 @@ package resource_test
import ( import (
"fmt" "fmt"
"reflect"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@@ -151,35 +152,33 @@ spec:
for name := range testCases { for name := range testCases {
tc := testCases[name] tc := testCases[name]
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
result, err := factory.SliceFromBytes([]byte(tc.input)) result, err := factory.RNodesFromBytes([]byte(tc.input))
if err != nil { if err != nil {
t.Fatalf("%v: fails with err: %v", name, err) t.Fatalf("%v: fails with err: %v", name, err)
} }
if len(result) != len(tc.expected) { if len(result) != len(tc.expected) {
for i := range result { for i := range result {
bytes, err := result[i].AsYAML() str, err := result[i].String()
if err != nil { if err != nil {
t.Fatalf("%v: result to YAML fails with err: %v", name, err) t.Fatalf("%v: result to YAML fails with err: %v", name, err)
} }
tmp := string(bytes) t.Logf("--- %d:\n%s", i, str)
t.Logf("--- %d:\n%s", i, tmp)
} }
t.Fatalf( t.Fatalf(
"%v: actual len %d != expected len %d", "%v: actual len %d != expected len %d",
name, len(result), len(tc.expected)) name, len(result), len(tc.expected))
} }
for i := range tc.expected { for i := range tc.expected {
bytes, err := result[i].AsYAML() str, err := result[i].String()
if err != nil { if err != nil {
t.Fatalf("%v: result to YAML fails with err: %v", name, err) t.Fatalf("%v: result to YAML fails with err: %v", name, err)
} }
tmp := string(bytes) if str != tc.expected[i] {
if tmp != tc.expected[i] {
t.Fatalf( t.Fatalf(
"%v: string mismatch in item %d\n"+ "%v: string mismatch in item %d\n"+
"actual:\n-----\n%s\n-----\n"+ "actual:\n-----\n%s\n-----\n"+
"expected:\n-----\n%s\n-----\n", "expected:\n-----\n%s\n-----\n",
name, i, tmp, tc.expected[i]) name, i, str, tc.expected[i])
} }
} }
}) })
@@ -307,76 +306,376 @@ kind: List
t.Fatal(err) t.Fatal(err)
} }
tests := []struct { tests := map[string]struct {
name string
input []types.PatchStrategicMerge input []types.PatchStrategicMerge
expectedOut []*Resource expectedOut []*Resource
expectedErr bool expectedErr bool
}{ }{
{ "happy": {
name: "happy",
input: []types.PatchStrategicMerge{patchGood1, patchGood2}, input: []types.PatchStrategicMerge{patchGood1, patchGood2},
expectedOut: []*Resource{testDeployment, testConfigMap}, expectedOut: []*Resource{testDeployment, testConfigMap},
expectedErr: false, expectedErr: false,
}, },
{ "badFileName": {
name: "badFileName",
input: []types.PatchStrategicMerge{patchGood1, "doesNotExist"}, input: []types.PatchStrategicMerge{patchGood1, "doesNotExist"},
expectedOut: []*Resource{}, expectedOut: []*Resource{},
expectedErr: true, expectedErr: true,
}, },
{ "badData": {
name: "badData",
input: []types.PatchStrategicMerge{patchGood1, patchBad}, input: []types.PatchStrategicMerge{patchGood1, patchBad},
expectedOut: []*Resource{}, expectedOut: []*Resource{},
expectedErr: true, expectedErr: true,
}, },
{ "listOfPatches": {
name: "listOfPatches",
input: []types.PatchStrategicMerge{patchList}, input: []types.PatchStrategicMerge{patchList},
expectedOut: []*Resource{testDeployment, testConfigMap}, expectedOut: []*Resource{testDeployment, testConfigMap},
expectedErr: false, expectedErr: false,
}, },
{ "listWithAnchorReference": {
name: "listWithAnchorReference",
input: []types.PatchStrategicMerge{patchList2}, input: []types.PatchStrategicMerge{patchList2},
expectedOut: []*Resource{testDeploymentA, testDeploymentB}, expectedOut: []*Resource{testDeploymentA, testDeploymentB},
// The error using kyaml is: // The error using kyaml is:
// json: unsupported type: map[interface {}]interface {} // json: unsupported type: map[interface {}]interface {}
// maybe arising from too many conversions between // maybe arising from too many conversions between
// yaml, json, Resource, RNode, Unstructured etc. // yaml, json, Resource, RNode, etc.
// These conversions go away after closing #3506 // These conversions go away after closing #3506
// TODO(#3271) This shouldn't have an error, but does when kyaml is used. // TODO(#3271) This shouldn't have an error, but does when kyaml is used.
expectedErr: true, expectedErr: true,
}, },
{ "listWithNoEntries": {
name: "listWithNoEntries",
input: []types.PatchStrategicMerge{patchList3}, input: []types.PatchStrategicMerge{patchList3},
expectedOut: []*Resource{}, expectedOut: []*Resource{},
expectedErr: false, expectedErr: false,
}, },
{ "listWithNoItems": {
name: "listWithNoItems",
input: []types.PatchStrategicMerge{patchList4}, input: []types.PatchStrategicMerge{patchList4},
expectedOut: []*Resource{}, expectedOut: []*Resource{},
expectedErr: false, expectedErr: false,
}, },
} }
for _, test := range tests { for n, test := range tests {
rs, err := factory.SliceFromPatches(ldr, test.input) t.Run(n, func(t *testing.T) {
if err != nil { rs, err := factory.SliceFromPatches(ldr, test.input)
assert.True(t, test.expectedErr, if err != nil {
fmt.Sprintf("in test %s, got unexpected error: %v", test.name, err)) assert.True(t, test.expectedErr,
continue fmt.Sprintf("in test %s, got unexpected error: %v", n, err))
} return
assert.False(t, test.expectedErr, "expected no error in "+test.name) }
assert.Equal(t, len(test.expectedOut), len(rs)) assert.False(t, test.expectedErr, "expected no error in "+n)
for i := range rs { assert.Equal(t, len(test.expectedOut), len(rs))
expYaml, err := test.expectedOut[i].AsYAML() for i := range rs {
assert.NoError(t, err) expYaml, err := test.expectedOut[i].AsYAML()
actYaml, err := rs[i].AsYAML() assert.NoError(t, err)
assert.NoError(t, err) actYaml, err := rs[i].AsYAML()
assert.Equal(t, expYaml, actYaml) 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)
}
}
})
} }
} }

View File

@@ -12,7 +12,6 @@ import (
"sigs.k8s.io/kustomize/api/filters/patchstrategicmerge" "sigs.k8s.io/kustomize/api/filters/patchstrategicmerge"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/internal/wrappy"
"sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resid"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
@@ -22,11 +21,10 @@ import (
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
// Resource is a representation of a Kubernetes Resource Model (KRM) object // Resource is an RNode, representing a Kubernetes Resource Model object,
// paired with metadata used by kustomize. // paired with metadata used by kustomize.
// For more history, see sigs.k8s.io/kustomize/api/ifc.Unstructured
type Resource struct { type Resource struct {
kunStr ifc.Kunstructured node *kyaml.RNode
options *types.GenArgs options *types.GenArgs
refBy []resid.ResId refBy []resid.ResId
refVarNames []string refVarNames []string
@@ -55,115 +53,133 @@ var buildAnnotations = []string{
buildAnnotationAllowKindChange, buildAnnotationAllowKindChange,
} }
func (r *Resource) AsRNode() *kyaml.RNode {
return r.node.Copy()
}
func (r *Resource) ResetPrimaryData(incoming *Resource) { func (r *Resource) ResetPrimaryData(incoming *Resource) {
r.kunStr = incoming.Copy() r.node = incoming.node.Copy()
} }
func (r *Resource) GetAnnotations() map[string]string { func (r *Resource) GetAnnotations() map[string]string {
annotations := r.kunStr.GetAnnotations() annotations, err := r.node.GetAnnotations()
if annotations == nil { if err != nil || annotations == nil {
return make(map[string]string) return make(map[string]string)
} }
return annotations return annotations
} }
func (r *Resource) Copy() ifc.Kunstructured {
return r.kunStr.Copy()
}
func (r *Resource) GetFieldValue(f string) (interface{}, error) { func (r *Resource) GetFieldValue(f string) (interface{}, error) {
return r.kunStr.GetFieldValue(f) //nolint:staticcheck
return r.node.GetFieldValue(f)
} }
func (r *Resource) GetDataMap() map[string]string { func (r *Resource) GetDataMap() map[string]string {
return r.kunStr.GetDataMap() return r.node.GetDataMap()
} }
func (r *Resource) GetBinaryDataMap() map[string]string { func (r *Resource) GetBinaryDataMap() map[string]string {
return r.kunStr.GetBinaryDataMap() return r.node.GetBinaryDataMap()
} }
func (r *Resource) GetGvk() resid.Gvk { func (r *Resource) GetGvk() resid.Gvk {
return r.kunStr.GetGvk() meta, err := r.node.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.KustHasher) (string, error) {
return h.Hash(r.node)
} }
func (r *Resource) GetKind() string { func (r *Resource) GetKind() string {
return r.kunStr.GetKind() return r.node.GetKind()
} }
func (r *Resource) GetLabels() map[string]string { func (r *Resource) GetLabels() map[string]string {
return r.kunStr.GetLabels() l, err := r.node.GetLabels()
if err != nil {
return map[string]string{}
}
return l
} }
func (r *Resource) GetName() string { func (r *Resource) GetName() string {
return r.kunStr.GetName() return r.node.GetName()
} }
func (r *Resource) GetSlice(p string) ([]interface{}, error) { func (r *Resource) GetSlice(p string) ([]interface{}, error) {
return r.kunStr.GetSlice(p) //nolint:staticcheck
return r.node.GetSlice(p)
} }
func (r *Resource) GetString(p string) (string, error) { func (r *Resource) GetString(p string) (string, error) {
return r.kunStr.GetString(p) //nolint:staticcheck
return r.node.GetString(p)
} }
func (r *Resource) IsEmpty() (bool, error) { func (r *Resource) IsEmpty() bool {
m, err := r.kunStr.Map() return r.node.IsNilOrEmpty()
return len(m) == 0, err
} }
func (r *Resource) Map() (map[string]interface{}, error) { func (r *Resource) Map() (map[string]interface{}, error) {
return r.kunStr.Map() return r.node.Map()
} }
func (r *Resource) MarshalJSON() ([]byte, error) { func (r *Resource) MarshalJSON() ([]byte, error) {
return r.kunStr.MarshalJSON() return r.node.MarshalJSON()
} }
func (r *Resource) MatchesLabelSelector(selector string) (bool, error) { func (r *Resource) MatchesLabelSelector(selector string) (bool, error) {
return r.kunStr.MatchesLabelSelector(selector) return r.node.MatchesLabelSelector(selector)
} }
func (r *Resource) MatchesAnnotationSelector(selector string) (bool, error) { func (r *Resource) MatchesAnnotationSelector(selector string) (bool, error) {
return r.kunStr.MatchesAnnotationSelector(selector) return r.node.MatchesAnnotationSelector(selector)
} }
func (r *Resource) SetAnnotations(m map[string]string) { func (r *Resource) SetAnnotations(m map[string]string) {
if len(m) == 0 { if len(m) == 0 {
// Force field erasure. // Force field erasure.
r.kunStr.SetAnnotations(nil) r.node.SetAnnotations(nil)
return return
} }
r.kunStr.SetAnnotations(m) r.node.SetAnnotations(m)
} }
func (r *Resource) SetDataMap(m map[string]string) { func (r *Resource) SetDataMap(m map[string]string) {
r.kunStr.SetDataMap(m) r.node.SetDataMap(m)
} }
func (r *Resource) SetBinaryDataMap(m map[string]string) { func (r *Resource) SetBinaryDataMap(m map[string]string) {
r.kunStr.SetBinaryDataMap(m) r.node.SetBinaryDataMap(m)
} }
func (r *Resource) SetGvk(gvk resid.Gvk) { func (r *Resource) SetGvk(gvk resid.Gvk) {
r.kunStr.SetGvk(gvk) r.node.SetMapField(
kyaml.NewScalarRNode(gvk.Kind), kyaml.KindField)
r.node.SetMapField(
kyaml.NewScalarRNode(gvk.ApiVersion()), kyaml.APIVersionField)
} }
func (r *Resource) SetLabels(m map[string]string) { func (r *Resource) SetLabels(m map[string]string) {
if len(m) == 0 { if len(m) == 0 {
// Force field erasure. // Force field erasure.
r.kunStr.SetLabels(nil) r.node.SetLabels(nil)
return return
} }
r.kunStr.SetLabels(m) r.node.SetLabels(m)
} }
func (r *Resource) SetName(n string) { func (r *Resource) SetName(n string) {
r.kunStr.SetName(n) r.node.SetName(n)
} }
func (r *Resource) SetNamespace(n string) { func (r *Resource) SetNamespace(n string) {
r.kunStr.SetNamespace(n) r.node.SetNamespace(n)
} }
func (r *Resource) SetKind(k string) { func (r *Resource) SetKind(k string) {
@@ -173,7 +189,7 @@ func (r *Resource) SetKind(k string) {
} }
func (r *Resource) UnmarshalJSON(s []byte) error { func (r *Resource) UnmarshalJSON(s []byte) error {
return r.kunStr.UnmarshalJSON(s) return r.node.UnmarshalJSON(s)
} }
// ResCtx is an interface describing the contextual added // ResCtx is an interface describing the contextual added
@@ -193,14 +209,14 @@ type ResCtxMatcher func(ResCtx) bool
// DeepCopy returns a new copy of resource // DeepCopy returns a new copy of resource
func (r *Resource) DeepCopy() *Resource { func (r *Resource) DeepCopy() *Resource {
rc := &Resource{ rc := &Resource{
kunStr: r.Copy(), node: r.node.Copy(),
} }
rc.copyOtherFields(r) rc.copyOtherFields(r)
return rc return rc
} }
// CopyMergeMetaDataFields copies everything but the non-metadata in // CopyMergeMetaDataFields copies everything but the non-metadata in
// the ifc.Kunstructured map, merging labels and annotations. // the resource.
func (r *Resource) CopyMergeMetaDataFieldsFrom(other *Resource) { func (r *Resource) CopyMergeMetaDataFieldsFrom(other *Resource) {
r.SetLabels(mergeStringMaps(other.GetLabels(), r.GetLabels())) r.SetLabels(mergeStringMaps(other.GetLabels(), r.GetLabels()))
r.SetAnnotations( r.SetAnnotations(
@@ -266,8 +282,10 @@ func (r *Resource) ReferencesEqual(other *Resource) bool {
return len(setSelf) == len(setOther) return len(setSelf) == len(setOther)
} }
func (r *Resource) KunstructEqual(o *Resource) bool { // NodeEqual returns true if the resource's nodes are
return reflect.DeepEqual(r.kunStr, o.kunStr) // equal, ignoring ancillary information like genargs, refby, etc.
func (r *Resource) NodeEqual(o *Resource) bool {
return reflect.DeepEqual(r.node, o.node)
} }
func (r *Resource) copyRefBy() []resid.ResId { func (r *Resource) copyRefBy() []resid.ResId {
@@ -541,11 +559,7 @@ func (r *Resource) ApplySmPatch(patch *Resource) error {
if err != nil { if err != nil {
return err return err
} }
empty, err := r.IsEmpty() if r.IsEmpty() {
if err != nil {
return err
}
if empty {
return nil return nil
} }
if !patch.KindChangeAllowed() { if !patch.KindChangeAllowed() {
@@ -559,15 +573,12 @@ func (r *Resource) ApplySmPatch(patch *Resource) error {
} }
func (r *Resource) ApplyFilter(f kio.Filter) error { func (r *Resource) ApplyFilter(f kio.Filter) error {
if wn, ok := r.kunStr.(*wrappy.WNode); ok { l, err := f.Filter([]*kyaml.RNode{r.node})
l, err := f.Filter([]*kyaml.RNode{wn.AsRNode()}) if len(l) == 0 {
if len(l) == 0 { // The node was deleted. The following makes r.IsEmpty() true.
// Hack to deal with deletion. r.node = nil
r.kunStr = wrappy.NewWNode()
}
return err
} }
return filtersutil.ApplyToJSON(f, r) return err
} }
func mergeStringMaps(maps ...map[string]string) map[string]string { func mergeStringMaps(maps ...map[string]string) map[string]string {

View File

@@ -695,7 +695,7 @@ spec:
} }
} }
func TestResource_StorePreviousId(t *testing.T) { func TestResourceStorePreviousId(t *testing.T) {
tests := map[string]struct { tests := map[string]struct {
input string input string
newName 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)
}
}

View File

@@ -9,8 +9,7 @@ import (
"sigs.k8s.io/kustomize/api/resid" "sigs.k8s.io/kustomize/api/resid"
) )
// FieldSpec completely specifies a kustomizable field in // FieldSpec completely specifies a kustomizable field in a k8s API object.
// an unstructured representation of a k8s API object.
// It helps define the operands of transformations. // It helps define the operands of transformations.
// //
// For example, a directive to add a common label to objects // For example, a directive to add a common label to objects

View File

@@ -46,10 +46,7 @@ func main() {
} }
} }
resourceList.Items, err = resMap.ToRNodeSlice() resourceList.Items = resMap.ToRNodeSlice()
if err != nil {
return err
}
return nil return nil
}) })
if err := framework.Execute(&processor, nil); err != nil { if err := framework.Execute(&processor, nil); err != nil {

View File

@@ -49,8 +49,8 @@ See https://sigs.k8s.io/kustomize
completion.NewCommand(), completion.NewCommand(),
makeBuildCommand(fSys, stdOut), makeBuildCommand(fSys, stdOut),
edit.NewCmdEdit( edit.NewCmdEdit(
fSys, pvd.GetFieldValidator(), pvd.GetKunstructuredFactory()), fSys, pvd.GetFieldValidator(), pvd.GetResourceFactory()),
create.NewCmdCreate(fSys, pvd.GetKunstructuredFactory()), create.NewCmdCreate(fSys, pvd.GetResourceFactory()),
version.NewCmdVersion(stdOut), version.NewCmdVersion(stdOut),
openapi.NewCmdOpenAPI(stdOut), openapi.NewCmdOpenAPI(stdOut),
) )

View File

@@ -11,9 +11,9 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/konfig"
"sigs.k8s.io/kustomize/api/loader" "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/kustfile"
"sigs.k8s.io/kustomize/kustomize/v4/commands/internal/util" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/util"
) )
@@ -31,7 +31,7 @@ type createFlags struct {
} }
// NewCmdCreate returns an instance of 'create' subcommand. // 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} opts := createFlags{path: filesys.SelfDir}
c := &cobra.Command{ c := &cobra.Command{
Use: "create", 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- kustomize create --resources deployment.yaml,service.yaml,../base --namespace staging --nameprefix acme-
`, `,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
return runCreate(opts, fSys, uf) return runCreate(opts, fSys, rf)
}, },
} }
c.Flags().StringVar( c.Flags().StringVar(
@@ -95,7 +95,7 @@ func NewCmdCreate(fSys filesys.FileSystem, uf ifc.KunstructuredFactory) *cobra.C
return 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 resources []string
var err error var err error
if opts.resources != "" { if opts.resources != "" {
@@ -108,7 +108,7 @@ func runCreate(opts createFlags, fSys filesys.FileSystem, uf ifc.KunstructuredFa
return fmt.Errorf("kustomization file already exists") return fmt.Errorf("kustomization file already exists")
} }
if opts.detectResources { if opts.detectResources {
detected, err := detectResources(fSys, uf, opts.path, opts.detectRecursive) detected, err := detectResources(fSys, rf, opts.path, opts.detectRecursive)
if err != nil { if err != nil {
return err return err
} }
@@ -149,7 +149,7 @@ func runCreate(opts createFlags, fSys filesys.FileSystem, uf ifc.KunstructuredFa
return mf.Write(m) 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 var paths []string
err := fSys.Walk(base, func(path string, info os.FileInfo, err error) error { err := fSys.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
@@ -176,7 +176,7 @@ func detectResources(fSys filesys.FileSystem, uf ifc.KunstructuredFactory, base
if err != nil { if err != nil {
return err return err
} }
if _, err := uf.SliceFromBytes(fContents); err != nil { if _, err := rf.SliceFromBytes(fContents); err != nil {
return nil return nil
} }
paths = append(paths, path) paths = append(paths, path)

View File

@@ -13,7 +13,7 @@ import (
"sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" "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 { func readKustomizationFS(t *testing.T, fSys filesys.FileSystem) *types.Kustomization {
kf, err := kustfile.NewKustomizationFile(fSys) kf, err := kustfile.NewKustomizationFile(fSys)

View File

@@ -7,13 +7,14 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/resource"
) )
// NewCmdAdd returns an instance of 'add' subcommand. // NewCmdAdd returns an instance of 'add' subcommand.
func NewCmdAdd( func NewCmdAdd(
fSys filesys.FileSystem, fSys filesys.FileSystem,
ldr ifc.KvLoader, ldr ifc.KvLoader,
kf ifc.KunstructuredFactory) *cobra.Command { rf *resource.Factory) *cobra.Command {
c := &cobra.Command{ c := &cobra.Command{
Use: "add", Use: "add",
Short: "Adds an item to the kustomization file.", Short: "Adds an item to the kustomization file.",
@@ -53,8 +54,8 @@ func NewCmdAdd(
newCmdAddResource(fSys), newCmdAddResource(fSys),
newCmdAddPatch(fSys), newCmdAddPatch(fSys),
newCmdAddComponent(fSys), newCmdAddComponent(fSys),
newCmdAddSecret(fSys, ldr, kf), newCmdAddSecret(fSys, ldr, rf),
newCmdAddConfigMap(fSys, ldr, kf), newCmdAddConfigMap(fSys, ldr, rf),
newCmdAddBase(fSys), newCmdAddBase(fSys),
newCmdAddLabel(fSys, ldr.Validator().MakeLabelValidator()), newCmdAddLabel(fSys, ldr.Validator().MakeLabelValidator()),
newCmdAddAnnotation(fSys, ldr.Validator().MakeAnnotationValidator()), newCmdAddAnnotation(fSys, ldr.Validator().MakeAnnotationValidator()),

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile"
) )
@@ -15,7 +16,7 @@ import (
func newCmdAddConfigMap( func newCmdAddConfigMap(
fSys filesys.FileSystem, fSys filesys.FileSystem,
ldr ifc.KvLoader, ldr ifc.KvLoader,
kf ifc.KunstructuredFactory) *cobra.Command { rf *resource.Factory) *cobra.Command {
var flags flagsAndArgs var flags flagsAndArgs
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "configmap NAME [--behavior={create|merge|replace}] [--from-file=[key=]source] [--from-literal=key1=value1]", 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. // Add the flagsAndArgs map to the kustomization file.
err = addConfigMap(ldr, kustomization, flags, kf) err = addConfigMap(ldr, kustomization, flags, rf)
if err != nil { if err != nil {
return err return err
} }
@@ -106,13 +107,13 @@ func newCmdAddConfigMap(
func addConfigMap( func addConfigMap(
ldr ifc.KvLoader, ldr ifc.KvLoader,
k *types.Kustomization, k *types.Kustomization,
flags flagsAndArgs, kf ifc.KunstructuredFactory) error { flags flagsAndArgs, rf *resource.Factory) error {
args := findOrMakeConfigMapArgs(k, flags.Name) args := findOrMakeConfigMapArgs(k, flags.Name)
mergeFlagsIntoCmArgs(args, flags) mergeFlagsIntoCmArgs(args, flags)
// Validate by trying to create corev1.configmap. // Validate by trying to create corev1.configmap.
args.Options = types.MergeGlobalOptionsIntoLocal( args.Options = types.MergeGlobalOptionsIntoLocal(
args.Options, k.GeneratorOptions) args.Options, k.GeneratorOptions)
_, err := kf.MakeConfigMap(ldr, args) _, err := rf.MakeConfigMap(ldr, args)
return err return err
} }

View File

@@ -7,6 +7,7 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
"sigs.k8s.io/kustomize/api/filesys" "sigs.k8s.io/kustomize/api/filesys"
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/resource"
"sigs.k8s.io/kustomize/api/types" "sigs.k8s.io/kustomize/api/types"
"sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile" "sigs.k8s.io/kustomize/kustomize/v4/commands/internal/kustfile"
) )
@@ -15,7 +16,7 @@ import (
func newCmdAddSecret( func newCmdAddSecret(
fSys filesys.FileSystem, fSys filesys.FileSystem,
ldr ifc.KvLoader, ldr ifc.KvLoader,
kf ifc.KunstructuredFactory) *cobra.Command { rf *resource.Factory) *cobra.Command {
var flags flagsAndArgs var flags flagsAndArgs
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "secret NAME [--from-file=[key=]source] [--from-literal=key1=value1] [--type=Opaque|kubernetes.io/tls]", 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. // Add the flagsAndArgs map to the kustomization file.
err = addSecret(ldr, kustomization, flags, kf) err = addSecret(ldr, kustomization, flags, rf)
if err != nil { if err != nil {
return err return err
} }
@@ -106,13 +107,13 @@ func newCmdAddSecret(
func addSecret( func addSecret(
ldr ifc.KvLoader, ldr ifc.KvLoader,
k *types.Kustomization, k *types.Kustomization,
flags flagsAndArgs, kf ifc.KunstructuredFactory) error { flags flagsAndArgs, rf *resource.Factory) error {
args := findOrMakeSecretArgs(k, flags.Name, flags.Namespace, flags.Type) args := findOrMakeSecretArgs(k, flags.Name, flags.Namespace, flags.Type)
mergeFlagsIntoGeneratorArgs(&args.GeneratorArgs, flags) mergeFlagsIntoGeneratorArgs(&args.GeneratorArgs, flags)
// Validate by trying to create corev1.secret. // Validate by trying to create corev1.secret.
args.Options = types.MergeGlobalOptionsIntoLocal( args.Options = types.MergeGlobalOptionsIntoLocal(
args.Options, k.GeneratorOptions) args.Options, k.GeneratorOptions)
_, err := kf.MakeSecret(ldr, args) _, err := rf.MakeSecret(ldr, args)
return err return err
} }

View File

@@ -9,6 +9,7 @@ import (
"sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/ifc"
"sigs.k8s.io/kustomize/api/kv" "sigs.k8s.io/kustomize/api/kv"
"sigs.k8s.io/kustomize/api/loader" "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/add"
"sigs.k8s.io/kustomize/kustomize/v4/commands/edit/fix" "sigs.k8s.io/kustomize/kustomize/v4/commands/edit/fix"
"sigs.k8s.io/kustomize/kustomize/v4/commands/edit/listbuiltin" "sigs.k8s.io/kustomize/kustomize/v4/commands/edit/listbuiltin"
@@ -18,7 +19,7 @@ import (
// NewCmdEdit returns an instance of 'edit' subcommand. // NewCmdEdit returns an instance of 'edit' subcommand.
func NewCmdEdit( 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{ c := &cobra.Command{
Use: "edit", Use: "edit",
Short: "Edits a kustomization file", Short: "Edits a kustomization file",
@@ -40,7 +41,7 @@ func NewCmdEdit(
add.NewCmdAdd( add.NewCmdAdd(
fSys, fSys,
kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v), kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v),
kf), rf),
set.NewCmdSet( set.NewCmdSet(
fSys, fSys,
kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v), kv.NewLoader(loader.NewFileLoaderAtCwd(fSys), v),

View File

@@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
"regexp"
"strconv" "strconv"
"strings" "strings"
@@ -336,6 +337,33 @@ func (rn *RNode) SetYNode(node *yaml.Node) {
*rn.value = *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. // GetNamespace gets the metadata namespace field.
func (rn *RNode) GetNamespace() (string, error) { func (rn *RNode) GetNamespace() (string, error) {
meta, err := rn.GetMeta() meta, err := rn.GetMeta()
@@ -752,7 +780,7 @@ func (rn *RNode) GetValidatedMetadata() (ResourceMeta, error) {
return m, nil return m, nil
} }
// MatchesAnnotationSelector implements ifc.Kunstructured. // MatchesAnnotationSelector returns true on a selector match to annotations.
func (rn *RNode) MatchesAnnotationSelector(selector string) (bool, error) { func (rn *RNode) MatchesAnnotationSelector(selector string) (bool, error) {
s, err := labels.Parse(selector) s, err := labels.Parse(selector)
if err != nil { if err != nil {
@@ -765,7 +793,7 @@ func (rn *RNode) MatchesAnnotationSelector(selector string) (bool, error) {
return s.Matches(labels.Set(slice)), nil 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) { func (rn *RNode) MatchesLabelSelector(selector string) (bool, error) {
s, err := labels.Parse(selector) s, err := labels.Parse(selector)
if err != nil { if err != nil {
@@ -780,9 +808,8 @@ func (rn *RNode) MatchesLabelSelector(selector string) (bool, error) {
// HasNilEntryInList returns true if the RNode contains a list which has // HasNilEntryInList returns true if the RNode contains a list which has
// a nil item, along with the path to the missing item. // a nil item, along with the path to the missing item.
// TODO(broken): This was copied from // TODO(broken): This doesn't do what it claims to do.
// api/k8sdeps/kunstruct/factory.go//checkListItemNil // (see TODO in unit test and pr 1513).
// and doesn't do what it claims to do (see TODO in unit test and pr 1513).
func (rn *RNode) HasNilEntryInList() (bool, string) { func (rn *RNode) HasNilEntryInList() (bool, string) {
return hasNilEntryInList(rn.value) return hasNilEntryInList(rn.value)
} }
@@ -859,3 +886,123 @@ func checkKey(key string, elems []*Node) bool {
} }
return count == len(elems) 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)
}

View File

@@ -10,6 +10,7 @@ import (
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/yaml.v3"
) )
func TestRNodeHasNilEntryInList(t *testing.T) { 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)
}
}

View File

@@ -12,7 +12,7 @@ import (
) )
type plugin struct { type plugin struct {
hasher ifc.KunstructuredHasher hasher ifc.KustHasher
} }
//noinspection GoUnusedGlobalVariable //noinspection GoUnusedGlobalVariable
@@ -28,7 +28,7 @@ func (p *plugin) Config(
func (p *plugin) Transform(m resmap.ResMap) error { func (p *plugin) Transform(m resmap.ResMap) error {
for _, res := range m.Resources() { for _, res := range m.Resources() {
if res.NeedHashSuffix() { if res.NeedHashSuffix() {
h, err := p.hasher.Hash(res) h, err := res.Hash(p.hasher)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -34,20 +34,15 @@ func (p *plugin) Transform(m resmap.ResMap) error {
return nil return nil
} }
for _, r := range m.Resources() { for _, r := range m.Resources() {
empty, err := r.IsEmpty() if r.IsEmpty() {
if err != nil {
return err
}
if empty {
// Don't mutate empty objects? // Don't mutate empty objects?
continue continue
} }
r.StorePreviousId() r.StorePreviousId()
err = r.ApplyFilter(namespace.Filter{ if err := r.ApplyFilter(namespace.Filter{
Namespace: p.Namespace, Namespace: p.Namespace,
FsSlice: p.FieldSpecs, FsSlice: p.FieldSpecs,
}) }); err != nil {
if err != nil {
return err return err
} }
matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals) matches := m.GetMatchingResourcesByCurrentId(r.CurId().Equals)