diff --git a/api/go.mod b/api/go.mod index 29330cf62..4ec567f88 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,6 +6,7 @@ require ( github.com/evanphx/json-patch v4.5.0+incompatible github.com/go-openapi/spec v0.19.5 github.com/golangci/golangci-lint v1.21.0 + github.com/google/go-cmp v0.3.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/go-multierror v1.1.0 github.com/pkg/errors v0.8.1 diff --git a/api/internal/wrappy/factory_test.go b/api/internal/wrappy/factory_test.go index 7db433cb6..4eea8268b 100644 --- a/api/internal/wrappy/factory_test.go +++ b/api/internal/wrappy/factory_test.go @@ -1,4 +1,4 @@ // Copyright 2020 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package wrappy_test +package wrappy diff --git a/api/internal/wrappy/wnode.go b/api/internal/wrappy/wnode.go index be3bb8485..0989dbc09 100644 --- a/api/internal/wrappy/wnode.go +++ b/api/internal/wrappy/wnode.go @@ -4,7 +4,9 @@ package wrappy import ( + "fmt" "log" + "strings" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/resid" @@ -54,10 +56,41 @@ func (wn *WNode) GetAnnotations() map[string]string { // GetFieldValue implements ifc.Kunstructured. func (wn *WNode) GetFieldValue(path string) (interface{}, error) { - // The argument is a json path, e.g. "metadata.name" - // fields := strings.Split(path, ".") - // return wn.node.Pipe(yaml.Lookup(fields...)) - panic("TODO(#WNode): GetFieldValue; implement or drop from API") + fields := 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{} + for _, node := range yn.Content { + result = append(result, node.Value) + } + return result, nil + } + + // Return value value directly for all other (ScalarNode) kinds + return yn.Value, nil } // GetGvk implements ifc.Kunstructured. @@ -83,18 +116,37 @@ func (wn *WNode) GetName() string { } // GetSlice implements ifc.Kunstructured. -func (wn *WNode) GetSlice(string) ([]interface{}, error) { - panic("TODO(#WNode) GetSlice; implement or drop from API") +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(string) (string, error) { - panic("TODO(#WNode) GetString; implement or drop from API") +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{} { - panic("TODO(#WNode) Map; implement or drop from API") + var result map[string]interface{} + if err := wn.node.YNode().Decode(&result); err != nil { + // Log and die since interface doesn't allow error. + log.Fatalf("failed to decode ynode: %v", err) + } + return result } // MarshalJSON implements ifc.Kunstructured. @@ -113,31 +165,51 @@ func (wn *WNode) MatchesLabelSelector(string) (bool, error) { } // SetAnnotations implements ifc.Kunstructured. -func (wn *WNode) SetAnnotations(map[string]string) { - panic("TODO(#WNode) SetAnnotations; implement or drop from API") +func (wn *WNode) SetAnnotations(annotations map[string]string) { + wn.setField(yaml.NewMapRNode(&annotations), yaml.MetadataField, yaml.AnnotationsField) } // SetGvk implements ifc.Kunstructured. -func (wn *WNode) SetGvk(resid.Gvk) { - panic("TODO(#WNode) SetGvk; implement or drop from API") +func (wn *WNode) SetGvk(gvk resid.Gvk) { + wn.setField(yaml.NewScalarRNode(gvk.Kind), yaml.KindField) + wn.setField(yaml.NewScalarRNode(fmt.Sprintf("%s/%s", gvk.Group, gvk.Version)), yaml.APIVersionField) } // SetLabels implements ifc.Kunstructured. -func (wn *WNode) SetLabels(map[string]string) { - panic("TODO(#WNode) SetLabels; implement or drop from API") +func (wn *WNode) SetLabels(labels map[string]string) { + wn.setField(yaml.NewMapRNode(&labels), yaml.MetadataField, yaml.LabelsField) } // SetName implements ifc.Kunstructured. -func (wn *WNode) SetName(string) { - panic("TODO(#WNode) SetName; implement or drop from API") +func (wn *WNode) SetName(name string) { + wn.setField(yaml.NewScalarRNode(name), yaml.MetadataField, yaml.NameField) } // SetNamespace implements ifc.Kunstructured. -func (wn *WNode) SetNamespace(string) { - panic("TODO(#WNode) SetNamespace; implement or drop from API") +func (wn *WNode) SetNamespace(ns string) { + wn.setField(yaml.NewScalarRNode(ns), yaml.MetadataField, yaml.NamespaceField) +} + +func (wn *WNode) setField(value *yaml.RNode, path ...string) { + err := wn.node.PipeE( + yaml.LookupCreate(yaml.MappingNode, path[0:len(path)-1]...), + yaml.SetField(path[len(path)-1], value), + ) + if err != nil { + // Log and die since interface doesn't allow error. + log.Fatalf("failed to set field %v: %v", path, err) + } } // UnmarshalJSON implements ifc.Kunstructured. func (wn *WNode) UnmarshalJSON(data []byte) error { return wn.node.UnmarshalJSON(data) } + +type NoFieldError struct { + Field string +} + +func (e NoFieldError) Error() string { + return fmt.Sprintf("no field named '%s'", e.Field) +} diff --git a/api/internal/wrappy/wnode_test.go b/api/internal/wrappy/wnode_test.go index 072373a23..379c8b7d3 100644 --- a/api/internal/wrappy/wnode_test.go +++ b/api/internal/wrappy/wnode_test.go @@ -1,14 +1,16 @@ // Copyright 2019 The Kubernetes Authors. // SPDX-License-Identifier: Apache-2.0 -package wrappy_test +package wrappy import ( "strings" "testing" + "github.com/google/go-cmp/cmp" + "sigs.k8s.io/kustomize/api/resid" + "gopkg.in/yaml.v3" - . "sigs.k8s.io/kustomize/api/internal/wrappy" kyaml "sigs.k8s.io/kustomize/kyaml/yaml" ) @@ -337,3 +339,215 @@ func TestGettingFields(t *testing.T) { 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 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 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 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 := wn.Map() + 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) + } +}