diff --git a/.gitignore b/.gitignore index 289b82f42..a7bd0b67f 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ postgres-db/ ui/scripts/ Chart.lock chart/charts/ +.downloads diff --git a/api/v1/checks.go b/api/v1/checks.go index 1025a6f17..df5042558 100644 --- a/api/v1/checks.go +++ b/api/v1/checks.go @@ -974,6 +974,9 @@ type GitCheckout struct { 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"` + // Destination is the full path to where the contents of the URL should be downloaded to. + // If left empty, the sha256 hash of the URL will be used as the dir name. + Destination string `yaml:"destination,omitempty" json:"destination,omitempty"` } type ExecCheck struct { diff --git a/checks/exec.go b/checks/exec.go index 884ad671b..89371fba1 100644 --- a/checks/exec.go +++ b/checks/exec.go @@ -15,8 +15,9 @@ import ( "github.com/flanksource/canary-checker/api/external" v1 "github.com/flanksource/canary-checker/api/v1" "github.com/flanksource/canary-checker/pkg" + "github.com/flanksource/commons/files" + "github.com/flanksource/commons/hash" "github.com/flanksource/commons/logger" - "github.com/flanksource/duty/types" ) type ExecChecker struct { @@ -41,26 +42,65 @@ func (c *ExecChecker) Run(ctx *context.Context) pkg.Results { return results } -func (c *ExecChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { - check := extConfig.(v1.ExecCheck) - for i, env := range check.EnvVars { +type execEnv struct { + envs []string + mountPoint string +} + +func (c *ExecChecker) prepareEnvironment(ctx *context.Context, check v1.ExecCheck) (*execEnv, error) { + var result execEnv + + for _, 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)} + return nil, fmt.Errorf("error fetching env value (name=%s): %w", env.Name, err) + } + + result.envs = append(result.envs, fmt.Sprintf("%s=%s", env.Name, val)) + } + + if check.Checkout != nil { + if connection, err := ctx.HydrateConnectionByURL(check.Checkout.Connection); err != nil { + return nil, fmt.Errorf("error hydrating connection: %w", err) + } else if connection != nil { + check.Checkout.URL = connection.URL + } + + if check.Checkout.URL == "" { + return nil, fmt.Errorf("error checking out. missing URL") + } + + result.mountPoint = check.Checkout.Destination + if check.Checkout.Destination == "" { + pwd, _ := os.Getwd() + result.mountPoint = filepath.Join(pwd, ".downloads", hash.Sha256Hex(check.Checkout.URL)) + } + + if err := files.Getter(check.Checkout.URL, result.mountPoint); err != nil { + return nil, fmt.Errorf("error checking out %s: %w", check.Checkout.URL, err) } + } - check.EnvVars[i].ValueStatic = val + return &result, nil +} + +func (c *ExecChecker) Check(ctx *context.Context, extConfig external.Check) pkg.Results { + check := extConfig.(v1.ExecCheck) + + env, err := c.prepareEnvironment(ctx, check) + if err != nil { + return []*pkg.CheckResult{pkg.Fail(check, ctx.Canary).Failf("something went wrong while preparing exec env: %v", err)} } switch runtime.GOOS { case "windows": - return execPowershell(ctx, check) + return execPowershell(ctx, check, env) default: - return execBash(ctx, check) + return execBash(ctx, check, env) } } -func execPowershell(ctx *context.Context, check v1.ExecCheck) pkg.Results { +func execPowershell(ctx *context.Context, check v1.ExecCheck, envParams *execEnv) pkg.Results { result := pkg.Success(check, ctx.Canary) ps, err := osExec.LookPath("powershell.exe") if err != nil { @@ -69,11 +109,17 @@ func execPowershell(ctx *context.Context, check v1.ExecCheck) pkg.Results { args := []string{check.Script} cmd := osExec.CommandContext(ctx, ps, args...) - cmd.Env = append(os.Environ(), envVarSlice(check.EnvVars)...) + if len(envParams.envs) != 0 { + cmd.Env = append(os.Environ(), envParams.envs...) + } + if envParams.mountPoint != "" { + cmd.Dir = envParams.mountPoint + } + return runCmd(cmd, result) } -func execBash(ctx *context.Context, check v1.ExecCheck) pkg.Results { +func execBash(ctx *context.Context, check v1.ExecCheck, envParams *execEnv) pkg.Results { result := pkg.Success(check, ctx.Canary) fields := strings.Fields(check.Script) if len(fields) == 0 { @@ -81,7 +127,13 @@ func execBash(ctx *context.Context, check v1.ExecCheck) pkg.Results { } cmd := osExec.CommandContext(ctx, "bash", "-c", check.Script) - cmd.Env = append(os.Environ(), envVarSlice(check.EnvVars)...) + if len(envParams.envs) != 0 { + cmd.Env = append(os.Environ(), envParams.envs...) + } + if envParams.mountPoint != "" { + cmd.Dir = envParams.mountPoint + } + if err := setupConnection(ctx, check, cmd); err != nil { return []*pkg.CheckResult{result.Failf("failed to setup connection: %v", err)} } @@ -188,15 +240,6 @@ 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 eac1232e7..73236e103 100644 --- a/config/deploy/crd.yaml +++ b/config/deploy/crd.yaml @@ -2138,6 +2138,9 @@ spec: type: object connection: type: string + destination: + description: Destination is the full path to where the contents of the URL should be downloaded to. If left empty, the sha256 hash of the URL will be used as the dir name. + type: string password: properties: name: diff --git a/config/deploy/manifests.yaml b/config/deploy/manifests.yaml index 228a51f38..9337721d3 100644 --- a/config/deploy/manifests.yaml +++ b/config/deploy/manifests.yaml @@ -2105,6 +2105,101 @@ 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 + destination: + description: Destination is the full path to where the contents of the URL should be downloaded to. If left empty, the sha256 hash of the URL will be used as the dir name. + 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 +2419,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 660b42ec4..b40faea98 100644 --- a/config/schemas/canary.schema.json +++ b/config/schemas/canary.schema.json @@ -1585,6 +1585,9 @@ }, "certificate": { "$ref": "#/$defs/EnvVar" + }, + "destination": { + "type": "string" } }, "additionalProperties": false, diff --git a/config/schemas/component.schema.json b/config/schemas/component.schema.json index 791c2e90c..be7245517 100644 --- a/config/schemas/component.schema.json +++ b/config/schemas/component.schema.json @@ -1797,6 +1797,9 @@ }, "certificate": { "$ref": "#/$defs/EnvVar" + }, + "destination": { + "type": "string" } }, "additionalProperties": false, diff --git a/config/schemas/health_exec.schema.json b/config/schemas/health_exec.schema.json index a70644733..8e9f4948d 100644 --- a/config/schemas/health_exec.schema.json +++ b/config/schemas/health_exec.schema.json @@ -197,6 +197,9 @@ }, "certificate": { "$ref": "#/$defs/EnvVar" + }, + "destination": { + "type": "string" } }, "additionalProperties": false, diff --git a/config/schemas/topology.schema.json b/config/schemas/topology.schema.json index 2460b6d40..368a4e4ed 100644 --- a/config/schemas/topology.schema.json +++ b/config/schemas/topology.schema.json @@ -1767,6 +1767,9 @@ }, "certificate": { "$ref": "#/$defs/EnvVar" + }, + "destination": { + "type": "string" } }, "additionalProperties": false, diff --git a/fixtures/minimal/exec_checkout.yaml b/fixtures/minimal/exec_checkout.yaml new file mode 100644 index 000000000..344019a6a --- /dev/null +++ b/fixtures/minimal/exec_checkout.yaml @@ -0,0 +1,15 @@ +apiVersion: canaries.flanksource.com/v1 +kind: Canary +metadata: + name: exec-checkout +spec: + interval: 30 + exec: + - name: exec-checkout + description: "exec with git" + script: | + cat go.mod | head -n 1 + checkout: + url: github.com/flanksource/duty + test: + expr: 'results.stdout == "module github.com/flanksource/duty"'