Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pass ephemeral variables to terraform apply #35903

Merged
merged 14 commits into from
Nov 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
ephemeral: set ephemeral variables for combined plans as well
they are by definition the same as in the plan, but there is no reason to skip the validation step
  • Loading branch information
DanielMSchmidt committed Nov 6, 2024
commit 2e026dec0f88c5013b01b2366e992f71e18379b3
4 changes: 1 addition & 3 deletions internal/backend/local/backend_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,8 @@ func (b *Local) opApply(
stateHook.PersistInterval = time.Duration(op.StatePersistInterval) * time.Second

var plan *plans.Plan
combinedPlanApply := false
// If we weren't given a plan, then we refresh/plan
if op.PlanFile == nil {
combinedPlanApply = true
// Perform the plan
log.Printf("[INFO] backend/local: apply calling Plan")
plan, moreDiags = lr.Core.Plan(lr.Config, lr.InputState, lr.PlanOpts)
Expand Down Expand Up @@ -235,7 +233,7 @@ func (b *Local) opApply(
stateHook.StateMgr = opState

var applyOpts *terraform.ApplyOpts
if len(op.Variables) != 0 && !combinedPlanApply {
if len(op.Variables) != 0 {
applyTimeValues := make(terraform.InputValues, plan.ApplyTimeVariables.Len())
for varName, rawV := range op.Variables {
// We're "parsing" only to get the resulting value's SourceType,
Expand Down
147 changes: 118 additions & 29 deletions internal/command/apply_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1034,11 +1034,12 @@ func TestApply_planVars(t *testing.T) {
// Test that an apply supplying all apply-time variables succeeds, and then test
// that supplying a declared ephemeral input variable that is *not* in the list
// of apply-time variables fails.
//
// In the fixture used for this test foo is a required ephemeral variable, whereas bar is
// an optional one.
func TestApply_planVarsEphemeral_applyTime(t *testing.T) {
for name, tc := range map[string]func(*testing.T, *ApplyCommand, string, string, func(*testing.T) *terminal.TestOutput){
// Test first that an apply supplying only the apply-time variable "foo"
// succeeds.
"only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
"with planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
"-var", "foo=bar",
Expand All @@ -1047,12 +1048,11 @@ func TestApply_planVarsEphemeral_applyTime(t *testing.T) {
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.Stderr())
t.Fatal("should've succeeded: ", output.All())
}
},

// Now test that supplying "bar", which is not an apply-time variable, fails.
"passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
"with planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
"-var", "foo=bar",
Expand All @@ -1062,25 +1062,23 @@ func TestApply_planVarsEphemeral_applyTime(t *testing.T) {
code := c.Run(args)
output := done(t)
if code == 0 {
t.Fatal("should've failed: ", output.Stdout())
t.Fatal("should've failed: ", output.All())
}
},

// Test that the apply also fails if we do *not* supply a value for
// the apply-time variable foo.
"missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
"with planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
planPath,
}
code := c.Run(args)
output := done(t)
if code == 0 {
t.Fatal("should've failed: ", output.Stdout())
t.Fatal("should've failed: ", output.All())
}
},

"passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
"with planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
const planVarFile = `
foo = "bar"
`
Expand All @@ -1100,12 +1098,13 @@ foo = "bar"
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.Stderr())
t.Fatal("should've succeeded: ", output.All())
}
},

"passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
"with planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
t.Setenv("TF_VAR_foo", "bar")
defer t.Setenv("TF_VAR_foo", "")

args := []string{
"-state", statePath,
Expand All @@ -1114,24 +1113,114 @@ foo = "bar"
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.Stderr())
t.Fatal("should've succeeded: ", output.All())
}
},

"with planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
close := testInteractiveInput(t, []string{"bar"})
defer close()

args := []string{
"-state", statePath,
planPath,
}
code := c.Run(args)
output := done(t)
if code == 0 {
// We don't support interactive inputs for apply-time variables
t.Fatal("should have failed: ", output.All())
}
},

"without planfile only passing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
"-var", "foo=bar",
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.All())
}
},

"without planfile passing non-ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
"-var", "foo=bar",
"-var", "bar=bar",
}
code := c.Run(args)
output := done(t)

// For a combined plan & apply operation it's okay (and expected) to also be able to pass non-ephemeral variables
if code != 0 {
t.Fatal("should've succeeded: ", output.All())
}
},

"without planfile missing ephemeral variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
args := []string{
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code == 0 {
t.Fatal("should've failed: ", output.All())
}
},

"without planfile passing ephemeral variable through vars file": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
const planVarFile = `
foo = "bar"
`

// Write a tfvars file with the variable
tfVarsPath := testVarsFile(t)
err := os.WriteFile(tfVarsPath, []byte(planVarFile), 0600)
if err != nil {
t.Fatalf("Could not write vars file %e", err)
}

args := []string{
"-state", statePath,
"-var-file", tfVarsPath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.All())
}
},

// "passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
// close := testInteractiveInput(t, []string{"bar"})
// defer close()

// args := []string{
// "-state", statePath,
// planPath,
// }
// code := c.Run(args)
// output := done(t)
// if code != 0 {
// t.Fatal("should've succeeded: ", output.Stderr())
// }
// },
"without planfile passing ephemeral variable through environment variable": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
t.Setenv("TF_VAR_foo", "bar")
defer t.Setenv("TF_VAR_foo", "")

args := []string{
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code != 0 {
t.Fatal("should've succeeded: ", output.All())
}
},

"without planfile passing ephemeral variable through interactive prompts": func(t *testing.T, c *ApplyCommand, statePath, planPath string, done func(*testing.T) *terminal.TestOutput) {
close := testInteractiveInput(t, []string{"bar"})
defer close()

args := []string{
"-state", statePath,
}
code := c.Run(args)
output := done(t)
if code == 0 {
t.Fatal("should've failed: ", output.All())
}
},
} {
t.Run(name, func(t *testing.T) {
td := t.TempDir()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
variable "foo" {
type = string
default = null
ephemeral = true
}

Expand Down
Loading