From 8cbf41887b42100d093816ad4bc3e83c0da4620b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Charles-Edouard=20Br=C3=A9t=C3=A9ch=C3=A9?= Date: Sat, 24 Feb 2024 11:08:40 +0100 Subject: [PATCH] feat: add env vars support in command and script (#963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add env vars support in command and script Signed-off-by: Charles-Edouard Brétéché * feat: add env vars support in command and script Signed-off-by: Charles-Edouard Brétéché --------- Signed-off-by: Charles-Edouard Brétéché --- .crds/chainsaw.kyverno.io_tests.yaml | 102 +++++++++++ .crds/chainsaw.kyverno.io_teststeps.yaml | 102 +++++++++++ .schemas/json/test-chainsaw-v1alpha1.json | 168 ++++++++++++++++++ pkg/apis/v1alpha1/binding.go | 39 ++++ pkg/apis/v1alpha1/command.go | 4 + pkg/apis/v1alpha1/script.go | 4 + pkg/apis/v1alpha1/zz_generated.deepcopy.go | 14 ++ pkg/data/crds/chainsaw.kyverno.io_tests.yaml | 102 +++++++++++ .../crds/chainsaw.kyverno.io_teststeps.yaml | 102 +++++++++++ .../schemas/json/test-chainsaw-v1alpha1.json | 168 ++++++++++++++++++ pkg/runner/operations/command/operation.go | 22 +-- pkg/runner/operations/internal/env.go | 35 ++++ pkg/runner/operations/script/operation.go | 16 +- pkg/runner/processors/bindings.go | 22 +-- testdata/e2e/examples/CATALOG.md | 1 + testdata/e2e/examples/script-env/README.md | 20 +++ .../examples/script-env/chainsaw-test.yaml | 32 ++++ website/docs/apis/chainsaw.v1alpha1.md | 4 + 18 files changed, 920 insertions(+), 37 deletions(-) create mode 100644 pkg/runner/operations/internal/env.go create mode 100644 testdata/e2e/examples/script-env/README.md create mode 100644 testdata/e2e/examples/script-env/chainsaw-test.yaml diff --git a/.crds/chainsaw.kyverno.io_tests.yaml b/.crds/chainsaw.kyverno.io_tests.yaml index 0fa51c427..aabc3f97b 100644 --- a/.crds/chainsaw.kyverno.io_tests.yaml +++ b/.crds/chainsaw.kyverno.io_tests.yaml @@ -136,6 +136,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -385,6 +402,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -442,6 +476,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -691,6 +742,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -893,6 +961,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -1203,6 +1288,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce diff --git a/.crds/chainsaw.kyverno.io_teststeps.yaml b/.crds/chainsaw.kyverno.io_teststeps.yaml index 9f87038fe..65ea21658 100644 --- a/.crds/chainsaw.kyverno.io_teststeps.yaml +++ b/.crds/chainsaw.kyverno.io_teststeps.yaml @@ -78,6 +78,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -311,6 +328,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -365,6 +399,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -598,6 +649,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -791,6 +859,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -1089,6 +1174,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. diff --git a/.schemas/json/test-chainsaw-v1alpha1.json b/.schemas/json/test-chainsaw-v1alpha1.json index d52dfdd60..2770a328d 100644 --- a/.schemas/json/test-chainsaw-v1alpha1.json +++ b/.schemas/json/test-chainsaw-v1alpha1.json @@ -459,6 +459,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -873,6 +901,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -973,6 +1029,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -1387,6 +1471,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -1716,6 +1828,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -2181,6 +2321,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ 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/apis/v1alpha1/command.go b/pkg/apis/v1alpha1/command.go index 3fff7b4ce..376e22e6a 100644 --- a/pkg/apis/v1alpha1/command.go +++ b/pkg/apis/v1alpha1/command.go @@ -10,6 +10,10 @@ type Command struct { // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` + // Env defines additional environment variables. + // +optional + Env []Binding `json:"env,omitempty"` + // Cluster defines the target cluster (default cluster will be used if not specified and/or overridden). // +optional Cluster string `json:"cluster,omitempty"` diff --git a/pkg/apis/v1alpha1/script.go b/pkg/apis/v1alpha1/script.go index edc83236a..5fa1e3628 100644 --- a/pkg/apis/v1alpha1/script.go +++ b/pkg/apis/v1alpha1/script.go @@ -10,6 +10,10 @@ type Script struct { // +optional Timeout *metav1.Duration `json:"timeout,omitempty"` + // Env defines additional environment variables. + // +optional + Env []Binding `json:"env,omitempty"` + // Cluster defines the target cluster (default cluster will be used if not specified and/or overridden). // +optional Cluster string `json:"cluster,omitempty"` diff --git a/pkg/apis/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/v1alpha1/zz_generated.deepcopy.go index 4fede44f5..04fb3e707 100644 --- a/pkg/apis/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,13 @@ func (in *Command) DeepCopyInto(out *Command) { *out = new(v1.Duration) **out = **in } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]Binding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Args != nil { in, out := &in.Args, &out.Args *out = make([]string, len(*in)) @@ -834,6 +841,13 @@ func (in *Script) DeepCopyInto(out *Script) { *out = new(v1.Duration) **out = **in } + if in.Env != nil { + in, out := &in.Env, &out.Env + *out = make([]Binding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Check != nil { in, out := &in.Check, &out.Check *out = (*in).DeepCopy() diff --git a/pkg/data/crds/chainsaw.kyverno.io_tests.yaml b/pkg/data/crds/chainsaw.kyverno.io_tests.yaml index 0fa51c427..aabc3f97b 100644 --- a/pkg/data/crds/chainsaw.kyverno.io_tests.yaml +++ b/pkg/data/crds/chainsaw.kyverno.io_tests.yaml @@ -136,6 +136,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -385,6 +402,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -442,6 +476,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -691,6 +742,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -893,6 +961,23 @@ spec: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce @@ -1203,6 +1288,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set + as a binding in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce diff --git a/pkg/data/crds/chainsaw.kyverno.io_teststeps.yaml b/pkg/data/crds/chainsaw.kyverno.io_teststeps.yaml index 9f87038fe..65ea21658 100644 --- a/pkg/data/crds/chainsaw.kyverno.io_teststeps.yaml +++ b/pkg/data/crds/chainsaw.kyverno.io_teststeps.yaml @@ -78,6 +78,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -311,6 +328,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -365,6 +399,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -598,6 +649,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -791,6 +859,23 @@ spec: entrypoint: description: Entrypoint is the command entry point to run. type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. @@ -1089,6 +1174,23 @@ spec: description: Content defines a shell script (run with "sh -c ..."). type: string + env: + description: Env defines additional environment variables. + items: + description: Binding represents a key/value set as a binding + in an executing test. + properties: + name: + description: Name the name of the binding. + type: string + value: + description: Value value of the binding. + x-kubernetes-preserve-unknown-fields: true + required: + - name + - value + type: object + type: array skipLogOutput: description: SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise. diff --git a/pkg/data/schemas/json/test-chainsaw-v1alpha1.json b/pkg/data/schemas/json/test-chainsaw-v1alpha1.json index d52dfdd60..2770a328d 100644 --- a/pkg/data/schemas/json/test-chainsaw-v1alpha1.json +++ b/pkg/data/schemas/json/test-chainsaw-v1alpha1.json @@ -459,6 +459,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -873,6 +901,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -973,6 +1029,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -1387,6 +1471,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -1716,6 +1828,34 @@ "description": "Entrypoint is the command entry point to run.", "type": "string" }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ @@ -2181,6 +2321,34 @@ "null" ] }, + "env": { + "description": "Env defines additional environment variables.", + "type": [ + "array", + "null" + ], + "items": { + "description": "Binding represents a key/value set as a binding in an executing test.", + "type": [ + "object", + "null" + ], + "required": [ + "name", + "value" + ], + "properties": { + "name": { + "description": "Name the name of the binding.", + "type": "string" + }, + "value": { + "description": "Value value of the binding.", + "x-kubernetes-preserve-unknown-fields": true + } + } + } + }, "skipLogOutput": { "description": "SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.", "type": [ 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 diff --git a/website/docs/apis/chainsaw.v1alpha1.md b/website/docs/apis/chainsaw.v1alpha1.md index 32dca2589..f6fc562a9 100644 --- a/website/docs/apis/chainsaw.v1alpha1.md +++ b/website/docs/apis/chainsaw.v1alpha1.md @@ -94,10 +94,12 @@ during the testing process.

- [Apply](#chainsaw-kyverno-io-v1alpha1-Apply) - [Assert](#chainsaw-kyverno-io-v1alpha1-Assert) +- [Command](#chainsaw-kyverno-io-v1alpha1-Command) - [Create](#chainsaw-kyverno-io-v1alpha1-Create) - [Delete](#chainsaw-kyverno-io-v1alpha1-Delete) - [Error](#chainsaw-kyverno-io-v1alpha1-Error) - [Patch](#chainsaw-kyverno-io-v1alpha1-Patch) +- [Script](#chainsaw-kyverno-io-v1alpha1-Script) - [TestSpec](#chainsaw-kyverno-io-v1alpha1-TestSpec) - [TestStepSpec](#chainsaw-kyverno-io-v1alpha1-TestStepSpec) @@ -155,6 +157,7 @@ during the testing process.

| Field | Type | Required | Inline | Description | |---|---|---|---|---| | `timeout` | [`meta/v1.Duration`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration) | | |

Timeout for the operation. Overrides the global timeout set in the Configuration.

| +| `env` | [`[]Binding`](#chainsaw-kyverno-io-v1alpha1-Binding) | | |

Env defines additional environment variables.

| | `cluster` | `string` | | |

Cluster defines the target cluster (default cluster will be used if not specified and/or overridden).

| | `entrypoint` | `string` | :white_check_mark: | |

Entrypoint is the command entry point to run.

| | `args` | `[]string` | | |

Args is the command arguments.

| @@ -536,6 +539,7 @@ If a resource doesn't exist yet in the cluster it will fail.

| Field | Type | Required | Inline | Description | |---|---|---|---|---| | `timeout` | [`meta/v1.Duration`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1#Duration) | | |

Timeout for the operation. Overrides the global timeout set in the Configuration.

| +| `env` | [`[]Binding`](#chainsaw-kyverno-io-v1alpha1-Binding) | | |

Env defines additional environment variables.

| | `cluster` | `string` | | |

Cluster defines the target cluster (default cluster will be used if not specified and/or overridden).

| | `content` | `string` | | |

Content defines a shell script (run with "sh -c ...").

| | `skipLogOutput` | `bool` | | |

SkipLogOutput removes the output from the command. Useful for sensitive logs or to reduce noise.

|