Skip to content

Commit

Permalink
Add Dry Run for Shell Steps
Browse files Browse the repository at this point in the history
Add the capability to run dry runs on shell script steps.
Did some slight refactorings to add tests.
Also moved AKS Kubeconfig creation into Resource group to only call it once, as it is not required to run it for every stip since it can be reused
  • Loading branch information
janboll committed Nov 25, 2024
1 parent 9232253 commit 41c0bc6
Show file tree
Hide file tree
Showing 13 changed files with 353 additions and 50 deletions.
45 changes: 45 additions & 0 deletions tooling/templatize/pkg/config/types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package config

import "testing"

func TestGetByPath(t *testing.T) {
tests := []struct {
name string
vars Variables
path string
want any
found bool
}{
{
name: "simple",
vars: Variables{
"key": "value",
},
path: "key",
want: "value",
found: true,
},
{
name: "nested",
vars: Variables{
"key": Variables{
"key": "value",
},
},
path: "key.key",
want: "value",
found: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, found := tt.vars.GetByPath(tt.path)
if got != tt.want {
t.Errorf("Variables.GetByPath() got = %v, want %v", got, tt.want)
}
if found != tt.found {
t.Errorf("Variables.GetByPath() found = %v, want %v", found, tt.found)
}
})
}
}
6 changes: 4 additions & 2 deletions tooling/templatize/pkg/ev2/pipeline_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,10 @@ func TestPrecompilePipelineForEV2(t *testing.T) {
}
fmt.Println(p)
expectedParamsPath := "ev2-precompiled-test.bicepparam"
if p.ResourceGroups[0].Steps[1].Parameters != expectedParamsPath {
t.Errorf("expected parameters path %v, but got %v", expectedParamsPath, p.ResourceGroups[0].Steps[1].Parameters)

armStep := p.ResourceGroups[0].Steps[2]
if armStep.Parameters != expectedParamsPath {
t.Errorf("expected parameters path %v, but got %v", expectedParamsPath, armStep.Parameters)
}
// TODO improve test, check against fixture
}
2 changes: 1 addition & 1 deletion tooling/templatize/pkg/pipeline/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func inspectVars(s *step, options *PipelineInspectOptions, writer io.Writer) err
var err error
switch s.Action {
case "Shell":
envVars, err = s.getEnvVars(options.Vars, false)
envVars, err = s.mapStepVariables(options.Vars)
default:
return fmt.Errorf("inspecting step variables not implemented for action type %s", s.Action)
}
Expand Down
41 changes: 39 additions & 2 deletions tooling/templatize/pkg/pipeline/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,19 @@ func (rg *resourceGroup) run(ctx context.Context, options *PipelineRunOptions) e
}

logger := logr.FromContextOrDiscard(ctx)

kubeconfigFile, err := prepareKubeConfig(ctx, executionTarget)
if kubeconfigFile != "" {
defer func() {
if err := os.Remove(kubeconfigFile); err != nil {
logger.V(5).Error(err, "failed to delete kubeconfig file", "kubeconfig", kubeconfigFile)
}
}()
}
if err != nil {
return fmt.Errorf("failed to prepare kubeconfig: %w", err)
}

for _, step := range rg.Steps {
if options.Step != "" && step.Name != options.Step {
// skip steps that don't match the specified step name
Expand All @@ -109,6 +122,7 @@ func (rg *resourceGroup) run(ctx context.Context, options *PipelineRunOptions) e
"aksCluster", executionTarget.AKSClusterName,
),
),
kubeconfigFile,
executionTarget, options,
)
if err != nil {
Expand All @@ -118,20 +132,43 @@ func (rg *resourceGroup) run(ctx context.Context, options *PipelineRunOptions) e
return nil
}

func (s *step) run(ctx context.Context, executionTarget *ExecutionTarget, options *PipelineRunOptions) error {
func (s *step) run(ctx context.Context, kubeconfigFile string, executionTarget *ExecutionTarget, options *PipelineRunOptions) error {
fmt.Println("\n---------------------")
if options.DryRun {
fmt.Println("This is a dry run!")
}
fmt.Println(s.description())
fmt.Print("\n")

switch s.Action {
case "Shell":
return s.runShellStep(ctx, executionTarget, options)
return s.runShellStep(ctx, kubeconfigFile, options)
case "ARM":
return s.runArmStep(ctx, executionTarget, options)
default:
return fmt.Errorf("unsupported action type %q", s.Action)
}
}

func prepareKubeConfig(ctx context.Context, executionTarget *ExecutionTarget) (string, error) {
logger := logr.FromContextOrDiscard(ctx)
kubeconfigFile := ""
if executionTarget.AKSClusterName != "" {
logger.V(5).Info("Building kubeconfig for AKS cluster")
kubeconfigFile, err := executionTarget.KubeConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to build kubeconfig for %s: %w", executionTarget.aksID(), err)
}
defer func() {
if err := os.Remove(kubeconfigFile); err != nil {
logger.V(5).Error(err, "failed to delete kubeconfig file", "kubeconfig", kubeconfigFile)
}
}()
logger.V(5).Info("kubeconfig set to shell execution environment", "kubeconfig", kubeconfigFile)
}
return kubeconfigFile, nil
}

func (s *step) description() string {
var details []string
switch s.Action {
Expand Down
88 changes: 44 additions & 44 deletions tooling/templatize/pkg/pipeline/shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package pipeline
import (
"context"
"fmt"
"os"
"maps"
"os/exec"

"github.com/go-logr/logr"
Expand All @@ -12,75 +12,75 @@ import (
"github.com/Azure/ARO-HCP/tooling/templatize/pkg/utils"
)

func (s *step) runShellStep(ctx context.Context, executionTarget *ExecutionTarget, options *PipelineRunOptions) error {
func (s *step) createCommand(ctx context.Context, dryRun bool, envVars map[string]string) (*exec.Cmd, bool) {
var cmd *exec.Cmd
if dryRun {
if s.DryRun.Command == nil && s.DryRun.EnvVars == nil {
return nil, true
}
for _, e := range s.DryRun.EnvVars {
envVars[e.Name] = e.Value
}
if s.DryRun.Command != nil {
cmd = exec.CommandContext(ctx, s.DryRun.Command[0], s.DryRun.Command[1:]...)
}
}
if cmd == nil {
// if dry-run is not enabled, use the actual command or also if no dry-run command is defined
cmd = exec.CommandContext(ctx, s.Command[0], s.Command[1:]...)
}
cmd.Env = append(cmd.Env, utils.MapToEnvVarArray(envVars)...)
return cmd, false
}

func (s *step) runShellStep(ctx context.Context, kubeconfigFile string, options *PipelineRunOptions) error {
if s.outputFunc == nil {
s.outputFunc = func(output string) {
fmt.Println(output)
}
}

logger := logr.FromContextOrDiscard(ctx)

// build ENV vars
envVars, err := s.getEnvVars(options.Vars, true)
stepVars, err := s.mapStepVariables(options.Vars)
if err != nil {
return fmt.Errorf("failed to build env vars: %w", err)
}

// prepare kubeconfig
if executionTarget.AKSClusterName != "" {
logger.V(5).Info("Building kubeconfig for AKS cluster")
kubeconfigFile, err := executionTarget.KubeConfig(ctx)
if err != nil {
return fmt.Errorf("failed to build kubeconfig for %s: %w", executionTarget.aksID(), err)
}
defer func() {
if err := os.Remove(kubeconfigFile); err != nil {
logger.V(5).Error(err, "failed to delete kubeconfig file", "kubeconfig", kubeconfigFile)
}
}()
envVars["KUBECONFIG"] = kubeconfigFile
logger.V(5).Info("kubeconfig set to shell execution environment", "kubeconfig", kubeconfigFile)
envVars := utils.GetOsVariable()

maps.Copy(envVars, stepVars)
// execute the command
cmd, skipCommand := s.createCommand(ctx, options.DryRun, envVars)
if skipCommand {
logger.V(5).Info("Skipping step '%s' due to missing dry-run configuiration", s.Name)
return nil
}

// TODO handle dry-run
if kubeconfigFile != "" {
cmd.Env = append(cmd.Env, fmt.Sprintf("KUBECONFIG=%s", kubeconfigFile))
}

// execute the command
logger.V(5).Info(fmt.Sprintf("Executing shell command: %s\n", s.Command), "command", s.Command)
cmd := exec.CommandContext(ctx, s.Command[0], s.Command[1:]...)
cmd.Env = append(cmd.Env, utils.MapToEnvVarArray(envVars)...)
output, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to execute shell command: %s %w", string(output), err)
}

// print the output of the command
fmt.Println(string(output))
s.outputFunc(string(output))

return nil
}

func (s *step) getEnvVars(vars config.Variables, includeOSEnvVars bool) (map[string]string, error) {
func (s *step) mapStepVariables(vars config.Variables) (map[string]string, error) {
envVars := make(map[string]string)
envVars["RUNS_IN_TEMPLATIZE"] = "1"
if includeOSEnvVars {
for k, v := range utils.GetOSEnvVarsAsMap() {
envVars[k] = v
}
}
for _, e := range s.Env {
value, found := vars.GetByPath(e.ConfigRef)
if !found {
return nil, fmt.Errorf("failed to lookup config reference %s for %s", e.ConfigRef, e.Name)
}
envVars[e.Name] = anyToString(value)
envVars[e.Name] = utils.AnyToString(value)
}
return envVars, nil
}

func anyToString(value any) string {
switch v := value.(type) {
case string:
return v
case int:
return fmt.Sprintf("%d", v)
case bool:
return fmt.Sprintf("%t", v)
default:
return fmt.Sprintf("%v", v)
}
}
Loading

0 comments on commit 41c0bc6

Please sign in to comment.