Skip to content

Commit

Permalink
bt: add post apply checks
Browse files Browse the repository at this point in the history
  • Loading branch information
DavidGamba committed Oct 2, 2024
1 parent 85fdd31 commit 5cdac26
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 3 deletions.
12 changes: 12 additions & 0 deletions bt/config/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ type TerraformProfile struct {
Enabled bool
Commands []Command
} `json:"pre_apply_checks"`
PostApplyChecks struct {
Enabled bool
Commands []Command
} `json:"post_apply_checks"`
BinaryName string `json:"binary_name"`
Platforms []string `json:"platforms"`
}
Expand Down Expand Up @@ -57,6 +61,14 @@ func (t TerraformProfile) String() string {
}
output += fmt.Sprintf("%v", names)
}
if t.PostApplyChecks.Enabled {
output += ", post_apply_checks: "
names := []string{}
for _, cmd := range t.PostApplyChecks.Commands {
names = append(names, cmd.Name)
}
output += fmt.Sprintf("%v", names)
}
return output
}

Expand Down
12 changes: 9 additions & 3 deletions bt/terraform/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ func buildCMD(ctx context.Context, parent *getoptions.GetOpt) *getoptions.GetOpt
opt.Bool("detailed-exitcode", false)
opt.Bool("dry-run", false)
opt.Bool("ignore-cache", false, opt.Description("Ignore the cache and re-run the plan"), opt.Alias("ic"))
opt.Bool("no-checks", false, opt.Description("Do not run pre-apply checks"), opt.Alias("nc"))
opt.Bool("no-checks", false, opt.Description("Do not run pre-apply/post-apply checks"), opt.Alias("nc"))
opt.Bool("show", false, opt.Description("Show Terraform plan"))
opt.Bool("lock", false, opt.Description("Run 'terraform providers lock' after init"))
opt.Int("parallelism", 10*runtime.NumCPU())
Expand Down Expand Up @@ -89,11 +89,14 @@ func BuildRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error
if cfg.TFProfile[cfg.Profile(profile)].PreApplyChecks.Enabled {
tm.Add("checks", checksRun)
}
if show {
tm.Add("show", showPlanRun)
}
if apply {
tm.Add("apply", applyRun)
}
if show {
tm.Add("show", showPlanRun)
if cfg.TFProfile[cfg.Profile(profile)].PostApplyChecks.Enabled {
tm.Add("post-checks", postChecksRun)
}

g := dag.NewGraph(fmt.Sprintf("%s:build", component))
Expand All @@ -114,6 +117,9 @@ func BuildRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error
if cfg.TFProfile[cfg.Profile(profile)].PreApplyChecks.Enabled {
g.TaskDependsOn(tm.Get("apply"), tm.Get("checks"))
}
if cfg.TFProfile[cfg.Profile(profile)].PostApplyChecks.Enabled {
g.TaskDependsOn(tm.Get("post-checks"), tm.Get("apply"))
}
}
err = g.Validate(tm)
if err != nil {
Expand Down
161 changes: 161 additions & 0 deletions bt/terraform/checks.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,3 +208,164 @@ func checksRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error

return nil
}

func postChecksCMD(ctx context.Context, parent *getoptions.GetOpt) *getoptions.GetOpt {
opt := parent.NewCommand("post-checks", "Run post-apply checks")
opt.Bool("dry-run", false)
opt.StringSlice("var-file", 1, 1)
opt.Bool("no-checks", false, opt.Description("Do not run post-apply checks"), opt.Alias("nc"))
opt.Bool("ignore-cache", false, opt.Description("ignore the cache and re-run the checks"), opt.Alias("ic"))
opt.SetCommandFn(postChecksRun)

return opt
}

func postChecksRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error {
dryRun := opt.Value("dry-run").(bool)
profile := opt.Value("profile").(string)
varFiles := opt.Value("var-file").([]string)
ws := opt.Value("ws").(string)
ignoreCache := opt.Value("ignore-cache").(bool)
nc := opt.Value("no-checks").(bool)
if nc {
Logger.Printf("WARNING: no-checks flag passed. Skipping post-apply checks.\n")
return nil
}

cfg := config.ConfigFromContext(ctx)
component := ComponentFromContext(ctx)
dir := DirFromContext(ctx)
LogConfig(cfg, profile)

ws, err := updateWSIfSelected(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile), ws)
if err != nil {
return err
}

cwd, err := filepath.Abs(dir)
if err != nil {
return fmt.Errorf("failed to get current dir: %w", err)
}
if component == "." {
component = filepath.Base(cwd)
}
component = strings.Split(component, ":")[0]
Logger.Printf("component: %s\n", component)

ws, err = getWorkspace(cfg, profile, ws, varFiles)
if err != nil {
return err
}

checkFile := ""
if ws == "" {
checkFile = ".tf.postcheck"
} else {
checkFile = fmt.Sprintf(".tf.postcheck-%s", ws)
}
wsEnv := ws
if ws == "" {
wsEnv = "default"
}

env := map[string]string{
"CONFIG_ROOT": cfg.ConfigRoot,
"TF_WORKSPACE": wsEnv,
"BT_COMPONENT": component,
}

cmdFiles := []string{}
for _, cmd := range cfg.TFProfile[cfg.Profile(profile)].PostApplyChecks.Commands {
exp, err := fsmodtime.ExpandEnv(cmd.Files, env)
if err != nil {
return fmt.Errorf("failed to expand: %w", err)
}
for _, f := range exp {
if strings.HasPrefix(f, "/") {
cmdFiles = append(cmdFiles, filepath.Join("./", f))
} else {
cmdFiles = append(cmdFiles, filepath.Join("./", cwd, f))
}
}
}
globs, _, err := fsmodtime.Glob(os.DirFS("/"), false, cmdFiles)
if err != nil {
return fmt.Errorf("failed to glob sources: %w", err)
}

// Paths tested with fs.FS can't start with "/". See https://pkg.go.dev/io/fs#ValidPath
files, modified, err := fsmodtime.Target(os.DirFS("/"),
[]string{filepath.Join("./", cwd, checkFile)},
globs)
if err != nil {
Logger.Printf("failed to check changes for: '%s'\n", checkFile)
}

if !ignoreCache && !modified {
Logger.Printf("no changes: skipping check\n")
return nil
}
if len(files) > 0 {
modifiedFiles := []string{}
for _, f := range files {
rel, err := filepath.Rel(cwd, "/"+f)
if err != nil {
rel = f
}
modifiedFiles = append(modifiedFiles, rel)
}
Logger.Printf("modified: %v\n", modifiedFiles)
} else {
Logger.Printf("missing target: %v\n", checkFile)
}

dataDir := fmt.Sprintf("TF_DATA_DIR=%s", getDataDir(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile)))
Logger.Printf("export %s\n", dataDir)

for _, cmd := range cfg.TFProfile[cfg.Profile(profile)].PostApplyChecks.Commands {
Logger.Printf("running check: %s\n", cmd.Name)
exp, err := fsmodtime.ExpandEnv(cmd.Command, env)
if err != nil {
return fmt.Errorf("failed to expand: %w", err)
}
ri := run.CMDCtx(ctx, exp...).Stdin().Log().
Env(dataDir).
Env(fmt.Sprintf("CONFIG_ROOT=%s", cfg.ConfigRoot)).
Env(fmt.Sprintf("TF_WORKSPACE=%s", wsEnv)).
Env(fmt.Sprintf("BT_COMPONENT=%s", component)).
Dir(dir).DryRun(dryRun)
if cmd.OutputFile == "" {
err = ri.Run()
if err != nil {
return fmt.Errorf("failed to run: %w", err)
}
} else {
f, err := fsmodtime.ExpandEnv([]string{cmd.OutputFile}, env)
if err != nil {
return fmt.Errorf("failed to expand: %w", err)
}
fh, err := os.Create(filepath.Join(dir, f[0]))
if err != nil {
return fmt.Errorf("failed to create cmd output file: %w", err)
}
defer fh.Close()
err = ri.Run(fh, os.Stderr)
if err != nil {
return fmt.Errorf("failed to run: %w", err)
}
Logger.Printf("Saving check output to file: %s\n", filepath.Join(dir, f[0]))
}
}

if dryRun {
return nil
}

fh, err := os.Create(checkFile)
if err != nil {
return fmt.Errorf("failed to create file: %w", err)
}
fh.Close()

return nil
}
1 change: 1 addition & 0 deletions bt/terraform/terraform.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func NewCommand(ctx context.Context, parent *getoptions.GetOpt) *getoptions.GetO
// Custom
buildCMD(ctx, opt)
checksCMD(ctx, opt)
postChecksCMD(ctx, opt)

return opt
}
Expand Down

0 comments on commit 5cdac26

Please sign in to comment.