From 95ebc7e4ee9d7e6c9aa8fe7eb4eba7a3ec89f08d Mon Sep 17 00:00:00 2001 From: Louis Garman <75728+leg100@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:29:23 +0100 Subject: [PATCH] feat: auto load workspace tfvars file (#45) --- README.md | 4 ++ internal/app/helpers_test.go | 2 +- internal/app/run_test.go | 52 +++++++++++++++++++ .../run_with_vars/modules/a/default.tfvars | 1 + .../testdata/run_with_vars/modules/a/main.tf | 11 ++++ internal/run/run.go | 15 ++++++ internal/run/run_test.go | 23 ++++++++ 7 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 internal/app/testdata/run_with_vars/modules/a/default.tfvars create mode 100644 internal/app/testdata/run_with_vars/modules/a/main.tf diff --git a/README.md b/README.md index ed025c4c..78367f47 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,10 @@ Environment variables are specified by prefixing the value with `PUG_` and appen The config file by default is expected to be found in the current working directory in which you invoke pug, and by default it's expected to be named `pug.yaml`. Override the default using the flag `-c` or environment variable `PUG_CONFIG`. +## Workspace Variables + +Pug automatically loads variables from a .tfvars file. It looks for a file named `.tfvars` in the module directory, where `` is the name of the workspace. For example, if the workspace is named `dev` then it'll look for `dev.tfvars`. If the file exists then it'll pass the name to `terraform plan`, e.g. for a workspace named `dev`, it'll invoke `terraform plan -vars-file=dev.tfvars`. + ## Resource hierarchy There are several types of resources in pug: diff --git a/internal/app/helpers_test.go b/internal/app/helpers_test.go index 3cc93a62..aa701b2c 100644 --- a/internal/app/helpers_test.go +++ b/internal/app/helpers_test.go @@ -133,7 +133,7 @@ type testLogger struct { func (l *testLogger) Write(b []byte) (int, error) { l.t.Helper() - //l.t.Log(string(b)) + l.t.Log(string(b)) return len(b), nil } diff --git a/internal/app/run_test.go b/internal/app/run_test.go index 914c6782..252dc2aa 100644 --- a/internal/app/run_test.go +++ b/internal/app/run_test.go @@ -1,7 +1,10 @@ package app import ( + "strings" "testing" + + "github.com/leg100/pug/internal" ) func TestRun(t *testing.T) { @@ -10,3 +13,52 @@ func TestRun(t *testing.T) { // Initialize and apply run on modules/a initAndApplyModuleA(t, tm) } + +func TestRun_WithVars(t *testing.T) { + tm := setup(t, "./testdata/run_with_vars") + + // Wait for module to be loaded + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "modules/a") + }) + + // Initialize module + tm.Type("i") + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "Terraform has been successfully initialized!") + }) + + // Go to module page + tm.Type("m") + + // Wait for default workspace to be loaded + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "default") + }) + + // Create plan for default workspace + tm.Type("p") + + // User should now be taken to the run page... + + // Expect to see summary of changes + waitFor(t, tm, func(s string) bool { + // Remove formatting + s = internal.StripAnsi(s) + return strings.Contains(s, "Changes to Outputs:") && + strings.Contains(s, `+ foo = "override"`) + }) + + // Apply plan and provide confirmation + tm.Type("a") + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "Proceed with apply (y/N)?") + }) + tm.Type("y") + + // Wait for apply to complete + waitFor(t, tm, func(s string) bool { + return strings.Contains(s, "Apply complete! Resources: 0 added, 0 changed, 0 destroyed.") && + strings.Contains(s, `foo = "override"`) + }) +} diff --git a/internal/app/testdata/run_with_vars/modules/a/default.tfvars b/internal/app/testdata/run_with_vars/modules/a/default.tfvars new file mode 100644 index 00000000..78237198 --- /dev/null +++ b/internal/app/testdata/run_with_vars/modules/a/default.tfvars @@ -0,0 +1 @@ +foo = "override" diff --git a/internal/app/testdata/run_with_vars/modules/a/main.tf b/internal/app/testdata/run_with_vars/modules/a/main.tf new file mode 100644 index 00000000..0039633a --- /dev/null +++ b/internal/app/testdata/run_with_vars/modules/a/main.tf @@ -0,0 +1,11 @@ +terraform { + backend "local" {} +} + +variable "foo" { + default = "bar" +} + +output "foo" { + value = var.foo +} diff --git a/internal/run/run.go b/internal/run/run.go index 05ee4bfb..93a7f892 100644 --- a/internal/run/run.go +++ b/internal/run/run.go @@ -55,6 +55,10 @@ type Run struct { // Call this function after every status update afterUpdate func(run *Run) + + // Name of tfvars file to pass to terraform plan. An empty string means + // there is no vars file. + varsFilename string } type CreateOptions struct { @@ -84,6 +88,14 @@ func newRun(mod *module.Module, ws *workspace.Workspace, opts CreateOptions) (*R return nil, err } + // Check if a tfvars file exists for the workspace. If so then use it for + // the plan. + varsFilename := fmt.Sprintf("%s.tfvars", ws.Name) + tfvars := filepath.Join(mod.FullPath(), varsFilename) + if _, err := os.Stat(tfvars); err == nil { + run.varsFilename = varsFilename + } + return run, nil } @@ -106,6 +118,9 @@ func (r *Run) PlanArgs() []string { if r.Destroy { args = append(args, "-destroy") } + if r.varsFilename != "" { + args = append(args, fmt.Sprintf("-var-file=%s", r.varsFilename)) + } return args } diff --git a/internal/run/run_test.go b/internal/run/run_test.go index 46ea948f..ceeffa96 100644 --- a/internal/run/run_test.go +++ b/internal/run/run_test.go @@ -2,6 +2,8 @@ package run import ( "fmt" + "os" + "path/filepath" "testing" "github.com/leg100/pug/internal" @@ -12,6 +14,27 @@ import ( "github.com/stretchr/testify/require" ) +// TestRun_VarsFile tests creating a run with a workspace tfvars file. +func TestRun_VarsFile(t *testing.T) { + workdir := internal.NewTestWorkdir(t) + testutils.ChTempDir(t, workdir.String()) + + mod := module.New(workdir, "a/b/c") + ws, err := workspace.New(mod, "dev") + require.NoError(t, err) + + // Create a workspace tfvars file for dev + os.MkdirAll(mod.FullPath(), 0o755) + _, err = os.Create(filepath.Join(mod.FullPath(), "dev.tfvars")) + require.NoError(t, err) + + run, err := newRun(mod, ws, CreateOptions{}) + require.NoError(t, err) + + assert.Equal(t, "dev.tfvars", run.varsFilename) + assert.Contains(t, run.PlanArgs(), "-var-file=dev.tfvars") +} + func TestRun_MakePugDirectory(t *testing.T) { workdir := internal.NewTestWorkdir(t) testutils.ChTempDir(t, workdir.String())