diff --git a/cmd/config/internal/commands/run-fns.go b/cmd/config/internal/commands/run-fns.go index fe24a97fd..f31d2fe7f 100644 --- a/cmd/config/internal/commands/run-fns.go +++ b/cmd/config/internal/commands/run-fns.go @@ -54,6 +54,9 @@ func GetRunFnRunner(name string) *RunFnRunner { &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") + r.Command.Flags().StringSliceVar( + &r.Volumes, "volume", []string{}, + "the volumes to bind mount to the container.") return r } @@ -75,6 +78,7 @@ type RunFnRunner struct { RunFns runfn.RunFns Network bool NetworkName string + Volumes []string } func (r *RunFnRunner) runE(c *cobra.Command, args []string) error { @@ -250,6 +254,7 @@ func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error { Network: r.Network, NetworkName: r.NetworkName, EnableStarlark: r.EnableStar, + Volumes: r.Volumes, } // 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 4a2062072..887451035 100644 --- a/cmd/config/internal/commands/run_test.go +++ b/cmd/config/internal/commands/run_test.go @@ -27,6 +27,7 @@ func TestRunFnCommand_preRunE(t *testing.T) { functionPaths []string network bool networkName string + volumes []string }{ { name: "config map", @@ -213,6 +214,29 @@ metadata: data: {g: h, i: j=k} kind: Foo apiVersion: v1 +`, + }, + { + name: "volumes", + args: []string{"run", "dir", "--volume", "vol1", "--volume", "vol2"}, + path: "dir", + volumes: []string{"vol1", "vol2"}, + }, + { + name: "custom kind with volumes", + args: []string{ + "run", "dir", "--volume", "vol", "--image", "foo:bar", "--", "Foo", "g=h", "i=j=k"}, + path: "dir", + volumes: []string{"vol"}, + expected: ` +metadata: + name: function-input + annotations: + config.kubernetes.io/function: | + container: {image: 'foo:bar'} +data: {g: h, i: j=k} +kind: Foo +apiVersion: v1 `, }, { @@ -303,6 +327,15 @@ apiVersion: v1 t.FailNow() } + // check if Volumes were set + if tt.volumes == nil { + // make Equal work against flag default + tt.volumes = []string{} + } + if !assert.Equal(t, tt.volumes, r.RunFns.Volumes) { + t.FailNow() + } + // check if Functions were set if tt.expected != "" { if !assert.Len(t, r.RunFns.Functions, 1) { diff --git a/kyaml/kio/filters/container.go b/kyaml/kio/filters/container.go index 6e3dee11f..663774356 100644 --- a/kyaml/kio/filters/container.go +++ b/kyaml/kio/filters/container.go @@ -134,6 +134,9 @@ type ContainerFilter struct { // Network is the container network to use. Network string `yaml:"network,omitempty"` + // Volumes are the directories to mount as container volumes. + Volumes []string `yaml:"volumes,omitempty"` + // StorageMounts is a list of storage options that the container will have mounted. StorageMounts []StorageMount @@ -335,6 +338,11 @@ func (c *ContainerFilter) getArgs() []string { args = append(args, "--mount", storageMount.String()) } + // export volumes to the container + for _, volume := range c.Volumes { + args = append(args, "--volume", volume) + } + // export the local environment vars to the container for _, pair := range os.Environ() { tokens := strings.Split(pair, "=") diff --git a/kyaml/kio/filters/container_test.go b/kyaml/kio/filters/container_test.go index 3ae8d901a..5d37ee55e 100644 --- a/kyaml/kio/filters/container_test.go +++ b/kyaml/kio/filters/container_test.go @@ -141,6 +141,87 @@ metadata: assert.Equal(t, expected, cmd.Args) } +func TestFilter_command_volume(t *testing.T) { + cfg, err := yaml.Parse(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: foo +`) + if !assert.NoError(t, err) { + return + } + instance := &ContainerFilter{ + Image: "example.com:version", + Volumes: []string{"/host-src:/container-dest:ro"}, + Config: cfg, + } + cmd, err := instance.getCommand() + if !assert.NoError(t, err) { + return + } + + expected := []string{ + "docker", "run", + "--rm", + "-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", + "--network", "none", + "--user", "nobody", + "--security-opt=no-new-privileges", + "--volume", "/host-src:/container-dest:ro", + } + for _, e := range os.Environ() { + // the process env + tokens := strings.Split(e, "=") + if tokens[0] == "" { + continue + } + expected = append(expected, "-e", tokens[0]) + } + expected = append(expected, "example.com:version") + assert.Equal(t, expected, cmd.Args) +} + +func TestFilter_command_volumes(t *testing.T) { + cfg, err := yaml.Parse(`apiVersion: apps/v1 +kind: Deployment +metadata: + name: foo +`) + if !assert.NoError(t, err) { + return + } + instance := &ContainerFilter{ + Image: "example.com:version", + Volumes: []string{"/host-src1:/container-dest1:ro", "/host-src2:/container-dest2:rw"}, + Config: cfg, + } + cmd, err := instance.getCommand() + if !assert.NoError(t, err) { + return + } + + expected := []string{ + "docker", "run", + "--rm", + "-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", + "--network", "none", + "--user", "nobody", + "--security-opt=no-new-privileges", + "--volume", "/host-src1:/container-dest1:ro", + "--volume", "/host-src2:/container-dest2:rw", + } + for _, e := range os.Environ() { + // the process env + tokens := strings.Split(e, "=") + if tokens[0] == "" { + continue + } + expected = append(expected, "-e", tokens[0]) + } + expected = append(expected, "example.com:version") + assert.Equal(t, expected, cmd.Args) +} + func TestFilter_Filter(t *testing.T) { cfg, err := yaml.Parse(`apiVersion: apps/v1 kind: Deployment @@ -355,6 +436,70 @@ container: `, }, + { + name: "volume", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + annotations: + config.kubernetes.io/function: |- + container: + image: foo:v1.0.0 + volumes: ["/host-src:/container-dest:ro"] +`, + expectedFn: ` +container: + image: foo:v1.0.0 + volumes: + - /host-src:/container-dest:ro +`, + }, + + { + name: "volumes as array", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + annotations: + config.kubernetes.io/function: |- + container: + image: foo:v1.0.0 + volumes: ["/host-src1:/container-dest1:ro", "/host-src2:/container-dest2:rw"] +`, + expectedFn: ` +container: + image: foo:v1.0.0 + volumes: + - /host-src1:/container-dest1:ro + - /host-src2:/container-dest2:rw +`, + }, + + { + name: "volumes as list", + resource: ` +apiVersion: v1beta1 +kind: Example +metadata: + annotations: + config.kubernetes.io/function: |- + container: + image: foo:v1.0.0 + volumes: + - "/host-src1:/container-dest1:ro" + - "/host-src2:/container-dest2:rw" +`, + expectedFn: ` +container: + image: foo:v1.0.0 + volumes: + - /host-src1:/container-dest1:ro + - /host-src2:/container-dest2:rw +`, + }, + { name: "path", resource: ` @@ -516,6 +661,76 @@ metadata: } } +func Test_GetContainerVolumeRequired(t *testing.T) { + tests := []struct { + input string + volumes []string + }{ + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + configFn: + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + volumes: [ /host-src:/container-dest:ro ] +`, + volumes: []string{"/host-src:/container-dest:ro"}, + }, + { + + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + configFn: + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 +`, + volumes: []string(nil), + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + annotations: + config.kubernetes.io/function: | + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + volumes: [ "/host-src1:/container-dest1:ro", "/host-src2:/container-dest2:rw" ] +`, + volumes: []string{"/host-src1:/container-dest1:ro", "/host-src2:/container-dest2:rw"}, + }, + { + input: `apiVersion: v1 +kind: Foo +metadata: + name: foo + annotations: + config.kubernetes.io/function: | + container: + image: gcr.io/kustomize-functions/example-tshirt:v0.1.0 + volumes: + - /host-src1:/container-dest1:ro + - /host-src2:/container-dest2:rw +`, + volumes: []string{"/host-src1:/container-dest1:ro", "/host-src2:/container-dest2:rw"}, + }, + } + + for _, tc := range tests { + cfg, err := yaml.Parse(tc.input) + if !assert.NoError(t, err) { + return + } + + fn := GetFunctionSpec(cfg) + assert.Equal(t, tc.volumes, fn.Container.Volumes) + } +} + func TestFilter_Filter_defaultNaming(t *testing.T) { cfg, err := yaml.Parse(`apiVersion: apps/v1 kind: Deployment diff --git a/kyaml/kio/filters/functiontypes.go b/kyaml/kio/filters/functiontypes.go index 83f6f647c..4096cd952 100644 --- a/kyaml/kio/filters/functiontypes.go +++ b/kyaml/kio/filters/functiontypes.go @@ -28,6 +28,9 @@ type FunctionSpec struct { // Starlark is the spec for running a function as a starlark script Starlark StarlarkSpec `json:"starlark,omitempty" yaml:"starlark,omitempty"` + + // Volumes are the directories to mount as container volumes + Volumes []string `json:"volumes,omitempty" yaml:"volumes,omitempty"` } // ContainerSpec defines a spec for running a function as a container @@ -37,6 +40,9 @@ type ContainerSpec struct { // Network defines network specific configuration Network ContainerNetwork `json:"network,omitempty" yaml:"network,omitempty"` + + // Volumes are the directories to mount as container volumes + Volumes []string `json:"volumes,omitempty" yaml:"volumes,omitempty"` } // ContainerNetwork @@ -68,6 +74,7 @@ func GetFunctionSpec(n *yaml.RNode) *FunctionSpec { path := meta.Annotations[kioutil.PathAnnotation] if fn := getFunctionSpecFromAnnotation(n, meta); fn != nil { fn.Network = "" + fn.Volumes = []string{} fn.Path = path return fn } diff --git a/kyaml/runfn/runfn.go b/kyaml/runfn/runfn.go index 19d348486..23ccd00f6 100644 --- a/kyaml/runfn/runfn.go +++ b/kyaml/runfn/runfn.go @@ -51,6 +51,10 @@ type RunFns struct { // NetworkName is the name of the docker network to use for the container NetworkName string + // Volumes Volumes allows directories to be specified outside the configuration + // directory. + Volumes []string + // Output can be set to write the result to Output rather than back to the directory Output io.Writer @@ -133,6 +137,13 @@ func (r RunFns) getFilters(nodes []*yaml.RNode) ([]kio.Filter, error) { } fltrs = append(fltrs, f...) + // directories from volumes specified on the struct + f, err = r.getDirectoriesFromVolumes() + if err != nil { + return nil, err + } + fltrs = append(fltrs, f...) + // explicit fns specified on the struct f, err = r.getFunctionsFromFunctions() if err != nil { @@ -196,6 +207,24 @@ func (r RunFns) getFunctionsFromFunctionPaths() ([]kio.Filter, error) { return r.getFunctionFilters(true, buff.Nodes...) } +// getDirectoriesFromVolumes returns the set of directories read from r.Volumes +// as a slice of Filters +func (r RunFns) getDirectoriesFromVolumes() ([]kio.Filter, error) { + buff := &kio.PackageBuffer{} + for i := range r.Volumes { + err := kio.Pipeline{ + Inputs: []kio.Reader{ + kio.LocalPackageReader{PackagePath: r.Volumes[i]}, + }, + Outputs: []kio.Writer{buff}, + }.Execute() + if err != nil { + return nil, err + } + } + return r.getFunctionFilters(true, buff.Nodes...) +} + // getFunctionsFromFunctions returns the set of explicitly provided functions as // Filters func (r RunFns) getFunctionsFromFunctions() ([]kio.Filter, error) { diff --git a/kyaml/runfn/runfn_test.go b/kyaml/runfn/runfn_test.go index f88349194..b3018129f 100644 --- a/kyaml/runfn/runfn_test.go +++ b/kyaml/runfn/runfn_test.go @@ -140,6 +140,16 @@ func TestRunFns_Execute__initDefault(t *testing.T) { FunctionPaths: []string{"foo"}, }, }, + { + name: "explicit directories in volumes", + instance: RunFns{Volumes: []string{"vol"}}, + expected: RunFns{ + Output: os.Stdout, + Input: os.Stdin, + NoFunctionsFromInput: getFalse(), + Volumes: []string{"vol"}, + }, + }, } for i := range tests { tt := tests[i]