mirror of
https://github.com/kubernetes-sigs/kustomize.git
synced 2026-07-18 02:01:29 +00:00
Refactor container function runtime into its own package
This commit is contained in:
202
kyaml/fn/runtime/container/container.go
Normal file
202
kyaml/fn/runtime/container/container.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
runtimeexec "sigs.k8s.io/kustomize/kyaml/fn/runtime/exec"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
||||
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
// Filter filters Resources using a container image.
|
||||
// The container must start a process that reads the list of
|
||||
// input Resources from stdin, reads the Configuration from the env
|
||||
// API_CONFIG, and writes the filtered Resources to stdout.
|
||||
// If there is a error or validation failure, the process must exit
|
||||
// non-zero.
|
||||
// The full set of environment variables from the parent process
|
||||
// are passed to the container.
|
||||
//
|
||||
// Function Scoping:
|
||||
// Filter applies the function only to Resources to which it is scoped.
|
||||
//
|
||||
// Resources are scoped to a function if any of the following are true:
|
||||
// - the Resource were read from the same directory as the function config
|
||||
// - the Resource were read from a subdirectory of the function config directory
|
||||
// - the function config is in a directory named "functions" and
|
||||
// they were read from a subdirectory of "functions" parent
|
||||
// - the function config doesn't have a path annotation (considered globally scoped)
|
||||
// - the Filter has GlobalScope == true
|
||||
//
|
||||
// In Scope Examples:
|
||||
//
|
||||
// Example 1: deployment.yaml and service.yaml in function.yaml scope
|
||||
// same directory as the function config directory
|
||||
// .
|
||||
// ├── function.yaml
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 2: apps/deployment.yaml and apps/service.yaml in function.yaml scope
|
||||
// subdirectory of the function config directory
|
||||
// .
|
||||
// ├── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 3: apps/deployment.yaml and apps/service.yaml in functions/function.yaml scope
|
||||
// function config is in a directory named "functions"
|
||||
// .
|
||||
// ├── functions
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Out of Scope Examples:
|
||||
//
|
||||
// Example 1: apps/deployment.yaml and apps/service.yaml NOT in stuff/function.yaml scope
|
||||
// .
|
||||
// ├── stuff
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Example 2: apps/deployment.yaml and apps/service.yaml NOT in stuff/functions/function.yaml scope
|
||||
// .
|
||||
// ├── stuff
|
||||
// │ └── functions
|
||||
// │ └── function.yaml
|
||||
// └── apps
|
||||
// ├── deployment.yaml
|
||||
// └── service.yaml
|
||||
//
|
||||
// Default Paths:
|
||||
// Resources emitted by functions will have default path applied as annotations
|
||||
// if none is present.
|
||||
// The default path will be the function-dir/ (or parent directory in the case of "functions")
|
||||
// + function-file-name/ + namespace/ + kind_name.yaml
|
||||
//
|
||||
// Example 1: Given a function in fn.yaml that produces a Deployment name foo and a Service named bar
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── fn.yaml
|
||||
// └── fn
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
//
|
||||
// Example 2: Given a function in functions/fn.yaml that produces a Deployment name foo and a Service named bar
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── functions
|
||||
// │ └── fn.yaml
|
||||
// └── fn
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
//
|
||||
// Example 3: Given a function in fn.yaml that produces a Deployment name foo, namespace baz and a Service named bar namespace baz
|
||||
// dir
|
||||
// └── fn.yaml
|
||||
//
|
||||
// Would default newly generated Resources to:
|
||||
//
|
||||
// dir
|
||||
// ├── fn.yaml
|
||||
// └── fn
|
||||
// └── baz
|
||||
// ├── deployment_foo.yaml
|
||||
// └── service_bar.yaml
|
||||
type Filter struct {
|
||||
|
||||
// Image is the container image to use to create a container.
|
||||
Image string `yaml:"image,omitempty"`
|
||||
|
||||
// Network is the container network to use.
|
||||
Network string `yaml:"network,omitempty"`
|
||||
|
||||
// StorageMounts is a list of storage options that the container will have mounted.
|
||||
StorageMounts []runtimeutil.StorageMount `yaml:"mounts,omitempty"`
|
||||
|
||||
Exec runtimeexec.Filter
|
||||
}
|
||||
|
||||
func (c Filter) String() string {
|
||||
if c.Exec.DeferFailure {
|
||||
return fmt.Sprintf("%s deferFailure: %v", c.Image, c.Exec.DeferFailure)
|
||||
}
|
||||
return c.Image
|
||||
}
|
||||
func (c Filter) GetExit() error {
|
||||
return c.Exec.GetExit()
|
||||
}
|
||||
|
||||
func (c *Filter) Filter(nodes []*yaml.RNode) ([]*yaml.RNode, error) {
|
||||
c.setupExec()
|
||||
return c.Exec.Filter(nodes)
|
||||
}
|
||||
|
||||
func (c *Filter) setupExec() {
|
||||
// don't init 2x
|
||||
if c.Exec.Path != "" {
|
||||
return
|
||||
}
|
||||
|
||||
path, args := c.getCommand()
|
||||
c.Exec.Path = path
|
||||
c.Exec.Args = args
|
||||
}
|
||||
|
||||
// getArgs returns the command + args to run to spawn the container
|
||||
func (c *Filter) getCommand() (string, []string) {
|
||||
// run the container using docker. this is simpler than using the docker
|
||||
// libraries, and ensures things like auth work the same as if the container
|
||||
// was run from the cli.
|
||||
|
||||
network := "none"
|
||||
if c.Network != "" {
|
||||
network = c.Network
|
||||
}
|
||||
|
||||
args := []string{"run",
|
||||
"--rm", // delete the container afterward
|
||||
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR", // attach stdin, stdout, stderr
|
||||
"--network", network,
|
||||
|
||||
// added security options
|
||||
"--user", "nobody", // run as nobody
|
||||
"--security-opt=no-new-privileges", // don't allow the user to escalate privileges
|
||||
// note: don't make fs readonly because things like heredoc rely on writing tmp files
|
||||
}
|
||||
|
||||
// TODO(joncwong): Allow StorageMount fields to have default values.
|
||||
for _, storageMount := range c.StorageMounts {
|
||||
args = append(args, "--mount", storageMount.String())
|
||||
}
|
||||
|
||||
os.Setenv("LOG_TO_STDERR", "true")
|
||||
os.Setenv("STRUCTURED_RESULTS", "true")
|
||||
|
||||
// export the local environment vars to the container
|
||||
for _, pair := range os.Environ() {
|
||||
args = append(args, "-e", strings.Split(pair, "=")[0])
|
||||
}
|
||||
a := append(args, c.Image)
|
||||
|
||||
return "docker", a
|
||||
}
|
||||
203
kyaml/fn/runtime/container/container_test.go
Normal file
203
kyaml/fn/runtime/container/container_test.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Copyright 2019 The Kubernetes Authors.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package container
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
|
||||
"sigs.k8s.io/kustomize/kyaml/kio"
|
||||
"sigs.k8s.io/kustomize/kyaml/yaml"
|
||||
)
|
||||
|
||||
func TestFilter_setupExec(t *testing.T) {
|
||||
var tests = []struct {
|
||||
name string
|
||||
functionConfig string
|
||||
expectedArgs []string
|
||||
instance Filter
|
||||
}{
|
||||
{
|
||||
name: "command",
|
||||
functionConfig: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`,
|
||||
expectedArgs: []string{
|
||||
"run",
|
||||
"--rm",
|
||||
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR",
|
||||
"--network", "none",
|
||||
"--user", "nobody",
|
||||
"--security-opt=no-new-privileges",
|
||||
},
|
||||
instance: Filter{Image: "example.com:version"},
|
||||
},
|
||||
|
||||
{
|
||||
name: "network",
|
||||
functionConfig: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`,
|
||||
expectedArgs: []string{
|
||||
"run",
|
||||
"--rm",
|
||||
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR",
|
||||
"--network", "test-1",
|
||||
"--user", "nobody",
|
||||
"--security-opt=no-new-privileges",
|
||||
},
|
||||
instance: Filter{Image: "example.com:version", Network: "test-1"},
|
||||
},
|
||||
|
||||
{
|
||||
name: "storage_mounts",
|
||||
functionConfig: `apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`,
|
||||
expectedArgs: []string{
|
||||
"run",
|
||||
"--rm",
|
||||
"-i", "-a", "STDIN", "-a", "STDOUT", "-a", "STDERR",
|
||||
"--network", "none",
|
||||
"--user", "nobody",
|
||||
"--security-opt=no-new-privileges",
|
||||
"--mount", fmt.Sprintf("type=%s,src=%s,dst=%s:ro", "bind", "/mount/path", "/local/"),
|
||||
"--mount", fmt.Sprintf("type=%s,src=%s,dst=%s:ro", "volume", "myvol", "/local/"),
|
||||
"--mount", fmt.Sprintf("type=%s,src=%s,dst=%s:ro", "tmpfs", "", "/local/"),
|
||||
},
|
||||
instance: Filter{
|
||||
Image: "example.com:version",
|
||||
StorageMounts: []runtimeutil.StorageMount{
|
||||
{MountType: "bind", Src: "/mount/path", DstPath: "/local/"},
|
||||
{MountType: "volume", Src: "myvol", DstPath: "/local/"},
|
||||
{MountType: "tmpfs", Src: "", DstPath: "/local/"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range tests {
|
||||
tt := tests[i]
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg, err := yaml.Parse(tt.functionConfig)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
tt.instance.Exec.FunctionConfig = cfg
|
||||
|
||||
os.Setenv("KYAML_TEST", "FOO")
|
||||
tt.instance.setupExec()
|
||||
|
||||
// configure expected env
|
||||
for _, e := range os.Environ() {
|
||||
// the process env
|
||||
tt.expectedArgs = append(tt.expectedArgs, "-e", strings.Split(e, "=")[0])
|
||||
}
|
||||
tt.expectedArgs = append(tt.expectedArgs, tt.instance.Image)
|
||||
|
||||
if !assert.Equal(t, "docker", tt.instance.Exec.Path) {
|
||||
t.FailNow()
|
||||
}
|
||||
if !assert.Equal(t, tt.expectedArgs, tt.instance.Exec.Args) {
|
||||
t.FailNow()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_Filter(t *testing.T) {
|
||||
cfg, err := yaml.Parse(`apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: foo
|
||||
`)
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
input, err := (&kio.ByteReader{Reader: bytes.NewBufferString(`
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: deployment-foo
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: service-foo
|
||||
`)}).Read()
|
||||
if !assert.NoError(t, err) {
|
||||
return
|
||||
}
|
||||
|
||||
instance := Filter{}
|
||||
instance.Exec.FunctionConfig = cfg
|
||||
instance.Exec.Path = "sed"
|
||||
instance.Exec.Args = []string{"s/Deployment/StatefulSet/g"}
|
||||
output, err := instance.Filter(input)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
b := &bytes.Buffer{}
|
||||
err = kio.ByteWriter{Writer: b, KeepReaderAnnotations: true}.Write(output)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.Equal(t, `apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: deployment-foo
|
||||
annotations:
|
||||
config.kubernetes.io/index: '0'
|
||||
config.kubernetes.io/path: 'statefulset_deployment-foo.yaml'
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: service-foo
|
||||
annotations:
|
||||
config.kubernetes.io/index: '1'
|
||||
config.kubernetes.io/path: 'service_service-foo.yaml'
|
||||
`, b.String()) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
func TestFilter_String(t *testing.T) {
|
||||
instance := Filter{Image: "foo"}
|
||||
if !assert.Equal(t, "foo", instance.String()) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
instance.Exec.DeferFailure = true
|
||||
if !assert.Equal(t, "foo deferFailure: true", instance.String()) {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilter_ExitCode(t *testing.T) {
|
||||
instance := Filter{}
|
||||
instance.Exec.Path = "/not/real/command"
|
||||
instance.Exec.DeferFailure = true
|
||||
_, err := instance.Filter(nil)
|
||||
if !assert.NoError(t, err) {
|
||||
t.FailNow()
|
||||
}
|
||||
|
||||
if !assert.EqualError(t, instance.GetExit(), "fork/exec /not/real/command: no such file or directory") {
|
||||
t.FailNow()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user