diff --git a/kyaml/starlark/starlark.go b/kyaml/starlark/starlark.go index d888ebd7a..b855098dd 100644 --- a/kyaml/starlark/starlark.go +++ b/kyaml/starlark/starlark.go @@ -5,6 +5,8 @@ package starlark import ( "fmt" + "io/ioutil" + "net/http" "strconv" "github.com/qri-io/starlib/util" @@ -21,9 +23,54 @@ type Filter struct { // Program is a starlark script which will be run against the resources Program string + + // URL is the url of a starlark program to fetch and run + URL string + + // Path is the path to a starlark program to read and run + Path string + + // FunctionConfig is the value to be provided for resourceList.functionConfig as specified by + // https://github.com/kubernetes-sigs/kustomize/blob/master/cmd/config/docs/api-conventions/functions-spec.md. + FunctionConfig *yaml.RNode } func (sf *Filter) Filter(input []*yaml.RNode) ([]*yaml.RNode, error) { + if sf.URL != "" && sf.Path != "" || + sf.URL != "" && sf.Program != "" || + sf.Path != "" && sf.Program != "" { + return nil, errors.Errorf("Filter Path, Program and URL are mutually exclusive") + } + + // read the program from a file + if sf.Path != "" { + b, err := ioutil.ReadFile(sf.Path) + if err != nil { + return nil, err + } + sf.Program = string(b) + } + + // read the program from a URL + if sf.URL != "" { + err := func() error { + resp, err := http.Get(sf.URL) + if err != nil { + return err + } + defer resp.Body.Close() + b, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + sf.Program = string(b) + return nil + }() + if err != nil { + return nil, err + } + } + // retain map of inputs to outputs by id so if the name is changed by the // starlark program, we are able to match the same resources value, ids, err := sf.inputToResourceList(input) @@ -69,6 +116,16 @@ func (sf *Filter) inputToResourceList( if err != nil { return nil, nil, errors.Wrap(err) } + + // set the functionConfig + if sf.FunctionConfig != nil { + if err := resourceList.PipeE( + yaml.FieldSetter{Name: "functionConfig", Value: sf.FunctionConfig}); err != nil { + return nil, nil, err + } + } + + // the inputs should be provided as the list "items" items, err := resourceList.Pipe(yaml.LookupCreate(yaml.SequenceNode, "items")) if err != nil { return nil, nil, errors.Wrap(err) @@ -115,10 +172,24 @@ func (sf *Filter) resourceListToOutput( return nil, errors.Wrap(err) } o := out.(map[string]interface{}) - it := (o["items"].([]interface{})) + // parse the function config + if _, found := o["functionConfig"]; found { + fc := (o["functionConfig"].(map[string]interface{})) + b, err := yaml.Marshal(fc) + if err != nil { + return nil, errors.Wrap(err) + } + sf.FunctionConfig, err = yaml.Parse(string(b)) + if err != nil { + return nil, errors.Wrap(err) + } + } + + // parse the items // copy the items out of the ResourceList, and into the Filter output var results []*yaml.RNode + it := (o["items"].([]interface{})) for i := range it { // convert the resource back to the native yaml form b, err := yaml.Marshal(it[i]) diff --git a/kyaml/starlark/starlark_test.go b/kyaml/starlark/starlark_test.go index cb761aeb2..20abc7e22 100644 --- a/kyaml/starlark/starlark_test.go +++ b/kyaml/starlark/starlark_test.go @@ -5,19 +5,25 @@ package starlark import ( "bytes" + "fmt" + "os" "strings" "testing" + "github.com/go-errors/errors" "github.com/stretchr/testify/assert" "sigs.k8s.io/kustomize/kyaml/kio" + "sigs.k8s.io/kustomize/kyaml/yaml" ) func TestStarlarkFilter_Filter(t *testing.T) { var tests = []struct { - name string - input string - script string - expected string + name string + input string + functionConfig string + script string + expected string + expectedFunctionConfig string }{ { name: "add_annotation", @@ -234,16 +240,127 @@ apiVersion: apps/v1 kind: Deployment metadata: name: nginx-deployment-1 +`, + }, + { + name: "functionConfig", + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + template: + spec: + containers: + - name: nginx + # head comment + image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} +`, + functionConfig: ` +kind: Script +spec: + value: "hello world" +`, + script: ` +# set the foo annotation on each resource +def run(r, an): + for resource in r: + resource["metadata"]["annotations"]["foo"] = an + +an = resourceList["functionConfig"]["spec"]["value"] +run(resourceList["items"], an) +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + foo: hello world +spec: + template: + spec: + containers: + - name: nginx + # head comment + image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} +`, + expectedFunctionConfig: ` +kind: Script +spec: + value: hello world +`, + }, + + { + name: "functionConfig_update_functionConfig", + input: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment +spec: + template: + spec: + containers: + - name: nginx + # head comment + image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} +`, + functionConfig: ` +kind: Script +spec: + value: "hello world" +`, + script: ` +# set the foo annotation on each resource +def run(r, an): + for resource in r: + resource["metadata"]["annotations"]["foo"] = an + +an = resourceList["functionConfig"]["spec"]["value"] +run(resourceList["items"], an) +resourceList["functionConfig"]["spec"]["value"] = "updated" +`, + expected: ` +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + annotations: + foo: hello world +spec: + template: + spec: + containers: + - name: nginx + # head comment + image: nginx:1.8.1 # {"$ref": "#/definitions/io.k8s.cli.substitutions.image"} +`, + expectedFunctionConfig: ` +kind: Script +spec: + value: updated `, }, } for i := range tests { test := tests[i] t.Run(test.name, func(t *testing.T) { + f := &Filter{Name: test.name, Program: test.script} + + if test.functionConfig != "" { + fc, err := yaml.Parse(test.functionConfig) + if !assert.NoError(t, err) { + t.FailNow() + } + f.FunctionConfig = fc + } + r := &kio.ByteReader{Reader: bytes.NewBufferString(test.input)} o := &bytes.Buffer{} w := &kio.ByteWriter{Writer: o} - f := &Filter{Name: test.name, Program: test.script} p := kio.Pipeline{ Inputs: []kio.Reader{r}, Filters: []kio.Filter{f}, @@ -251,11 +368,22 @@ metadata: } err := p.Execute() if !assert.NoError(t, err) { + if e, ok := err.(*errors.Error); ok { + fmt.Fprintf(os.Stderr, "%s\n", e.Stack()) + } t.FailNow() } if !assert.Equal(t, strings.TrimSpace(test.expected), strings.TrimSpace(o.String())) { t.FailNow() } + + if test.expectedFunctionConfig != "" { + if !assert.Equal(t, + strings.TrimSpace(test.expectedFunctionConfig), + strings.TrimSpace(f.FunctionConfig.MustString())) { + t.FailNow() + } + } }) } }