From 6921cf5913e2c921f16771b7cbd231393daa8b2f Mon Sep 17 00:00:00 2001 From: Aditya Thebe Date: Tue, 17 Oct 2023 18:46:20 +0545 Subject: [PATCH] feat: add support for env vars in exec check --- api/v1/checks.go | 12 +++ api/v1/zz_generated.deepcopy.go | 30 ++++++ checks/exec.go | 61 ++++++++---- config/deploy/crd.yaml | 123 +++++++++++++++++++++++++ config/schemas/canary.schema.json | 30 ++++++ config/schemas/component.schema.json | 30 ++++++ config/schemas/health_exec.schema.json | 30 ++++++ config/schemas/topology.schema.json | 30 ++++++ fixtures/minimal/exec_env.yaml | 18 ++++ 9 files changed, 345 insertions(+), 19 deletions(-) create mode 100644 fixtures/minimal/exec_env.yaml diff --git a/api/v1/checks.go b/api/v1/checks.go index 5e4db0a0c..1025a6f17 100644 --- a/api/v1/checks.go +++ b/api/v1/checks.go @@ -968,6 +968,14 @@ type ExecConnections struct { Azure *AzureConnection `yaml:"azure,omitempty" json:"azure,omitempty"` } +type GitCheckout struct { + URL string `yaml:"url,omitempty" json:"url,omitempty"` + Connection string `yaml:"connection,omitempty" json:"connection,omitempty"` + Username types.EnvVar `yaml:"username,omitempty" json:"username,omitempty"` + Password types.EnvVar `yaml:"password,omitempty" json:"password,omitempty"` + Certificate types.EnvVar `yaml:"certificate,omitempty" json:"certificate,omitempty"` +} + type ExecCheck struct { Description `yaml:",inline" json:",inline"` Templatable `yaml:",inline" json:",inline"` @@ -975,6 +983,10 @@ type ExecCheck struct { // On windows executed via powershell and in darwin and linux executed using bash Script string `yaml:"script" json:"script"` Connections ExecConnections `yaml:"connections,omitempty" json:"connections,omitempty"` + // EnvVars are the environment variables that are accesible to exec processes + EnvVars []types.EnvVar `yaml:"env,omitempty" json:"env,omitempty"` + // Checkout details the git repository that should be mounted to the process + Checkout *GitCheckout `yaml:"checkout,omitempty" json:"checkout,omitempty"` } func (c ExecCheck) GetType() string { diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index aa828e78e..cbfaf3448 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -1600,6 +1600,18 @@ func (in *ExecCheck) DeepCopyInto(out *ExecCheck) { in.Description.DeepCopyInto(&out.Description) out.Templatable = in.Templatable in.Connections.DeepCopyInto(&out.Connections) + if in.EnvVars != nil { + in, out := &in.EnvVars, &out.EnvVars + *out = make([]types.EnvVar, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Checkout != nil { + in, out := &in.Checkout, &out.Checkout + *out = new(GitCheckout) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExecCheck. @@ -1846,6 +1858,24 @@ func (in *Git) DeepCopy() *Git { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GitCheckout) DeepCopyInto(out *GitCheckout) { + *out = *in + in.Username.DeepCopyInto(&out.Username) + in.Password.DeepCopyInto(&out.Password) + in.Certificate.DeepCopyInto(&out.Certificate) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GitCheckout. +func (in *GitCheckout) DeepCopy() *GitCheckout { + if in == nil { + return nil + } + out := new(GitCheckout) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitHubCheck) DeepCopyInto(out *GitHubCheck) { *out = *in diff --git a/checks/exec.go b/checks/exec.go index 3da78efcd..884ad671b 100644 --- a/checks/exec.go +++ b/checks/exec.go @@ -16,6 +16,7 @@ import ( v1 "github.com/flanksource/canary-checker/api/v1" "github.com/flanksource/canary-checker/pkg" "github.com/flanksource/commons/logger" + "github.com/flanksource/duty/types" ) type ExecChecker struct { @@ -36,27 +37,55 @@ func (c *ExecChecker) Run(ctx *context.Context) pkg.Results { for _, conf := range ctx.Canary.Spec.Exec { results = append(results, c.Check(ctx, conf)...) } + return results } func (c *ExecChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { check := extConfig.(v1.ExecCheck) + for i, env := range check.EnvVars { + val, err := ctx.GetEnvValueFromCache(env) + if err != nil { + return []*pkg.CheckResult{pkg.Fail(check, ctx.Canary).Failf("error fetching env value (name=%s): %v", env.Name, err)} + } + + check.EnvVars[i].ValueStatic = val + } + switch runtime.GOOS { case "windows": - return execPowershell(check, ctx) + return execPowershell(ctx, check) default: - return execBash(check, ctx) + return execBash(ctx, check) } } -func execPowershell(check v1.ExecCheck, ctx *context.Context) pkg.Results { +func execPowershell(ctx *context.Context, check v1.ExecCheck) pkg.Results { result := pkg.Success(check, ctx.Canary) ps, err := osExec.LookPath("powershell.exe") if err != nil { result.Failf("powershell not found") } + args := []string{check.Script} - cmd := osExec.Command(ps, args...) + cmd := osExec.CommandContext(ctx, ps, args...) + cmd.Env = append(os.Environ(), envVarSlice(check.EnvVars)...) + return runCmd(cmd, result) +} + +func execBash(ctx *context.Context, check v1.ExecCheck) pkg.Results { + result := pkg.Success(check, ctx.Canary) + fields := strings.Fields(check.Script) + if len(fields) == 0 { + return []*pkg.CheckResult{result.Failf("no script provided")} + } + + cmd := osExec.CommandContext(ctx, "bash", "-c", check.Script) + cmd.Env = append(os.Environ(), envVarSlice(check.EnvVars)...) + if err := setupConnection(ctx, check, cmd); err != nil { + return []*pkg.CheckResult{result.Failf("failed to setup connection: %v", err)} + } + return runCmd(cmd, result) } @@ -117,21 +146,6 @@ func setupConnection(ctx *context.Context, check v1.ExecCheck, cmd *osExec.Cmd) return nil } -func execBash(check v1.ExecCheck, ctx *context.Context) pkg.Results { - result := pkg.Success(check, ctx.Canary) - fields := strings.Fields(check.Script) - if len(fields) == 0 { - return []*pkg.CheckResult{result.Failf("no script provided")} - } - - cmd := osExec.Command("bash", "-c", check.Script) - if err := setupConnection(ctx, check, cmd); err != nil { - return []*pkg.CheckResult{result.Failf("failed to setup connection: %v", err)} - } - - return runCmd(cmd, result) -} - func runCmd(cmd *osExec.Cmd, result *pkg.CheckResult) (results pkg.Results) { var stdout bytes.Buffer var stderr bytes.Buffer @@ -174,6 +188,15 @@ func saveConfig(configTemplate *textTemplate.Template, view any) (string, error) return configPath, nil } +func envVarSlice(envs []types.EnvVar) []string { + result := make([]string, len(envs)) + for i, env := range envs { + result[i] = fmt.Sprintf("%s=%s", env.Name, env.ValueStatic) + } + + return result +} + var ( awsConfigTemplate *textTemplate.Template gcloudConfigTemplate *textTemplate.Template diff --git a/config/deploy/crd.yaml b/config/deploy/crd.yaml index 8211b31c5..eac1232e7 100644 --- a/config/deploy/crd.yaml +++ b/config/deploy/crd.yaml @@ -2105,6 +2105,98 @@ spec: exec: items: properties: + checkout: + description: Checkout details the git repository that should be mounted to the process + properties: + certificate: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + connection: + type: string + password: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + url: + type: string + username: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + type: object connections: properties: aws: @@ -2324,6 +2416,37 @@ spec: template: type: string type: object + env: + description: EnvVars are the environment variables that are accesible to exec processes + items: + properties: + name: + type: string + value: + type: string + valueFrom: + properties: + configMapKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + type: object + type: object + type: object + type: array icon: description: Icon for overwriting default icon on the dashboard type: string diff --git a/config/schemas/canary.schema.json b/config/schemas/canary.schema.json index f380d963e..660b42ec4 100644 --- a/config/schemas/canary.schema.json +++ b/config/schemas/canary.schema.json @@ -1394,6 +1394,15 @@ }, "connections": { "$ref": "#/$defs/ExecConnections" + }, + "env": { + "items": { + "$ref": "#/$defs/EnvVar" + }, + "type": "array" + }, + "checkout": { + "$ref": "#/$defs/GitCheckout" } }, "additionalProperties": false, @@ -1560,6 +1569,27 @@ "instance" ] }, + "GitCheckout": { + "properties": { + "url": { + "type": "string" + }, + "connection": { + "type": "string" + }, + "username": { + "$ref": "#/$defs/EnvVar" + }, + "password": { + "$ref": "#/$defs/EnvVar" + }, + "certificate": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object" + }, "GitHubCheck": { "properties": { "description": { diff --git a/config/schemas/component.schema.json b/config/schemas/component.schema.json index c5f40cf4f..791c2e90c 100644 --- a/config/schemas/component.schema.json +++ b/config/schemas/component.schema.json @@ -1573,6 +1573,15 @@ }, "connections": { "$ref": "#/$defs/ExecConnections" + }, + "env": { + "items": { + "$ref": "#/$defs/EnvVar" + }, + "type": "array" + }, + "checkout": { + "$ref": "#/$defs/GitCheckout" } }, "additionalProperties": false, @@ -1772,6 +1781,27 @@ "instance" ] }, + "GitCheckout": { + "properties": { + "url": { + "type": "string" + }, + "connection": { + "type": "string" + }, + "username": { + "$ref": "#/$defs/EnvVar" + }, + "password": { + "$ref": "#/$defs/EnvVar" + }, + "certificate": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object" + }, "GitHubCheck": { "properties": { "description": { diff --git a/config/schemas/health_exec.schema.json b/config/schemas/health_exec.schema.json index afe65cfc6..a70644733 100644 --- a/config/schemas/health_exec.schema.json +++ b/config/schemas/health_exec.schema.json @@ -133,6 +133,15 @@ }, "connections": { "$ref": "#/$defs/ExecConnections" + }, + "env": { + "items": { + "$ref": "#/$defs/EnvVar" + }, + "type": "array" + }, + "checkout": { + "$ref": "#/$defs/GitCheckout" } }, "additionalProperties": false, @@ -172,6 +181,27 @@ "additionalProperties": false, "type": "object" }, + "GitCheckout": { + "properties": { + "url": { + "type": "string" + }, + "connection": { + "type": "string" + }, + "username": { + "$ref": "#/$defs/EnvVar" + }, + "password": { + "$ref": "#/$defs/EnvVar" + }, + "certificate": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object" + }, "Labels": { "patternProperties": { ".*": { diff --git a/config/schemas/topology.schema.json b/config/schemas/topology.schema.json index dca59a7db..2460b6d40 100644 --- a/config/schemas/topology.schema.json +++ b/config/schemas/topology.schema.json @@ -1543,6 +1543,15 @@ }, "connections": { "$ref": "#/$defs/ExecConnections" + }, + "env": { + "items": { + "$ref": "#/$defs/EnvVar" + }, + "type": "array" + }, + "checkout": { + "$ref": "#/$defs/GitCheckout" } }, "additionalProperties": false, @@ -1742,6 +1751,27 @@ "instance" ] }, + "GitCheckout": { + "properties": { + "url": { + "type": "string" + }, + "connection": { + "type": "string" + }, + "username": { + "$ref": "#/$defs/EnvVar" + }, + "password": { + "$ref": "#/$defs/EnvVar" + }, + "certificate": { + "$ref": "#/$defs/EnvVar" + } + }, + "additionalProperties": false, + "type": "object" + }, "GitHubCheck": { "properties": { "description": { diff --git a/fixtures/minimal/exec_env.yaml b/fixtures/minimal/exec_env.yaml new file mode 100644 index 000000000..7f3f33fc0 --- /dev/null +++ b/fixtures/minimal/exec_env.yaml @@ -0,0 +1,18 @@ +apiVersion: canaries.flanksource.com/v1 +kind: Canary +metadata: + name: exec-env +spec: + interval: 30 + exec: + - name: exec-env + description: "exec with env" + script: | + echo -n ${FL_HELLO} ${FL_WORLD} + env: + - name: FL_HELLO + value: "hello" + - name: FL_WORLD + value: "world" + test: + expr: 'results.stdout == "hello world"'