diff --git a/k8sdeps/kunstruct/factory.go b/k8sdeps/kunstruct/factory.go index de058f07c..386789130 100644 --- a/k8sdeps/kunstruct/factory.go +++ b/k8sdeps/kunstruct/factory.go @@ -101,12 +101,16 @@ func (kf *KunstructuredFactoryImpl) Set(fs fs.FileSystem, ldr ifc.Loader) { } // validate validates that u has kind and name +// except for kind `List`, which doesn't require a name func (kf *KunstructuredFactoryImpl) validate(u unstructured.Unstructured) error { + kind := u.GetKind() + if kind == "" { + return fmt.Errorf("missing kind in object %v", u) + } else if kind == "List" { + return nil + } if u.GetName() == "" { return fmt.Errorf("missing metadata.name in object %v", u) } - if u.GetKind() == "" { - return fmt.Errorf("missing kind in object %v", u) - } return nil } diff --git a/k8sdeps/kunstruct/factory_test.go b/k8sdeps/kunstruct/factory_test.go index 24dbb8a25..2ef4acaa9 100644 --- a/k8sdeps/kunstruct/factory_test.go +++ b/k8sdeps/kunstruct/factory_test.go @@ -33,6 +33,15 @@ func TestSliceFromBytes(t *testing.T) { "name": "winnie", }, }) + testList := factory.FromMap( + map[string]interface{}{ + "apiVersion": "v1", + "kind": "List", + "items": []interface{}{ + testConfigMap.Map(), + testConfigMap.Map(), + }, + }) tests := []struct { name string @@ -112,6 +121,24 @@ metadata: expectedOut: nil, expectedErr: true, }, + { + name: "List", + input: []byte(` +apiVersion: v1 +kind: List +items: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie +`), + expectedOut: []ifc.Kunstructured{testList}, + expectedErr: false, + }, } for _, test := range tests { diff --git a/pkg/resource/factory.go b/pkg/resource/factory.go index 2d9da9c66..3c0dbab07 100644 --- a/pkg/resource/factory.go +++ b/pkg/resource/factory.go @@ -17,6 +17,8 @@ limitations under the License. package resource import ( + "encoding/json" + "fmt" "log" "sigs.k8s.io/kustomize/pkg/fs" @@ -78,8 +80,30 @@ func (rf *Factory) SliceFromBytes(in []byte) ([]*Resource, error) { return nil, err } var result []*Resource - for _, u := range kunStructs { - result = append(result, rf.FromKunstructured(u)) + for len(kunStructs) > 0 { + u := kunStructs[0] + kunStructs = kunStructs[1:] + if u.GetKind() == "List" { + items := u.Map()["items"] + itemsSlice, ok := items.([]interface{}) + if !ok { + return nil, fmt.Errorf("items in List is type %T, expected array.", items) + } + for _, item := range itemsSlice { + itemJSON, err := json.Marshal(item) + if err != nil { + return nil, err + } + innerU, err := rf.kf.SliceFromBytes(itemJSON) + if err != nil { + return nil, err + } + // append innerU to kunStructs so nested Lists can be handled + kunStructs = append(kunStructs, innerU...) + } + } else { + result = append(result, rf.FromKunstructured(u)) + } } return result, nil } diff --git a/pkg/resource/factory_test.go b/pkg/resource/factory_test.go index edb999c39..2fb0adeb8 100644 --- a/pkg/resource/factory_test.go +++ b/pkg/resource/factory_test.go @@ -49,10 +49,82 @@ metadata: patch3 := ` WOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOT: woot ` + patchList := patch.StrategicMerge("patch4.yaml") + patch4 := ` +apiVersion: v1 +kind: List +items: +- apiVersion: apps/v1 + kind: Deployment + metadata: + name: pooh +- apiVersion: v1 + kind: ConfigMap + metadata: + name: winnie + namespace: hundred-acre-wood +` + patchList2 := patch.StrategicMerge("patch5.yaml") + patch5 := ` +apiVersion: v1 +kind: List +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 +` + 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 := factory.FromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deployment-a", + }, + "spec": testDeploymentSpec, + }) + testDeploymentB := factory.FromMap( + map[string]interface{}{ + "apiVersion": "apps/v1", + "kind": "Deployment", + "metadata": map[string]interface{}{ + "name": "deployment-b", + }, + "spec": testDeploymentSpec, + }) l := loadertest.NewFakeLoader("/") l.AddFile("/"+string(patchGood1), []byte(patch1)) l.AddFile("/"+string(patchGood2), []byte(patch2)) l.AddFile("/"+string(patchBad), []byte(patch3)) + l.AddFile("/"+string(patchList), []byte(patch4)) + l.AddFile("/"+string(patchList2), []byte(patch5)) tests := []struct { name string @@ -78,6 +150,18 @@ WOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOT: woot expectedOut: []*Resource{}, expectedErr: true, }, + { + name: "listOfPatches", + input: []patch.StrategicMerge{patchList}, + expectedOut: []*Resource{testDeployment, testConfigMap}, + expectedErr: false, + }, + { + name: "listWithAnchorReference", + input: []patch.StrategicMerge{patchList2}, + expectedOut: []*Resource{testDeploymentA, testDeploymentB}, + expectedErr: false, + }, } for _, test := range tests { rs, err := factory.SliceFromPatches(l, test.input)