Skip to content

Commit

Permalink
feat: add env vars support in command and script
Browse files Browse the repository at this point in the history
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
  • Loading branch information
eddycharly committed Feb 24, 2024
1 parent 701598e commit 89282d5
Show file tree
Hide file tree
Showing 8 changed files with 149 additions and 37 deletions.
39 changes: 39 additions & 0 deletions pkg/apis/v1alpha1/binding.go
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
}
22 changes: 12 additions & 10 deletions pkg/runner/operations/command/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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()
}
Expand All @@ -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 {
Expand All @@ -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
Expand Down
35 changes: 35 additions & 0 deletions pkg/runner/operations/internal/env.go
Original file line number Diff line number Diff line change
@@ -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
}
16 changes: 10 additions & 6 deletions pkg/runner/operations/script/operation.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand All @@ -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 {
Expand All @@ -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
Expand Down
22 changes: 1 addition & 21 deletions pkg/runner/processors/bindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,20 @@ 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) {
if bindings == nil {
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))
Expand All @@ -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
}
1 change: 1 addition & 0 deletions testdata/e2e/examples/CATALOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions testdata/e2e/examples/script-env/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Test: `script-env`

*No description*

### Steps

| # | Name | Try | Catch | Finally |
|:-:|---|:-:|:-:|:-:|
| 1 | [step-1](#step-step-1) | 1 | 0 | 0 |

## Step: `step-1`

*No description*

### Try

| # | Operation | Description |
|:-:|---|---|
| 1 | `script` | *No description* |
32 changes: 32 additions & 0 deletions testdata/e2e/examples/script-env/chainsaw-test.yaml
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 89282d5

Please sign in to comment.