diff --git a/cmd/config/internal/commands/run-fns.go b/cmd/config/internal/commands/run-fns.go index 55b5ed6bf..c33e2f59f 100644 --- a/cmd/config/internal/commands/run-fns.go +++ b/cmd/config/internal/commands/run-fns.go @@ -40,6 +40,10 @@ func GetRunFnRunner(name string) *RunFnRunner { r.Command.Flags().StringVar( &r.Image, "image", "", "run this image as a function instead of discovering them.") + r.Command.Flags().BoolVar( + &r.Network, "network", false, "enable network access for functions that declare it") + r.Command.Flags().StringVar( + &r.NetworkName, "network-name", "bridge", "the docker network to run the container in") return r } @@ -56,6 +60,8 @@ type RunFnRunner struct { FnPaths []string Image string RunFns runfn.RunFns + Network bool + NetworkName string } func (r *RunFnRunner) runE(c *cobra.Command, args []string) error { @@ -188,6 +194,8 @@ func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error { Output: output, Input: input, Path: path, + Network: r.Network, + NetworkName: r.NetworkName, } // don't consider args for the function diff --git a/cmd/config/internal/commands/run_test.go b/cmd/config/internal/commands/run_test.go index e9ae8b9bc..12dcc5f8b 100644 --- a/cmd/config/internal/commands/run_test.go +++ b/cmd/config/internal/commands/run_test.go @@ -25,6 +25,8 @@ func TestRunFnCommand_preRunE(t *testing.T) { input io.Reader output io.Writer functionPaths []string + network bool + networkName string }{ { name: "config map", @@ -86,6 +88,40 @@ metadata: data: {} kind: ConfigMap apiVersion: v1 +`, + }, + { + name: "network enabled", + args: []string{"run", "dir", "--image", "foo:bar", "--network"}, + path: "dir", + network: true, + networkName: "bridge", + expected: ` +metadata: + name: function-input + annotations: + config.kubernetes.io/function: | + container: {image: 'foo:bar'} +data: {} +kind: ConfigMap +apiVersion: v1 +`, + }, + { + name: "with network name", + args: []string{"run", "dir", "--image", "foo:bar", "--network", "--network-name", "foo"}, + path: "dir", + network: true, + networkName: "foo", + expected: ` +metadata: + name: function-input + annotations: + config.kubernetes.io/function: | + container: {image: 'foo:bar'} +data: {} +kind: ConfigMap +apiVersion: v1 `, }, { @@ -206,6 +242,20 @@ apiVersion: v1 t.FailNow() } + // check if Network was set + if tt.network { + if !assert.Equal(t, tt.network, r.RunFns.Network) { + t.FailNow() + } + if !assert.Equal(t, tt.networkName, r.RunFns.NetworkName) { + t.FailNow() + } + } else { + if !assert.Equal(t, false, r.RunFns.Network) { + t.FailNow() + } + } + // check if FunctionPaths were set if tt.functionPaths == nil { // make Equal work against flag default diff --git a/kyaml/kio/filters/container.go b/kyaml/kio/filters/container.go index 0ef509078..c05a7a342 100644 --- a/kyaml/kio/filters/container.go +++ b/kyaml/kio/filters/container.go @@ -408,6 +408,17 @@ const ( var functionAnnotationKeys = []string{FunctionAnnotationKey, oldFunctionAnnotationKey} +// GetFunction parses the config function from the object if it is found +func GetFunction(n *yaml.RNode, meta yaml.ResourceMeta) (*yaml.RNode, error) { + for _, s := range functionAnnotationKeys { + fn := meta.Annotations[s] + if fn != "" { + return yaml.Parse(fn) + } + } + return n.Pipe(yaml.Lookup("metadata", "configFn")) +} + // GetContainerName returns the container image for an API if one exists func GetContainerName(n *yaml.RNode) (string, string) { meta, _ := n.GetMeta() @@ -415,14 +426,10 @@ func GetContainerName(n *yaml.RNode) (string, string) { // path to the function, this will be mounted into the container path := meta.Annotations[kioutil.PathAnnotation] - // check previous keys for backwards compatibility - for _, s := range functionAnnotationKeys { - functionAnnotation := meta.Annotations[s] - if functionAnnotation != "" { - annotationContent, _ := yaml.Parse(functionAnnotation) - image, _ := annotationContent.Pipe(yaml.Lookup("container", "image")) - return image.YNode().Value, path - } + fn, _ := GetFunction(n, meta) + if fn != nil { + image, _ := fn.Pipe(yaml.Lookup("container", "image")) + return yaml.GetValue(image), path } container := meta.Annotations["config.kubernetes.io/container"] @@ -434,5 +441,19 @@ func GetContainerName(n *yaml.RNode) (string, string) { if err != nil || yaml.IsMissingOrNull(image) { return "", path } - return image.YNode().Value, path + return yaml.GetValue(image), path +} + +// GetContainerNetworkRequired returns whether or not networking is required for the container +func GetContainerNetworkRequired(n *yaml.RNode) (bool, error) { + meta, err := n.GetMeta() + if err != nil { + return false, err + } + f, err := GetFunction(n, meta) + if err != nil { + return false, err + } + networkRequired, _ := f.Pipe(yaml.Lookup("container", "network", "required")) + return yaml.GetValue(networkRequired) == "true", nil } diff --git a/kyaml/kio/filters/container_test.go b/kyaml/kio/filters/container_test.go index f0faea2d3..43e697e3b 100644 --- a/kyaml/kio/filters/container_test.go +++ b/kyaml/kio/filters/container_test.go @@ -309,6 +309,79 @@ metadata: `, b.String()) } +func Test_GetFunction(t *testing.T) { + var tests = []struct { + name string + resource string + expectedFn string + missingFn bool + }{ + + // fn annotation + { + name: "fn annotation", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + annotations: + config.kubernetes.io/function: |- + container: foo:v1.0.0 +`, + expectedFn: `container: foo:v1.0.0`, + }, + + // legacy fn style + {name: "legacy fn meta", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + configFn: + container: foo:v1.0.0 +`, + expectedFn: `container: foo:v1.0.0`, + }, + + // no fn + {name: "no fn", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + annotations: {} +`, + missingFn: true, + }, + + // test network, etc... + } + + for i := range tests { + tt := tests[i] + t.Run(tt.name, func(t *testing.T) { + resource := yaml.MustParse(tt.resource) + meta, err := resource.GetMeta() + if !assert.NoError(t, err) { + t.FailNow() + } + fn, err := GetFunction(resource, meta) + if !assert.NoError(t, err) { + t.FailNow() + } + if tt.missingFn { + if !assert.Nil(t, fn) { + t.FailNow() + } + } else { + if !assert.Equal(t, strings.TrimSpace(fn.MustString()), strings.TrimSpace(tt.expectedFn)) { + t.FailNow() + } + } + }) + } +} + func Test_GetContainerName(t *testing.T) { // make sure gcr.io works n, err := yaml.Parse(`apiVersion: v1beta1 @@ -364,6 +437,76 @@ metadata: assert.Equal(t, "", c) } +func Test_GetContainerNetworkRequired(t *testing.T) { + tests := []struct { + input string + required bool + }{ + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + configFn: + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + network: + required: true +`, + required: true, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + configFn: + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + network: + required: false +`, + required: false, + }, + { + + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + configFn: + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 +`, + required: false, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + annotations: + config.kubernetes.io/function: | + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + network: + required: true +`, + required: true, + }, + } + + for _, tc := range tests { + cfg, err := yaml.Parse(tc.input) + if !assert.NoError(t, err) { + return + } + required, err := GetContainerNetworkRequired(cfg) + assert.NoError(t, err) + assert.Equal(t, tc.required, required) + } +} + func TestFilter_Filter_defaultNaming(t *testing.T) { cfg, err := yaml.Parse(`apiVersion: apps/v1 kind: Deployment diff --git a/kyaml/runfn/runfn.go b/kyaml/runfn/runfn.go index 3a19f1da5..e64c6222d 100644 --- a/kyaml/runfn/runfn.go +++ b/kyaml/runfn/runfn.go @@ -44,6 +44,12 @@ type RunFns struct { // Input can be set to read the Resources from Input rather than from a directory Input io.Reader + // Network enables network access for functions that declare it + Network bool + + // NetworkName is the name of the docker network to use for the container + NetworkName string + // Output can be set to write the result to Output rather than back to the directory Output io.Writer @@ -52,7 +58,7 @@ type RunFns struct { NoFunctionsFromInput *bool // for testing purposes only - containerFilterProvider func(string, string, *yaml.RNode) kio.Filter + containerFilterProvider func(string, string, string, *yaml.RNode) kio.Filter } // Execute runs the command @@ -119,7 +125,10 @@ func (r RunFns) getFilters(nodes []*yaml.RNode) ([]kio.Filter, error) { fltrs = append(fltrs, f...) // explicit filters from a list of directories - f = r.getFunctionsFromFunctions() + f, err = r.getFunctionsFromFunctions() + if err != nil { + return nil, err + } fltrs = append(fltrs, f...) return fltrs, nil @@ -160,8 +169,22 @@ func (r RunFns) getFunctionsFromInput(nodes []*yaml.RNode) ([]kio.Filter, error) sortFns(buff) for i := range buff.Nodes { api := buff.Nodes[i] + network := "" img, path := filters.GetContainerName(api) - fltrs = append(fltrs, r.containerFilterProvider(img, path, api)) + + required, err := filters.GetContainerNetworkRequired(api) + if err != nil { + return nil, err + } + if required { + if !r.Network { + // TODO(eddizane): Provide error info about which function needs the network + return fltrs, errors.Errorf("network required but not enabled with --network") + } + network = r.NetworkName + } + + fltrs = append(fltrs, r.containerFilterProvider(img, path, network, api)) } return fltrs, nil } @@ -182,8 +205,22 @@ func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) { } for i := range buff.Nodes { api := buff.Nodes[i] + network := "" img, path := filters.GetContainerName(api) - c := r.containerFilterProvider(img, path, api) + + required, err := filters.GetContainerNetworkRequired(api) + if err != nil { + return nil, err + } + if required { + if !r.Network { + // TODO(eddiezane): Provide error info about which function needs the network + return fltrs, errors.Errorf("network required but not enabled with --network") + } + network = r.NetworkName + } + + c := r.containerFilterProvider(img, path, network, api) cf, ok := c.(*filters.ContainerFilter) if ok { // functions provided by FunctionPaths are globally scoped @@ -196,12 +233,26 @@ func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) { // getFunctionsFromFunctions returns the set of explicitly provided functions as // Filters -func (r RunFns) getFunctionsFromFunctions() []kio.Filter { +func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) { var fltrs []kio.Filter for i := range r.Functions { api := r.Functions[i] + network := "" img, path := filters.GetContainerName(api) - c := r.containerFilterProvider(img, path, api) + + required, err := filters.GetContainerNetworkRequired(api) + if err != nil { + return nil, err + } + if required { + if !r.Network { + // TODO(eddizane): Provide error info about which function needs the network + return fltrs, errors.Errorf("network required but not enabled with --network") + } + network = r.NetworkName + } + + c := r.containerFilterProvider(img, path, network, api) cf, ok := c.(*filters.ContainerFilter) if ok { // functions provided by Functions are globally scoped @@ -209,7 +260,7 @@ func (r RunFns) getFunctionsFromFunctions() []kio.Filter { } fltrs = append(fltrs, c) } - return fltrs + return fltrs, nil } // sortFns sorts functions so that functions with the longest paths come first @@ -278,10 +329,11 @@ func (r *RunFns) init() { // if containerFilterProvider hasn't been set, use the default if r.containerFilterProvider == nil { - r.containerFilterProvider = func(image, path string, api *yaml.RNode) kio.Filter { + r.containerFilterProvider = func(image, path, network string, api *yaml.RNode) kio.Filter { cf := &filters.ContainerFilter{ Image: image, Config: api, + Network: network, StorageMounts: r.StorageMounts, GlobalScope: r.GlobalScope, } diff --git a/kyaml/runfn/runfn_test.go b/kyaml/runfn/runfn_test.go index 49e805a73..52d6ae229 100644 --- a/kyaml/runfn/runfn_test.go +++ b/kyaml/runfn/runfn_test.go @@ -50,7 +50,7 @@ kind: if !assert.NoError(t, err) { return } - filter := instance.containerFilterProvider("example.com:version", "", api) + filter := instance.containerFilterProvider("example.com:version", "", "", api) assert.Equal(t, &filters.ContainerFilter{Image: "example.com:version", Config: api}, filter) } @@ -69,7 +69,7 @@ kind: if !assert.NoError(t, err) { return } - filter := instance.containerFilterProvider("example.com:version", "", api) + filter := instance.containerFilterProvider("example.com:version", "", "", api) assert.Equal(t, &filters.ContainerFilter{ Image: "example.com:version", Config: api, GlobalScope: true}, filter) } @@ -659,8 +659,8 @@ func setupTest(t *testing.T) string { // getFilterProvider fakes the creation of a filter, replacing the ContainerFiler with // a filter to s/kind: Deployment/kind: StatefulSet/g. // this can be used to simulate running a filter. -func getFilterProvider(t *testing.T) func(string, string, *yaml.RNode) kio.Filter { - return func(s, _ string, node *yaml.RNode) kio.Filter { +func getFilterProvider(t *testing.T) func(string, string, string, *yaml.RNode) kio.Filter { + return func(s, _, _ string, node *yaml.RNode) kio.Filter { // parse the filter from the input filter := yaml.YFilter{} b := &bytes.Buffer{}