Files
kustomize/kyaml/starlark/starlark.go
2020-04-02 12:35:47 -07:00

252 lines
6.6 KiB
Go

// Copyright 2019 The Kubernetes Authors.
// SPDX-License-Identifier: Apache-2.0
package starlark
import (
"fmt"
"io/ioutil"
"net/http"
"strconv"
"github.com/qri-io/starlib/util"
"go.starlark.net/starlark"
"sigs.k8s.io/kustomize/kyaml/comments"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/kio/filters"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
// Filter transforms a set of resources through the provided program
type Filter struct {
Name string
// 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) String() string {
return fmt.Sprintf("name: %v path: %v url: %v program: %v", sf.Name, sf.Path, sf.URL, sf.Program)
}
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)
if err != nil {
return nil, errors.Wrap(err)
}
// run the starlark as program as transformation function
thread := &starlark.Thread{Name: sf.Name}
ctx := &Context{
resourceList: value,
}
pd, err := ctx.predeclared()
if err != nil {
return nil, errors.Wrap(err)
}
_, err = starlark.ExecFile(thread, sf.Name, sf.Program, pd)
if err != nil {
return nil, errors.Wrap(err)
}
results, err := sf.resourceListToOutput(value, ids)
if err != nil {
return nil, errors.Wrap(err)
}
// starlark will serialize the resources sorting the fields alphabetically,
// format them to have a better ordering
return filters.FormatFilter{}.Filter(results)
}
// tuple maps an input resource to the output resource
type tuple struct {
// in is the RNode provided to the starlark program
in *yaml.RNode
// out is the RNode emitted by the starlark program with the id matching in
out *yaml.RNode
}
// inputToResourceList transforms input into a starlark.Value
func (sf *Filter) inputToResourceList(
input []*yaml.RNode) (starlark.Value, map[int]*tuple, error) {
var id int
ids := map[int]*tuple{}
// convert into a ResourceList which will be converted to a starlark dictionary
// create the ResourceList
resourceList, err := yaml.Parse(`kind: ResourceList`)
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)
}
// add the input as items, give each resource an id
for i := range input {
item := input[i]
// create an id for tracking the resource through the program
err := item.PipeE(yaml.SetAnnotation("config.k8s.io/id", fmt.Sprintf("%d", id)))
if err != nil {
return nil, nil, errors.Wrap(err)
}
ids[id] = &tuple{in: item}
id++
items.YNode().Content = append(items.YNode().Content, item.YNode())
}
// convert the ResourceList into a starlark dictionary by
// first converting it into a map[string]interface{}
value, err := nodeToValue(resourceList)
return value, ids, err
}
// resourceListToOutput converts the output of the starlark program to the filter output
func (sf *Filter) resourceListToOutput(
value starlark.Value, ids map[int]*tuple) ([]*yaml.RNode, error) {
// convert the modified resourceList back into a slice of RNodes
// by first converting to a map[string]interface{}
out, err := util.Unmarshal(value)
if err != nil {
return nil, errors.Wrap(err)
}
o := out.(map[string]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])
if err != nil {
return nil, errors.Wrap(err)
}
node, err := yaml.Parse(string(b))
if err != nil {
return nil, errors.Wrap(err)
}
// match it to an input
idS, err := node.Pipe(yaml.GetAnnotation("config.k8s.io/id"))
if err != nil {
return nil, errors.Wrap(err)
}
if idS == nil {
// no matching input -- new resource
results = append(results, node)
continue
}
id, err := strconv.Atoi(idS.YNode().Value)
if err != nil {
return nil, errors.Wrap(err)
}
if match, found := ids[id]; found {
// matching resources
match.out = node
} else {
// no matching input with the same id -- new resource
// this may be an error case, the outputs probably shouldn't have ids
// assigned by the starlark program
results = append(results, node)
}
}
// retain the comments instead of dropping them by copying them from the original inputs
for i := 0; i < len(ids); i++ {
v := ids[i]
if v.out == nil {
continue
}
if err := comments.CopyComments(v.in, v.out); err != nil {
return nil, errors.Wrap(err)
}
results = append(results, v.out)
}
// delete the ids from resources, these were only to track through the starlark program
// and that is finished.
for i := range results {
err := results[i].PipeE(yaml.ClearAnnotation("config.k8s.io/id"))
if err != nil {
return nil, err
}
}
return results, nil
}