diff --git a/api/internal/wrappy/factory.go b/api/internal/wrappy/factory.go index b03b9e8fc..4bce50602 100644 --- a/api/internal/wrappy/factory.go +++ b/api/internal/wrappy/factory.go @@ -4,26 +4,71 @@ package wrappy import ( + "bytes" + "fmt" + "sigs.k8s.io/kustomize/api/ifc" + "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/types" + "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) { - panic("TODO(#WNodeFactory): implement SliceFromBytes") + r := kio.ByteReader{OmitReaderAnnotations: true} + r.Reader = bytes.NewBuffer(bs) + yamlRNodes, err := r.Read() + 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.IgnoredByKustomizeResourceAnnotation] + return y } func (k *WNodeFactory) FromMap(m map[string]interface{}) ifc.Kunstructured { - panic("TODO(#WNodeFactory): implement FromMap") + rn, err := FromMap(m) + if err != nil { + // TODO(#WNodeFactory): handle or bubble error" + panic(err) + } + return rn } func (k *WNodeFactory) Hasher() ifc.KunstructuredHasher { diff --git a/api/internal/wrappy/factory_test.go b/api/internal/wrappy/factory_test.go index 4eea8268b..77dee3bae 100644 --- a/api/internal/wrappy/factory_test.go +++ b/api/internal/wrappy/factory_test.go @@ -2,3 +2,223 @@ // SPDX-License-Identifier: Apache-2.0 package wrappy + +import ( + "fmt" + "reflect" + "testing" +) + +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, + }, + } + + 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}, + }, + }, + } + + for n := range testCases { + tc := testCases[n] + t.Run(n, func(t *testing.T) { + rs, err := factory.SliceFromBytes(tc.input) + if tc.exp.isErr && err == nil { + t.Fatalf("%v: should return error", n) + } + if !tc.exp.isErr && err != nil { + t.Fatalf("%v: unexpected error: %s", n, err) + } + if len(tc.exp.out) != len(rs) { + fmt.Printf("%s: \nexpected:%v\nactual: %v\n", + n, tc.exp.out, rs) + t.Fatalf("%s: length mismatch; expected %d, actual %d", + n, len(tc.exp.out), len(rs)) + } + for i := range rs { + if !reflect.DeepEqual(tc.exp.out[i], rs[i].Map()) { + t.Fatalf("%s: Got: %v\nexpected:%v", + n, rs[i].Map(), tc.exp.out[i]) + } + } + }) + } +} diff --git a/api/k8sdeps/kunstruct/factory.go b/api/k8sdeps/kunstruct/factory.go index cebfa1910..ec1333565 100644 --- a/api/k8sdeps/kunstruct/factory.go +++ b/api/k8sdeps/kunstruct/factory.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/kustomize/api/ifc" "sigs.k8s.io/kustomize/api/internal/k8sdeps/configmapandsecret" + "sigs.k8s.io/kustomize/api/konfig" "sigs.k8s.io/kustomize/api/types" ) @@ -116,10 +117,6 @@ func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error return nil } -// nonKustomizableResourceAnnotation if set on a Resource will cause Kustomize to -// ignore the Resource rather than Kustomize it. -const ignoredByKustomizeResourceAnnotation = "config.kubernetes.io/local-config" - // skipResource returns true if the Resource should not be accumulated func (kf *KunstructuredFactoryImpl) skipResource(u unstructured.Unstructured) bool { an := u.GetAnnotations() @@ -128,7 +125,7 @@ func (kf *KunstructuredFactoryImpl) skipResource(u unstructured.Unstructured) bo return false } // check if the Resource has opt-ed out of kustomize - _, found := an[ignoredByKustomizeResourceAnnotation] + _, found := an[konfig.IgnoredByKustomizeResourceAnnotation] return found } diff --git a/api/konfig/general.go b/api/konfig/general.go index de0292415..02c4067ce 100644 --- a/api/konfig/general.go +++ b/api/konfig/general.go @@ -31,6 +31,9 @@ const ( // A program name, for use in help, finding the XDG_CONFIG_DIR, etc. ProgramName = "kustomize" + // If a resource has this annotation, kustomize will drop it. + IgnoredByKustomizeResourceAnnotation = "config.kubernetes.io/local-config" + // Label key that indicates the resources are built from Kustomize ManagedbyLabelKey = "app.kubernetes.io/managed-by"