starlark fn support for functionConfig input

This commit is contained in:
Phillip Wittrock
2020-03-09 09:52:07 -07:00
parent 5364b2198a
commit 68a9389bfe
2 changed files with 205 additions and 6 deletions

View File

@@ -5,6 +5,8 @@ package starlark
import ( import (
"fmt" "fmt"
"io/ioutil"
"net/http"
"strconv" "strconv"
"github.com/qri-io/starlib/util" "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 is a starlark script which will be run against the resources
Program string 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) { 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 // 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 // starlark program, we are able to match the same resources
value, ids, err := sf.inputToResourceList(input) value, ids, err := sf.inputToResourceList(input)
@@ -69,6 +116,16 @@ func (sf *Filter) inputToResourceList(
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err) 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")) items, err := resourceList.Pipe(yaml.LookupCreate(yaml.SequenceNode, "items"))
if err != nil { if err != nil {
return nil, nil, errors.Wrap(err) return nil, nil, errors.Wrap(err)
@@ -115,10 +172,24 @@ func (sf *Filter) resourceListToOutput(
return nil, errors.Wrap(err) return nil, errors.Wrap(err)
} }
o := out.(map[string]interface{}) 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 // copy the items out of the ResourceList, and into the Filter output
var results []*yaml.RNode var results []*yaml.RNode
it := (o["items"].([]interface{}))
for i := range it { for i := range it {
// convert the resource back to the native yaml form // convert the resource back to the native yaml form
b, err := yaml.Marshal(it[i]) b, err := yaml.Marshal(it[i])

View File

@@ -5,19 +5,25 @@ package starlark
import ( import (
"bytes" "bytes"
"fmt"
"os"
"strings" "strings"
"testing" "testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/kio" "sigs.k8s.io/kustomize/kyaml/kio"
"sigs.k8s.io/kustomize/kyaml/yaml"
) )
func TestStarlarkFilter_Filter(t *testing.T) { func TestStarlarkFilter_Filter(t *testing.T) {
var tests = []struct { var tests = []struct {
name string name string
input string input string
script string functionConfig string
expected string script string
expected string
expectedFunctionConfig string
}{ }{
{ {
name: "add_annotation", name: "add_annotation",
@@ -234,16 +240,127 @@ apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: nginx-deployment-1 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 { for i := range tests {
test := tests[i] test := tests[i]
t.Run(test.name, func(t *testing.T) { 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)} r := &kio.ByteReader{Reader: bytes.NewBufferString(test.input)}
o := &bytes.Buffer{} o := &bytes.Buffer{}
w := &kio.ByteWriter{Writer: o} w := &kio.ByteWriter{Writer: o}
f := &Filter{Name: test.name, Program: test.script}
p := kio.Pipeline{ p := kio.Pipeline{
Inputs: []kio.Reader{r}, Inputs: []kio.Reader{r},
Filters: []kio.Filter{f}, Filters: []kio.Filter{f},
@@ -251,11 +368,22 @@ metadata:
} }
err := p.Execute() err := p.Execute()
if !assert.NoError(t, err) { if !assert.NoError(t, err) {
if e, ok := err.(*errors.Error); ok {
fmt.Fprintf(os.Stderr, "%s\n", e.Stack())
}
t.FailNow() t.FailNow()
} }
if !assert.Equal(t, strings.TrimSpace(test.expected), strings.TrimSpace(o.String())) { if !assert.Equal(t, strings.TrimSpace(test.expected), strings.TrimSpace(o.String())) {
t.FailNow() t.FailNow()
} }
if test.expectedFunctionConfig != "" {
if !assert.Equal(t,
strings.TrimSpace(test.expectedFunctionConfig),
strings.TrimSpace(f.FunctionConfig.MustString())) {
t.FailNow()
}
}
}) })
} }
} }