diff --git a/pkg/apis/v1alpha1/binding.go b/pkg/apis/v1alpha1/binding.go index 26256f910..b6eb183e8 100644 --- a/pkg/apis/v1alpha1/binding.go +++ b/pkg/apis/v1alpha1/binding.go @@ -1,5 +1,19 @@ package v1alpha1 +import ( + "fmt" + "regexp" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" +) + +var ( + identifier = regexp.MustCompile(`^\w+$`) + forbiddenNames = []string{"namespace", "client", "error", "values", "stdout", "stderr"} + forbidden = sets.New(forbiddenNames...) +) + // Binding represents a key/value set as a binding in an executing test. type Binding struct { // Name the name of the binding. @@ -10,3 +24,28 @@ type Binding struct { // +kubebuilder:pruning:PreserveUnknownFields Value Any `json:"value"` } + +func (b Binding) CheckName() error { + return CheckBindingName(b.Name) +} + +func (b Binding) CheckEnvName() error { + return CheckBindingEnvName(b.Name) +} + +func CheckBindingName(name string) error { + if forbidden.Has(name) { + return fmt.Errorf("name is forbidden (%s), it must not be (%s)", name, strings.Join(forbiddenNames, ", ")) + } + if !identifier.MatchString(name) { + return fmt.Errorf("invalid name %s", name) + } + return nil +} + +func CheckBindingEnvName(name string) error { + if !identifier.MatchString(name) { + return fmt.Errorf("invalid name %s", name) + } + return nil +} diff --git a/pkg/runner/operations/command/operation.go b/pkg/runner/operations/command/operation.go index 2b382e302..226959077 100644 --- a/pkg/runner/operations/command/operation.go +++ b/pkg/runner/operations/command/operation.go @@ -10,7 +10,7 @@ import ( "github.com/jmespath-community/go-jmespath/pkg/binding" "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" "github.com/kyverno/chainsaw/pkg/runner/check" - "github.com/kyverno/chainsaw/pkg/runner/env" + environment "github.com/kyverno/chainsaw/pkg/runner/env" "github.com/kyverno/chainsaw/pkg/runner/logging" "github.com/kyverno/chainsaw/pkg/runner/operations" "github.com/kyverno/chainsaw/pkg/runner/operations/internal" @@ -48,7 +48,7 @@ func (o *operation) Exec(ctx context.Context, bindings binding.Bindings) (_err e defer func() { internal.LogEnd(logger, logging.Command, _err) }() - cmd, cancel, err := o.createCommand(ctx) + cmd, cancel, err := o.createCommand(ctx, bindings) if cancel != nil { defer cancel() } @@ -59,17 +59,19 @@ func (o *operation) Exec(ctx context.Context, bindings binding.Bindings) (_err e return o.execute(ctx, bindings, cmd) } -func (o *operation) createCommand(ctx context.Context) (*exec.Cmd, context.CancelFunc, error) { - var cancel context.CancelFunc - args := env.Expand(map[string]string{"NAMESPACE": o.namespace}, o.command.Args...) - cmd := exec.CommandContext(ctx, o.command.Entrypoint, args...) //nolint:gosec - env := os.Environ() +func (o *operation) createCommand(ctx context.Context, bindings binding.Bindings) (*exec.Cmd, context.CancelFunc, error) { cwd, err := os.Getwd() if err != nil { return nil, nil, fmt.Errorf("failed to get current working directory (%w)", err) } + maps, envs, err := internal.RegisterEnvs(ctx, o.namespace, bindings, o.command.Env...) + if err != nil { + return nil, nil, err + } + env := os.Environ() + env = append(env, envs...) env = append(env, fmt.Sprintf("PATH=%s/bin/:%s", cwd, os.Getenv("PATH"))) - env = append(env, fmt.Sprintf("NAMESPACE=%s", o.namespace)) + var cancel context.CancelFunc if o.cfg != nil { f, err := os.CreateTemp(o.basePath, "chainsaw-kubeconfig-") if err != nil { @@ -87,10 +89,10 @@ func (o *operation) createCommand(ctx context.Context) (*exec.Cmd, context.Cance if err := restutils.Save(o.cfg, f); err != nil { return nil, cancel, err } - fmt.Println(f.Name()) - fmt.Println(o.basePath) env = append(env, fmt.Sprintf("KUBECONFIG=%s", filepath.Join(cwd, path))) } + args := environment.Expand(maps, o.command.Args...) + cmd := exec.CommandContext(ctx, o.command.Entrypoint, args...) //nolint:gosec cmd.Env = env cmd.Dir = o.basePath return cmd, cancel, nil diff --git a/pkg/runner/operations/internal/env.go b/pkg/runner/operations/internal/env.go new file mode 100644 index 000000000..248c07acd --- /dev/null +++ b/pkg/runner/operations/internal/env.go @@ -0,0 +1,35 @@ +package internal + +import ( + "context" + "fmt" + + "github.com/jmespath-community/go-jmespath/pkg/binding" + "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" + mutation "github.com/kyverno/chainsaw/pkg/mutate" + "github.com/kyverno/chainsaw/pkg/runner/functions" + "github.com/kyverno/kyverno-json/pkg/engine/template" +) + +func RegisterEnvs(ctx context.Context, namespace string, bindings binding.Bindings, envs ...v1alpha1.Binding) (map[string]string, []string, error) { + mapOut := map[string]string{} + var envsOut []string + for _, env := range envs { + if err := env.CheckEnvName(); err != nil { + return mapOut, envsOut, err + } + patched, err := mutation.Mutate(ctx, nil, mutation.Parse(ctx, env.Value.Value), nil, bindings, template.WithFunctionCaller(functions.Caller)) + if err != nil { + return mapOut, envsOut, err + } + if patched, ok := patched.(string); !ok { + return mapOut, envsOut, fmt.Errorf("value must be a string (%s)", env.Name) + } else { + mapOut[env.Name] = patched + envsOut = append(envsOut, env.Name+"="+patched) + } + } + mapOut["NAMESPACE"] = namespace + envsOut = append(envsOut, "NAMESPACE="+namespace) + return mapOut, envsOut, nil +} diff --git a/pkg/runner/operations/script/operation.go b/pkg/runner/operations/script/operation.go index be1741e48..ab25d0448 100644 --- a/pkg/runner/operations/script/operation.go +++ b/pkg/runner/operations/script/operation.go @@ -47,7 +47,7 @@ func (o *operation) Exec(ctx context.Context, bindings binding.Bindings) (err er defer func() { internal.LogEnd(logger, logging.Script, err) }() - cmd, cancel, err := o.createCommand(ctx) + cmd, cancel, err := o.createCommand(ctx, bindings) if cancel != nil { defer cancel() } @@ -58,16 +58,19 @@ func (o *operation) Exec(ctx context.Context, bindings binding.Bindings) (err er return o.execute(ctx, bindings, cmd) } -func (o *operation) createCommand(ctx context.Context) (*exec.Cmd, context.CancelFunc, error) { - var cancel context.CancelFunc - cmd := exec.CommandContext(ctx, "sh", "-c", o.script.Content) //nolint:gosec - env := os.Environ() +func (o *operation) createCommand(ctx context.Context, bindings binding.Bindings) (*exec.Cmd, context.CancelFunc, error) { cwd, err := os.Getwd() if err != nil { return nil, nil, fmt.Errorf("failed to get current working directory (%w)", err) } + _, envs, err := internal.RegisterEnvs(ctx, o.namespace, bindings, o.script.Env...) + if err != nil { + return nil, nil, err + } + env := os.Environ() + env = append(env, envs...) env = append(env, fmt.Sprintf("PATH=%s/bin/:%s", cwd, os.Getenv("PATH"))) - env = append(env, fmt.Sprintf("NAMESPACE=%s", o.namespace)) + var cancel context.CancelFunc if o.cfg != nil { f, err := os.CreateTemp(o.basePath, "chainsaw-kubeconfig-") if err != nil { @@ -87,6 +90,7 @@ func (o *operation) createCommand(ctx context.Context) (*exec.Cmd, context.Cance } env = append(env, fmt.Sprintf("KUBECONFIG=%s", filepath.Join(cwd, path))) } + cmd := exec.CommandContext(ctx, "sh", "-c", o.script.Content) //nolint:gosec cmd.Env = env cmd.Dir = o.basePath return cmd, cancel, nil diff --git a/pkg/runner/processors/bindings.go b/pkg/runner/processors/bindings.go index 17fe6ad5f..f17c687dd 100644 --- a/pkg/runner/processors/bindings.go +++ b/pkg/runner/processors/bindings.go @@ -2,22 +2,12 @@ package processors import ( "context" - "fmt" - "regexp" - "strings" "github.com/jmespath-community/go-jmespath/pkg/binding" "github.com/kyverno/chainsaw/pkg/apis/v1alpha1" mutation "github.com/kyverno/chainsaw/pkg/mutate" "github.com/kyverno/chainsaw/pkg/runner/functions" "github.com/kyverno/kyverno-json/pkg/engine/template" - "k8s.io/apimachinery/pkg/util/sets" -) - -var ( - identifier = regexp.MustCompile(`^\w+$`) - forbiddenNames = []string{"namespace", "client", "error", "values", "stdout", "stderr"} - forbidden = sets.New(forbiddenNames...) ) func registerBindings(ctx context.Context, bindings binding.Bindings, variables ...v1alpha1.Binding) (binding.Bindings, error) { @@ -25,7 +15,7 @@ func registerBindings(ctx context.Context, bindings binding.Bindings, variables bindings = binding.NewBindings() } for _, variable := range variables { - if err := checkBindingName(variable.Name); err != nil { + if err := variable.CheckName(); err != nil { return bindings, err } patched, err := mutation.Mutate(ctx, nil, mutation.Parse(ctx, variable.Value.Value), nil, bindings, template.WithFunctionCaller(functions.Caller)) @@ -36,13 +26,3 @@ func registerBindings(ctx context.Context, bindings binding.Bindings, variables } return bindings, nil } - -func checkBindingName(name string) error { - if forbidden.Has(name) { - return fmt.Errorf("binding name is forbidden (%s), it must not be (%s)", name, strings.Join(forbiddenNames, ", ")) - } - if !identifier.MatchString(name) { - return fmt.Errorf("invalid binding name %s", name) - } - return nil -} diff --git a/testdata/e2e/examples/CATALOG.md b/testdata/e2e/examples/CATALOG.md index 8ebad8a38..a835d1266 100644 --- a/testdata/e2e/examples/CATALOG.md +++ b/testdata/e2e/examples/CATALOG.md @@ -14,6 +14,7 @@ - [namespace-template](namespace-template/README.md) - [non-resource-assertion](non-resource-assertion/README.md) - [patch](patch/README.md) +- [script-env](script-env/README.md) - [sleep](sleep/README.md) - [template](template/README.md) - [timeout](timeout/README.md) diff --git a/testdata/e2e/examples/script-env/README.md b/testdata/e2e/examples/script-env/README.md new file mode 100644 index 000000000..98c304dc2 --- /dev/null +++ b/testdata/e2e/examples/script-env/README.md @@ -0,0 +1,20 @@ +# Test: `script-env` + +*No description* + +### Steps + +| # | Name | Try | Catch | Finally | +|:-:|---|:-:|:-:|:-:| +| 1 | [step-1](#step-step-1) | 2 | 0 | 0 | + +## Step: `step-1` + +*No description* + +### Try + +| # | Operation | Description | +|:-:|---|---| +| 1 | `script` | *No description* | +| 2 | `command` | *No description* | diff --git a/testdata/e2e/examples/script-env/chainsaw-test.yaml b/testdata/e2e/examples/script-env/chainsaw-test.yaml new file mode 100644 index 000000000..2ac88810d --- /dev/null +++ b/testdata/e2e/examples/script-env/chainsaw-test.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/kyverno/chainsaw/main/.schemas/json/test-chainsaw-v1alpha1.json +apiVersion: chainsaw.kyverno.io/v1alpha1 +kind: Test +metadata: + name: script-env +spec: + bindings: + - name: chainsaw + value: chainsaw + steps: + - bindings: + - name: hello + value: hello + try: + - script: + env: + - name: GREETINGS + value: (join(' ', [$hello, $chainsaw])) + content: echo $GREETINGS + check: + ($error): ~ + ($stdout): hello chainsaw + - command: + env: + - name: GREETINGS + value: (join(' ', [$hello, $chainsaw])) + entrypoint: echo + args: + - $GREETINGS + check: + ($error): ~ + ($stdout): hello chainsaw