mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-06-11 17:12:51 +00:00
config run: support for RunFns.Functions and RunFns.Input
- Support specifying RunFns.Functions using the `-i` flag to specify an image - Parse the function config from key-value arguments specified after ` -- ` - Support reading from stdin / writing to stdout if no arguments are provided - Table driven tests for parsing flags and args into RunFns structure
This commit is contained in:
@@ -4,22 +4,27 @@
|
||||
package commands
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"sigs.k8s.io/kustomize/cmd/config/internal/generateddocs/commands"
|
||||
"sigs.k8s.io/kustomize/kyaml/errors"
|
||||
"sigs.k8s.io/kustomize/kyaml/runfn"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// GetCatRunner returns a RunFnRunner.
|
||||
func GetRunFnRunner(name string) *RunFnRunner {
|
||||
r := &RunFnRunner{}
|
||||
c := &cobra.Command{
|
||||
Use: "run DIR",
|
||||
Aliases: []string{"run-fns"},
|
||||
Use: "run [DIR]",
|
||||
Short: commands.RunFnsShort,
|
||||
Long: commands.RunFnsLong,
|
||||
Example: commands.RunFnsExamples,
|
||||
RunE: r.runE,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PreRunE: r.preRunE,
|
||||
}
|
||||
fixDocs(name, c)
|
||||
c.Flags().BoolVar(&r.IncludeSubpackages, "include-subpackages", true,
|
||||
@@ -29,11 +34,12 @@ func GetRunFnRunner(name string) *RunFnRunner {
|
||||
&r.DryRun, "dry-run", false, "print results to stdout")
|
||||
r.Command.Flags().BoolVar(
|
||||
&r.GlobalScope, "global-scope", false, "set global scope for functions.")
|
||||
r.Command.Flags().StringSliceVar(
|
||||
&r.FnPaths, "fn-path", []string{},
|
||||
"directories containing functions without configuration")
|
||||
r.Command.AddCommand(XArgsCommand())
|
||||
r.Command.AddCommand(WrapCommand())
|
||||
r.Command.Flags().StringSliceVarP(
|
||||
&r.FnPaths, "fn-path", "p", []string{},
|
||||
"read functions from these directories instead of the configuration directory.")
|
||||
r.Command.Flags().StringVarP(
|
||||
&r.Image, "image", "i", "",
|
||||
"run this image as a function instead of discovering them.")
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -48,12 +54,142 @@ type RunFnRunner struct {
|
||||
DryRun bool
|
||||
GlobalScope bool
|
||||
FnPaths []string
|
||||
Image string
|
||||
RunFns runfn.RunFns
|
||||
}
|
||||
|
||||
func (r *RunFnRunner) runE(c *cobra.Command, args []string) error {
|
||||
rec := runfn.RunFns{Path: args[0], FunctionPaths: r.FnPaths, GlobalScope: r.GlobalScope}
|
||||
if r.DryRun {
|
||||
rec.Output = c.OutOrStdout()
|
||||
}
|
||||
return handleError(c, rec.Execute())
|
||||
return handleError(c, r.RunFns.Execute())
|
||||
}
|
||||
|
||||
// getFunctions parses the commandline flags and arguments into explicit
|
||||
// Functions to run.
|
||||
func (r *RunFnRunner) getFunctions(c *cobra.Command, args, dataItems []string) (
|
||||
[]*yaml.RNode, error) {
|
||||
// if image isn't specified, then Functions is empty
|
||||
if r.Image == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// create the function spec to set as an annotation
|
||||
fn, err := yaml.Parse(`container: {}`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// TODO: add support network, volumes, etc based on flag values
|
||||
err = fn.PipeE(
|
||||
yaml.Lookup("container"),
|
||||
yaml.SetField("image", yaml.NewScalarRNode(r.Image)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create the function config
|
||||
rc, err := yaml.Parse(`
|
||||
metadata:
|
||||
name: function-input
|
||||
data: {}
|
||||
`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// set the function annotation on the function config so it
|
||||
// is parsed by RunFns
|
||||
value, err := fn.String()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rc.PipeE(
|
||||
yaml.LookupCreate(yaml.MappingNode, "metadata", "annotations"),
|
||||
yaml.SetField("config.kubernetes.io/function", yaml.NewScalarRNode(value)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// default the function config kind to ConfigMap, this may be overridden
|
||||
var kind = "ConfigMap"
|
||||
var version = "v1"
|
||||
|
||||
// populate the function config with data. this is a convention for functions
|
||||
// to be more commandline friendly
|
||||
if len(dataItems) > 0 {
|
||||
dataField, err := rc.Pipe(yaml.Lookup("data"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, s := range dataItems {
|
||||
kv := strings.SplitN(s, "=", 2)
|
||||
if i == 0 && len(kv) == 1 {
|
||||
// first argument may be the kind
|
||||
kind = s
|
||||
continue
|
||||
}
|
||||
if len(kv) != 2 {
|
||||
return nil, fmt.Errorf("args must have keys and values separated by =")
|
||||
}
|
||||
err := dataField.PipeE(yaml.SetField(kv[0], yaml.NewScalarRNode(kv[1])))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
err = rc.PipeE(yaml.SetField("kind", yaml.NewScalarRNode(kind)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rc.PipeE(yaml.SetField("apiVersion", yaml.NewScalarRNode(version)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []*yaml.RNode{rc}, nil
|
||||
}
|
||||
|
||||
func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error {
|
||||
if c.ArgsLenAtDash() >= 0 && r.Image == "" {
|
||||
return errors.Errorf("must specify --image")
|
||||
}
|
||||
|
||||
var dataItems []string
|
||||
if c.ArgsLenAtDash() >= 0 {
|
||||
dataItems = args[c.ArgsLenAtDash():]
|
||||
args = args[:c.ArgsLenAtDash()]
|
||||
}
|
||||
if len(args) > 1 {
|
||||
return errors.Errorf("0 or 1 arguments supported, function arguments go after '--'")
|
||||
}
|
||||
|
||||
fns, err := r.getFunctions(c, args, dataItems)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the output to stdout if in dry-run mode or no arguments are specified
|
||||
var output io.Writer
|
||||
var input io.Reader
|
||||
if len(args) == 0 {
|
||||
output = c.OutOrStdout()
|
||||
input = c.InOrStdin()
|
||||
} else if r.DryRun {
|
||||
output = c.OutOrStdout()
|
||||
}
|
||||
|
||||
// set the path if specified as an argument
|
||||
var path string
|
||||
if len(args) == 1 {
|
||||
// argument is the directory
|
||||
path = args[0]
|
||||
}
|
||||
|
||||
r.RunFns = runfn.RunFns{
|
||||
FunctionPaths: r.FnPaths,
|
||||
GlobalScope: r.GlobalScope,
|
||||
Functions: fns,
|
||||
Output: output,
|
||||
Input: input,
|
||||
Path: path,
|
||||
}
|
||||
|
||||
// don't consider args for the function
|
||||
return nil
|
||||
}
|
||||
|
||||
232
cmd/config/internal/commands/run_test.go
Normal file
232
cmd/config/internal/commands/run_test.go
Normal file
@@ -0,0 +1,232 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestRunFnCommand_preRunE verifies that preRunE correctly parses the commandline
|
||||
// flags and arguments into the RunFns structure to be executed.
|
||||
func TestRunFnCommand_preRunE(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
expected string
|
||||
err string
|
||||
path string
|
||||
input io.Reader
|
||||
output io.Writer
|
||||
functionPaths []string
|
||||
}{
|
||||
{
|
||||
name: "config map",
|
||||
args: []string{"run", "dir", "--image", "foo:bar", "--", "a=b", "c=d", "e=f"},
|
||||
path: "dir",
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {a: b, c: d, e: f}
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "config map stdin / stdout",
|
||||
args: []string{"run", "--image", "foo:bar", "--", "a=b", "c=d", "e=f"},
|
||||
input: os.Stdin,
|
||||
output: os.Stdout,
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {a: b, c: d, e: f}
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "config map dry-run",
|
||||
args: []string{"run", "dir", "--image", "foo:bar", "--dry-run", "--", "a=b", "c=d", "e=f"},
|
||||
output: os.Stdout,
|
||||
path: "dir",
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {a: b, c: d, e: f}
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "config map no args",
|
||||
args: []string{"run", "dir", "--image", "foo:bar"},
|
||||
path: "dir",
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {}
|
||||
kind: ConfigMap
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "custom kind",
|
||||
args: []string{"run", "dir", "-i", "foo:bar", "--", "Foo", "g=h"},
|
||||
path: "dir",
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {g: h}
|
||||
kind: Foo
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "custom kind '=' in data",
|
||||
args: []string{"run", "dir", "-i", "foo:bar", "--", "Foo", "g=h", "i=j=k"},
|
||||
path: "dir",
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {g: h, i: j=k}
|
||||
kind: Foo
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "function paths",
|
||||
args: []string{"run", "dir", "-p", "path1", "--fn-path", "path2"},
|
||||
path: "dir",
|
||||
functionPaths: []string{"path1", "path2"},
|
||||
},
|
||||
{
|
||||
name: "custom kind with function paths",
|
||||
args: []string{
|
||||
"run", "dir", "-p", "path", "-i", "foo:bar", "--", "Foo", "g=h", "i=j=k"},
|
||||
path: "dir",
|
||||
functionPaths: []string{"path"},
|
||||
expected: `
|
||||
metadata:
|
||||
name: function-input
|
||||
annotations:
|
||||
config.kubernetes.io/function: |
|
||||
container: {image: 'foo:bar'}
|
||||
data: {g: h, i: j=k}
|
||||
kind: Foo
|
||||
apiVersion: v1
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "config map multi args",
|
||||
args: []string{"run", "dir", "dir2", "--image", "foo:bar", "--", "a=b", "c=d", "e=f"},
|
||||
err: "0 or 1 arguments supported",
|
||||
},
|
||||
{
|
||||
name: "config map not image",
|
||||
args: []string{"run", "dir", "--", "a=b", "c=d", "e=f"},
|
||||
err: "must specify --image",
|
||||
},
|
||||
{
|
||||
name: "config map bad data",
|
||||
args: []string{"run", "dir", "--image", "foo:bar", "--", "a=b", "c", "e=f"},
|
||||
err: "must have keys and values separated by",
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := GetRunFnRunner("kustomize")
|
||||
// Don't run the actual command
|
||||
r.Command.Run = nil
|
||||
r.Command.RunE = func(cmd *cobra.Command, args []string) error { return nil }
|
||||
r.Command.SilenceErrors = true
|
||||
r.Command.SilenceUsage = true
|
||||
|
||||
// hack due to https://github.com/spf13/cobra/issues/42
|
||||
root := &cobra.Command{Use: "root"}
|
||||
root.AddCommand(r.Command)
|
||||
root.SetArgs(tt.args)
|
||||
|
||||
// error case
|
||||
err := r.Command.Execute()
|
||||
if tt.err != "" {
|
||||
if !assert.Error(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Contains(t, err.Error(), tt.err) {
|
||||
t.FailNow()
|
||||
}
|
||||
// don't check anything else in error case
|
||||
return
|
||||
}
|
||||
|
||||
// non-error case
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// check if Input was set
|
||||
if !assert.Equal(t, tt.input, r.RunFns.Input) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// check if Output was set
|
||||
if !assert.Equal(t, tt.output, r.RunFns.Output) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// check if Path was set
|
||||
if !assert.Equal(t, tt.path, r.RunFns.Path) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// check if FunctionPaths were set
|
||||
if tt.functionPaths == nil {
|
||||
// make Equal work against flag default
|
||||
tt.functionPaths = []string{}
|
||||
}
|
||||
if !assert.Equal(t, tt.functionPaths, r.RunFns.FunctionPaths) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
// check if Functions were set
|
||||
if tt.expected != "" {
|
||||
if !assert.Len(t, r.RunFns.Functions, 1) {
|
||||
t.FailNow()
|
||||
}
|
||||
actual := strings.TrimSpace(r.RunFns.Functions[0].MustString())
|
||||
if !assert.Equal(t, strings.TrimSpace(tt.expected), actual) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user