diff --git a/bt/terraform/init.go b/bt/terraform/init.go index 3fd9169..649ee36 100644 --- a/bt/terraform/init.go +++ b/bt/terraform/init.go @@ -26,10 +26,10 @@ func initRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error { cfg := config.ConfigFromContext(ctx) dir := DirFromContext(ctx) LogConfig(cfg, profile) + os.Setenv("CONFIG_ROOT", cfg.ConfigRoot) cmd := []string{cfg.TFProfile[cfg.Profile(profile)].BinaryName, "init"} - os.Setenv("CONFIG_ROOT", cfg.ConfigRoot) for _, bvars := range cfg.TFProfile[cfg.Profile(profile)].Init.BackendConfig { b := strings.ReplaceAll(bvars, "~", "$HOME") bb, err := fsmodtime.ExpandEnv([]string{b}) diff --git a/bt/terraform/init_test.go b/bt/terraform/init_test.go index 2f7b949..9e3fd07 100644 --- a/bt/terraform/init_test.go +++ b/bt/terraform/init_test.go @@ -29,7 +29,7 @@ func TestInit(t *testing.T) { return fmt.Errorf("unexpected dir: %s", r.GetDir()) } if !slices.Equal(r.Cmd, []string{"terraform", "init", "-no-color"}) { - return fmt.Errorf("unexpected config: %v", r.Cmd) + return fmt.Errorf("unexpected cmd: %v", r.Cmd) } for _, e := range r.GetEnv() { if strings.Contains(e, "TF_DATA_DIR") { @@ -64,7 +64,7 @@ func TestInit(t *testing.T) { return fmt.Errorf("unexpected dir: %s", r.GetDir()) } if !slices.Equal(r.Cmd, []string{"tofu", "init", "-backend-config", "/home/user/dev-credentials.json", "-no-color"}) { - return fmt.Errorf("unexpected config: %v", r.Cmd) + return fmt.Errorf("unexpected cmd: %v", r.Cmd) } for _, e := range r.GetEnv() { if strings.Contains(e, "TF_DATA_DIR") { @@ -101,7 +101,7 @@ func TestInit(t *testing.T) { return fmt.Errorf("unexpected dir: %s", r.GetDir()) } if !slices.Equal(r.Cmd, []string{"tofu", "init", "-backend-config", "/home/user/dev-credentials.json", "-no-color"}) { - return fmt.Errorf("unexpected config: %v", r.Cmd) + return fmt.Errorf("unexpected cmd: %v", r.Cmd) } for _, e := range r.GetEnv() { if strings.Contains(e, "TF_DATA_DIR") { @@ -137,7 +137,7 @@ func TestInit(t *testing.T) { return fmt.Errorf("unexpected dir: %s", r.GetDir()) } if !slices.Equal(r.Cmd, []string{"terraform", "init", "-backend-config", "/tmp/terraform-project/prod-credentials.json", "-no-color"}) { - return fmt.Errorf("unexpected config: %v", r.Cmd) + return fmt.Errorf("unexpected cmd: %v", r.Cmd) } for _, e := range r.GetEnv() { if strings.Contains(e, "TF_DATA_DIR") { diff --git a/bt/terraform/plan.go b/bt/terraform/plan.go index 9dd46c4..7db002b 100644 --- a/bt/terraform/plan.go +++ b/bt/terraform/plan.go @@ -41,7 +41,9 @@ func planRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error { ws := opt.Value("ws").(string) cfg := config.ConfigFromContext(ctx) + dir := DirFromContext(ctx) LogConfig(cfg, profile) + os.Setenv("CONFIG_ROOT", cfg.ConfigRoot) ws, err := updateWSIfSelected(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile), ws) if err != nil { @@ -58,7 +60,7 @@ func planRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error { return err } - varFiles, err = AddVarFileIfWorkspaceSelected(cfg, profile, ws, varFiles) + varFiles, err = AddVarFileIfWorkspaceSelected(cfg, profile, dir, ws, varFiles) if err != nil { return err } @@ -174,7 +176,7 @@ func planRun(ctx context.Context, opt *getoptions.GetOpt, args []string) error { dataDir := fmt.Sprintf("TF_DATA_DIR=%s", getDataDir(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile))) Logger.Printf("export %s\n", dataDir) - ri := run.CMDCtx(ctx, cmd...).Stdin().Log().Env(dataDir) + ri := run.CMDCtx(ctx, cmd...).Stdin().Log().Env(dataDir).Dir(dir) if ws != "" { wsEnv := fmt.Sprintf("TF_WORKSPACE=%s", ws) Logger.Printf("export %s\n", wsEnv) diff --git a/bt/terraform/plan_test.go b/bt/terraform/plan_test.go new file mode 100644 index 0000000..ebf48ae --- /dev/null +++ b/bt/terraform/plan_test.go @@ -0,0 +1,186 @@ +package terraform + +import ( + "context" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "testing" + + "github.com/DavidGamba/dgtools/bt/config" + "github.com/DavidGamba/dgtools/buildutils" + "github.com/DavidGamba/dgtools/run" + "github.com/DavidGamba/go-getoptions" +) + +func TestPlan(t *testing.T) { + t.Setenv("HOME", "/home/user") + + t.Run("TestPlan without config", func(t *testing.T) { + buf := setupLogging() + ctx := context.Background() + cfg, _, _ := config.Get(ctx, "x") + ctx = config.NewConfigContext(ctx, cfg) + tDir := t.TempDir() + ctx = NewDirContext(ctx, tDir) + mock := run.CMDCtx(ctx).Mock(func(r *run.RunInfo) error { + if r.GetDir() != tDir { + return fmt.Errorf("unexpected dir: %s", r.GetDir()) + } + if !slices.Equal(r.Cmd, []string{"terraform", "plan", "-out", ".tf.plan", "-no-color"}) { + return fmt.Errorf("unexpected cmd: %v", r.Cmd) + } + for _, e := range r.GetEnv() { + if strings.Contains(e, "TF_DATA_DIR") { + if e != "TF_DATA_DIR=.terraform" { + return fmt.Errorf("unexpected env: %v", e) + } + } + } + return nil + }) + ctx = run.ContextWithRunInfo(ctx, mock) + opt := getoptions.New() + opt.String("profile", "default") + opt.Bool("destroy", false) + opt.Bool("detailed-exitcode", false) + opt.Bool("ignore-cache", false) + opt.String("ws", "") + opt.StringSlice("var-file", 1, 1) + opt.StringSlice("target", 1, 99) + opt.StringSlice("replace", 1, 99) + err := planRun(ctx, opt, []string{}) + if err != nil { + t.Errorf("TestPlan error: %s", err) + } + t.Log(buf.String()) + }) + + t.Run("TestPlan with default config but no valid profile selected", func(t *testing.T) { + buf := setupLogging() + ctx := context.Background() + cfg := getDefaultConfig() + ctx = config.NewConfigContext(ctx, cfg) + tDir := t.TempDir() + _ = os.MkdirAll(filepath.Join(tDir, "environments"), 0755) + _ = buildutils.Touch(filepath.Join(tDir, "environments", "dev.tfvars")) + ctx = NewDirContext(ctx, tDir) + mock := run.CMDCtx(ctx).Mock(func(r *run.RunInfo) error { + if r.GetDir() != tDir { + return fmt.Errorf("unexpected dir: %s", r.GetDir()) + } + if !slices.Equal(r.Cmd, []string{"tofu", "plan", "-out", ".tf.plan-dev", "-var-file", "/home/user/dev-backend-config.json", "-var-file", "environments/dev.tfvars", "-no-color"}) { + return fmt.Errorf("unexpected cmd: %v", r.Cmd) + } + for _, e := range r.GetEnv() { + if strings.Contains(e, "TF_DATA_DIR") { + if e != "TF_DATA_DIR=.terraform" { + return fmt.Errorf("unexpected env: %v", e) + } + } + } + return nil + }) + ctx = run.ContextWithRunInfo(ctx, mock) + opt := getoptions.New() + opt.String("profile", "default") + opt.Bool("destroy", false) + opt.Bool("detailed-exitcode", false) + opt.Bool("ignore-cache", false) + opt.String("ws", "dev") + opt.StringSlice("var-file", 1, 1) + opt.StringSlice("target", 1, 99) + opt.StringSlice("replace", 1, 99) + err := planRun(ctx, opt, []string{}) + if err != nil { + t.Errorf("TestPlan error: %s", err) + } + t.Log(buf.String()) + }) + + t.Run("TestPlan with default config and dev profile selected", func(t *testing.T) { + buf := setupLogging() + ctx := context.Background() + cfg := getDefaultConfig() + ctx = config.NewConfigContext(ctx, cfg) + tDir := t.TempDir() + _ = os.MkdirAll(filepath.Join(tDir, "environments"), 0755) + _ = buildutils.Touch(filepath.Join(tDir, "environments", "dev.tfvars")) + ctx = NewDirContext(ctx, tDir) + mock := run.CMDCtx(ctx).Mock(func(r *run.RunInfo) error { + if r.GetDir() != tDir { + return fmt.Errorf("unexpected dir: %s", r.GetDir()) + } + if !slices.Equal(r.Cmd, []string{"tofu", "plan", "-out", ".tf.plan-dev", "-var-file", "/home/user/dev-backend-config.json", "-var-file", "environments/dev.tfvars", "-no-color"}) { + return fmt.Errorf("unexpected cmd: %v", r.Cmd) + } + for _, e := range r.GetEnv() { + if strings.Contains(e, "TF_DATA_DIR") { + if e != "TF_DATA_DIR=.terraform" { + return fmt.Errorf("unexpected env: %v", e) + } + } + } + return nil + }) + ctx = run.ContextWithRunInfo(ctx, mock) + opt := getoptions.New() + opt.String("profile", "dev") + opt.Bool("destroy", false) + opt.Bool("detailed-exitcode", false) + opt.Bool("ignore-cache", false) + opt.String("ws", "dev") + opt.StringSlice("var-file", 1, 1) + opt.StringSlice("target", 1, 99) + opt.StringSlice("replace", 1, 99) + err := planRun(ctx, opt, []string{}) + if err != nil { + t.Errorf("TestPlan error: %s", err) + } + t.Log(buf.String()) + }) + + t.Run("TestPlan with default config and prod profile selected", func(t *testing.T) { + buf := setupLogging() + ctx := context.Background() + cfg := getDefaultConfig() + ctx = config.NewConfigContext(ctx, cfg) + tDir := t.TempDir() + _ = os.MkdirAll(filepath.Join(tDir, "environments"), 0755) + _ = buildutils.Touch(filepath.Join(tDir, "environments", "prod.tfvars")) + ctx = NewDirContext(ctx, tDir) + mock := run.CMDCtx(ctx).Mock(func(r *run.RunInfo) error { + if r.GetDir() != tDir { + return fmt.Errorf("unexpected dir: %s", r.GetDir()) + } + if !slices.Equal(r.Cmd, []string{"terraform", "plan", "-out", ".tf.plan-prod", "-var-file", "/tmp/terraform-project/prod-backend-config.json", "-var-file", "environments/prod.tfvars", "-no-color"}) { + return fmt.Errorf("unexpected cmd: %v", r.Cmd) + } + for _, e := range r.GetEnv() { + if strings.Contains(e, "TF_DATA_DIR") { + if e != "TF_DATA_DIR=.terraform-prod" { + return fmt.Errorf("unexpected env: %v", e) + } + } + } + return nil + }) + ctx = run.ContextWithRunInfo(ctx, mock) + opt := getoptions.New() + opt.String("profile", "prod") + opt.Bool("destroy", false) + opt.Bool("detailed-exitcode", false) + opt.Bool("ignore-cache", false) + opt.String("ws", "prod") + opt.StringSlice("var-file", 1, 1) + opt.StringSlice("target", 1, 99) + opt.StringSlice("replace", 1, 99) + err := planRun(ctx, opt, []string{}) + if err != nil { + t.Errorf("TestPlan error: %s", err) + } + t.Log(buf.String()) + }) +} diff --git a/bt/terraform/terraform.go b/bt/terraform/terraform.go index 98a9cd3..d4ceec2 100644 --- a/bt/terraform/terraform.go +++ b/bt/terraform/terraform.go @@ -159,11 +159,11 @@ func getWorkspace(cfg *config.Config, profile, ws string, varFiles []string) (st // If a workspace is selected automatically insert a var file matching the workspace. // If the var file is already present then don't add it again. -func AddVarFileIfWorkspaceSelected(cfg *config.Config, profile, ws string, varFiles []string) ([]string, error) { +func AddVarFileIfWorkspaceSelected(cfg *config.Config, profile, dir, ws string, varFiles []string) ([]string, error) { if ws != "" { glob := fmt.Sprintf("%s/%s.tfvars*", cfg.TFProfile[cfg.Profile(profile)].Workspaces.Dir, ws) Logger.Printf("ws: %s, glob: %s\n", ws, glob) - ff, _, err := fsmodtime.Glob(os.DirFS("."), true, []string{glob}) + ff, _, err := fsmodtime.Glob(os.DirFS(dir), true, []string{glob}) if err != nil { return varFiles, fmt.Errorf("failed to glob ws files: %w", err) } @@ -185,9 +185,10 @@ func getDefaultVarFiles(cfg *config.Config, profile string) ([]string, error) { if err != nil { return varFiles, fmt.Errorf("failed to expand: %w", err) } - if _, err := os.Stat(vv[0]); err == nil { - varFiles = append(varFiles, vv[0]) - } + // TODO: Consider re-introducing validation + // if _, err := os.Stat(vv[0]); err == nil { + // } + varFiles = append(varFiles, vv[0]) } return varFiles, nil } diff --git a/bt/terraform/varfile_cmd.go b/bt/terraform/varfile_cmd.go index 9219ea6..2c89619 100644 --- a/bt/terraform/varfile_cmd.go +++ b/bt/terraform/varfile_cmd.go @@ -65,6 +65,7 @@ func varFileCMDRun(fn VarFileCMDer, cmd ...string) getoptions.CommandFn { ws := opt.Value("ws").(string) cfg := config.ConfigFromContext(ctx) + dir := DirFromContext(ctx) LogConfig(cfg, profile) ws, err := updateWSIfSelected(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile), ws) @@ -82,7 +83,7 @@ func varFileCMDRun(fn VarFileCMDer, cmd ...string) getoptions.CommandFn { return err } - varFiles, err = AddVarFileIfWorkspaceSelected(cfg, profile, ws, varFiles) + varFiles, err = AddVarFileIfWorkspaceSelected(cfg, profile, dir, ws, varFiles) if err != nil { return err } @@ -101,7 +102,7 @@ func varFileCMDRun(fn VarFileCMDer, cmd ...string) getoptions.CommandFn { dataDir := fmt.Sprintf("TF_DATA_DIR=%s", getDataDir(cfg.Config.DefaultTerraformProfile, cfg.Profile(profile))) Logger.Printf("export %s\n", dataDir) - ri := run.CMDCtx(ctx, cmd...).Stdin().Log().Env(dataDir) + ri := run.CMDCtx(ctx, cmd...).Stdin().Log().Env(dataDir).Dir(dir) if ws != "" { wsEnv := fmt.Sprintf("TF_WORKSPACE=%s", ws) Logger.Printf("export %s\n", wsEnv)