exec function working dir is the kustomization that referenced it (#4125)

* exec function working dir is the kustomization that referenced it

* suggested changes

* more code review

* use a field instead of an annotation

* more code review
This commit is contained in:
Natasha Sarkar
2021-08-19 20:15:24 -07:00
committed by GitHub
parent f604619dd5
commit 1e1b9b484a
10 changed files with 202 additions and 45 deletions

View File

@@ -79,6 +79,7 @@ func NewFnPlugin(o *types.FnPluginLoadingOptions) *FnPlugin {
StorageMounts: toStorageMounts(o.Mounts),
Env: o.Env,
AsCurrentUser: o.AsCurrentUser,
WorkingDir: o.WorkingDir,
},
}
}

View File

@@ -47,6 +47,11 @@ func (l *Loader) Config() *types.PluginConfig {
return l.pc
}
// SetWorkDir sets the working directory for this loader's plugins
func (l *Loader) SetWorkDir(wd string) {
l.pc.FnpLoadingOptions.WorkingDir = wd
}
func (l *Loader) LoadGenerators(
ldr ifc.Loader, v ifc.Validator, rm resmap.ResMap) ([]resmap.Generator, error) {
var result []resmap.Generator

View File

@@ -40,11 +40,13 @@ func NewKustTarget(
validator ifc.Validator,
rFactory *resmap.Factory,
pLdr *loader.Loader) *KustTarget {
pLdrCopy := *pLdr
pLdrCopy.SetWorkDir(ldr.Root())
return &KustTarget{
ldr: ldr,
validator: validator,
rFactory: rFactory,
pLdr: pLdr,
pLdr: &pLdrCopy,
}
}
@@ -295,7 +297,6 @@ func (kt *KustTarget) configureExternalTransformers(transformers []string) ([]re
ra.AppendAll(rm)
}
ra, err := kt.accumulateResources(ra, transformerPaths, &resource.Origin{})
if err != nil {
return nil, err
}

View File

@@ -1,17 +1,52 @@
package krusty_test
import (
"os"
"os/exec"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
kusttest_test "sigs.k8s.io/kustomize/api/testutils/kusttest"
"sigs.k8s.io/kustomize/kyaml/filesys"
)
func TestFnExecGenerator(t *testing.T) {
// Function plugins should not need the env setup done by MakeEnhancedHarness
th := kusttest_test.MakeHarness(t)
const generateDeploymentDotSh = `#!/bin/sh
th.WriteK(".", `
cat <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
annotations:
tshirt-size: small # this injects the resource reservations
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
EOF
`
func TestFnExecGenerator(t *testing.T) {
fSys := filesys.MakeFsOnDisk()
th := kusttest_test.MakeHarnessWithFs(t, fSys)
o := th.MakeOptionsPluginsEnabled()
o.PluginConfig.FnpLoadingOptions.EnableExec = true
tmpDir, err := filesys.NewTmpConfirmedDir()
assert.NoError(t, err)
th.WriteK(tmpDir.String(), `
resources:
- short_secret.yaml
generators:
@@ -19,7 +54,8 @@ generators:
`)
// Create some additional resource just to make sure everything is added
th.WriteF("short_secret.yaml", `
th.WriteF(filepath.Join(tmpDir.String(), "short_secret.yaml"),
`
apiVersion: v1
kind: Secret
metadata:
@@ -32,22 +68,25 @@ stringData:
bootcmd:
- mkdir /mnt/vda
`)
th.WriteF(filepath.Join(tmpDir.String(), "generateDeployment.sh"), generateDeploymentDotSh)
th.WriteF("gener.yaml", `
assert.NoError(t, os.Chmod(filepath.Join(tmpDir.String(), "generateDeployment.sh"), 0777))
th.WriteF(filepath.Join(tmpDir.String(), "gener.yaml"), `
kind: executable
metadata:
name: demo
annotations:
config.kubernetes.io/function: |
exec:
path: ./fnplugin_test/fnexectest.sh
path: ./generateDeployment.sh
spec:
`)
o := th.MakeOptionsPluginsEnabled()
o.PluginConfig.FnpLoadingOptions.EnableExec = true
m := th.Run(".", o)
th.AssertActualEqualsExpected(m, `
apiVersion: v1
m := th.Run(tmpDir.String(), o)
assert.NoError(t, err)
yml, err := m.AsYaml()
assert.NoError(t, err)
assert.Equal(t, `apiVersion: v1
kind: Secret
metadata:
labels:
@@ -79,7 +118,99 @@ spec:
containers:
- image: nginx
name: nginx
`, string(yml))
assert.NoError(t, fSys.RemoveAll(tmpDir.String()))
}
func TestFnExecGeneratorWithOverlay(t *testing.T) {
fSys := filesys.MakeFsOnDisk()
th := kusttest_test.MakeHarnessWithFs(t, fSys)
o := th.MakeOptionsPluginsEnabled()
o.PluginConfig.FnpLoadingOptions.EnableExec = true
tmpDir, err := filesys.NewTmpConfirmedDir()
assert.NoError(t, err)
base := filepath.Join(tmpDir.String(), "base")
prod := filepath.Join(tmpDir.String(), "prod")
assert.NoError(t, fSys.Mkdir(base))
assert.NoError(t, fSys.Mkdir(prod))
th.WriteK(base, `
resources:
- short_secret.yaml
generators:
- gener.yaml
`)
th.WriteK(prod, `
resources:
- ../base
`)
th.WriteF(filepath.Join(base, "short_secret.yaml"),
`
apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: "true"
name: node1-bmc-secret
type: Opaque
stringData:
userData: |
bootcmd:
- mkdir /mnt/vda
`)
th.WriteF(filepath.Join(base, "generateDeployment.sh"), generateDeploymentDotSh)
assert.NoError(t, os.Chmod(filepath.Join(base, "generateDeployment.sh"), 0777))
th.WriteF(filepath.Join(base, "gener.yaml"), `
kind: executable
metadata:
name: demo
annotations:
config.kubernetes.io/function: |
exec:
path: ./generateDeployment.sh
spec:
`)
m := th.Run(prod, o)
assert.NoError(t, err)
yml, err := m.AsYaml()
assert.NoError(t, err)
assert.Equal(t, `apiVersion: v1
kind: Secret
metadata:
labels:
airshipit.org/ephemeral-user-data: "true"
name: node1-bmc-secret
stringData:
userData: |
bootcmd:
- mkdir /mnt/vda
type: Opaque
---
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
tshirt-size: small
labels:
app: nginx
name: nginx
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- image: nginx
name: nginx
`, string(yml))
assert.NoError(t, fSys.RemoveAll(tmpDir.String()))
}
func skipIfNoDocker(t *testing.T) {

View File

@@ -1,24 +0,0 @@
#!/bin/sh
cat <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx
labels:
app: nginx
annotations:
tshirt-size: small # this injects the resource reservations
spec:
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx
EOF

View File

@@ -57,4 +57,6 @@ type FnPluginLoadingOptions struct {
Env []string
// Run as uid and gid of the command executor
AsCurrentUser bool
// Run in this working directory
WorkingDir string
}

View File

@@ -6,6 +6,7 @@ package commands
import (
"fmt"
"io"
"os"
"strings"
"github.com/spf13/cobra"
@@ -73,6 +74,7 @@ func GetRunFnRunner(name string) *RunFnRunner {
"a list of environment variables to be used by functions")
r.Command.Flags().BoolVar(
&r.AsCurrentUser, "as-current-user", false, "use the uid and gid of the command executor to run the function in the container")
return r
}
@@ -302,6 +304,11 @@ func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error {
// parse mounts to set storageMounts
storageMounts := toStorageMounts(r.Mounts)
wd, err := os.Getwd()
if err != nil {
return err
}
r.RunFns = runfn.RunFns{
FunctionPaths: r.FnPaths,
GlobalScope: r.GlobalScope,
@@ -317,6 +324,7 @@ func (r *RunFnRunner) preRunE(c *cobra.Command, args []string) error {
LogSteps: r.LogSteps,
Env: r.Env,
AsCurrentUser: r.AsCurrentUser,
WorkingDir: wd,
}
// don't consider args for the function

View File

@@ -11,13 +11,14 @@ import (
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"sigs.k8s.io/kustomize/kyaml/runfn"
)
// 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) {
wd, err := os.Getwd()
assert.NoError(t, err)
tests := []struct {
name string
args []string
@@ -201,6 +202,7 @@ apiVersion: v1
Path: "dir",
EnableStarlark: true,
Env: []string{},
WorkingDir: wd,
},
},
{
@@ -254,6 +256,7 @@ apiVersion: v1
Path: "dir",
ResultsDir: "foo/",
Env: []string{},
WorkingDir: wd,
},
expected: `
metadata:
@@ -286,9 +289,10 @@ apiVersion: v1
args: []string{"run", "dir", "--log-steps"},
path: "dir",
expectedStruct: &runfn.RunFns{
Path: "dir",
LogSteps: true,
Env: []string{},
Path: "dir",
LogSteps: true,
Env: []string{},
WorkingDir: wd,
},
},
{
@@ -296,8 +300,9 @@ apiVersion: v1
args: []string{"run", "dir", "--env", "FOO=BAR", "-e", "BAR"},
path: "dir",
expectedStruct: &runfn.RunFns{
Path: "dir",
Env: []string{"FOO=BAR", "BAR"},
Path: "dir",
Env: []string{"FOO=BAR", "BAR"},
WorkingDir: wd,
},
},
{
@@ -308,6 +313,7 @@ apiVersion: v1
Path: "dir",
AsCurrentUser: true,
Env: []string{},
WorkingDir: wd,
},
},
}

View File

@@ -7,7 +7,9 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"sigs.k8s.io/kustomize/kyaml/errors"
"sigs.k8s.io/kustomize/kyaml/fn/runtime/runtimeutil"
"sigs.k8s.io/kustomize/kyaml/yaml"
)
@@ -19,6 +21,10 @@ type Filter struct {
// Args are the arguments to the executable
Args []string `yaml:"args,omitempty"`
// WorkingDir is the working directory that the executable
// should run in
WorkingDir string
runtimeutil.FunctionFilter
}
@@ -32,5 +38,16 @@ func (c *Filter) Run(reader io.Reader, writer io.Writer) error {
cmd.Stdin = reader
cmd.Stdout = writer
cmd.Stderr = os.Stderr
if c.WorkingDir != "" {
if !filepath.IsAbs(c.WorkingDir) {
return errors.Errorf(
"relative working directory %s not allowed", c.WorkingDir)
}
if c.WorkingDir == "/" {
return errors.Errorf(
"root working directory '/' not allowed")
}
cmd.Dir = c.WorkingDir
}
return cmd.Run()
}

View File

@@ -101,6 +101,9 @@ type RunFns struct {
// If it is true, the empty result will be provided as input to the next
// function in the list.
ContinueOnEmptyResult bool
// WorkingDir specifies which working directory an exec function should run in.
WorkingDir string
}
// Execute runs the command
@@ -507,7 +510,14 @@ func (r *RunFns) ffp(spec runtimeutil.FunctionSpec, api *yaml.RNode, currentUser
}
if r.EnableExec && spec.Exec.Path != "" {
ef := &exec.Filter{Path: spec.Exec.Path}
if r.WorkingDir == "" {
return nil, fmt.Errorf("no working directory set for exec function")
}
ef := &exec.Filter{
Path: spec.Exec.Path,
WorkingDir: r.WorkingDir,
}
ef.FunctionConfig = api
ef.GlobalScope = r.GlobalScope