Skip to content

Commit 9932161

Browse files
authored
Merge pull request #36025 from hashicorp/jbardin/undeclared-vars
undeclared variables must be allowed during apply
2 parents 840bc0c + 16fe12e commit 9932161

File tree

2 files changed

+153
-77
lines changed

2 files changed

+153
-77
lines changed

internal/backend/local/backend_apply.go

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,22 @@ func (b *Local) opApply(
252252
}
253253

254254
if len(op.Variables) != 0 {
255+
// Undeclared variables cause warnings during plan, but will show up
256+
// again here during apply. Their handling is tricky though, because it
257+
// depends on how they were declared, and is subject to compatibility
258+
// constraints. Collect any suspect values as we go, and then use the
259+
// same parsing logic from the plan to generate the diagnostics.
260+
undeclaredVariables := map[string]backendrun.UnparsedVariableValue{}
261+
255262
for varName, rawV := range op.Variables {
263+
decl, ok := lr.Config.Module.Variables[varName]
264+
if !ok {
265+
// We'll try to parse this and handle diagnostics for missing
266+
// variables with ParseUndeclaredVariableValues after.
267+
undeclaredVariables[varName] = rawV
268+
continue
269+
}
270+
256271
// We're "parsing" only to get the resulting value's SourceType,
257272
// so we'll use configs.VariableParseLiteral just because it's
258273
// the most liberal interpretation and so least likely to
@@ -269,25 +284,6 @@ func (b *Local) opApply(
269284
rng = v.SourceRange.ToHCL().Ptr()
270285
}
271286

272-
decl, ok := lr.Config.Module.Variables[varName]
273-
274-
// If the variable isn't declared in config at all, take
275-
// this opportunity to give the user a helpful error,
276-
// rather than waiting for a less helpful one later.
277-
// We are ok with over-supplying variables through environment variables
278-
// since it would be a breaking change to disallow it.
279-
if v.SourceType == terraform.ValueFromCLIArg || v.SourceType == terraform.ValueFromNamedFile {
280-
if !ok {
281-
diags = diags.Append(&hcl.Diagnostic{
282-
Severity: hcl.DiagError,
283-
Summary: "Value for undeclared variable",
284-
Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options because it is not declared in configuration.", varName),
285-
Subject: rng,
286-
})
287-
continue
288-
}
289-
}
290-
291287
// If the var is declared as ephemeral in config, go ahead and handle it
292288
if ok && decl.Ephemeral {
293289
// Determine whether this is an apply-time variable, i.e. an
@@ -345,12 +341,8 @@ func (b *Local) opApply(
345341
// If a non-ephemeral variable is set differently between plan and apply, we should emit a diagnostic.
346342
plannedVariableValue, ok := plan.VariableValues[varName]
347343
if !ok {
348-
diags = diags.Append(&hcl.Diagnostic{
349-
Severity: hcl.DiagError,
350-
Summary: "Can't set variable when applying a saved plan",
351-
Detail: fmt.Sprintf("The variable %s cannot be set using the -var and -var-file options when applying a saved plan file, because it is neither ephemeral nor has it been declared during the plan operation. To declare an ephemeral variable which is not saved in the plan file, use ephemeral = true.", varName),
352-
Subject: rng,
353-
})
344+
// We'll catch this with ParseUndeclaredVariableValues after
345+
undeclaredVariables[varName] = rawV
354346
continue
355347
}
356348

@@ -373,7 +365,15 @@ func (b *Local) opApply(
373365
}
374366
}
375367
}
368+
369+
}
370+
_, undeclaredDiags := backendrun.ParseUndeclaredVariableValues(undeclaredVariables, map[string]*configs.Variable{})
371+
// always add hard errors here, and add warnings if we're not in a
372+
// combined op which just emitted those same warnings already.
373+
if undeclaredDiags.HasErrors() || !combinedPlanApply {
374+
diags = diags.Append(undeclaredDiags)
376375
}
376+
377377
if diags.HasErrors() {
378378
op.ReportResult(runningOp, diags)
379379
return

internal/command/apply_test.go

Lines changed: 128 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -860,7 +860,7 @@ func TestApply_plan_remoteState(t *testing.T) {
860860
func TestApply_planWithVarFile(t *testing.T) {
861861
varFileDir := testTempDir(t)
862862
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
863-
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
863+
if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
864864
t.Fatalf("err: %s", err)
865865
}
866866

@@ -878,6 +878,19 @@ func TestApply_planWithVarFile(t *testing.T) {
878878
defer os.Chdir(cwd)
879879

880880
p := applyFixtureProvider()
881+
p.GetProviderSchemaResponse = &providers.GetProviderSchemaResponse{
882+
ResourceTypes: map[string]providers.Schema{
883+
"test_instance": {
884+
Block: &configschema.Block{
885+
Attributes: map[string]*configschema.Attribute{
886+
"id": {Type: cty.String, Computed: true},
887+
"value": {Type: cty.String, Optional: true},
888+
},
889+
},
890+
},
891+
},
892+
}
893+
881894
view, done := testView(t)
882895
c := &ApplyCommand{
883896
Meta: Meta{
@@ -906,59 +919,14 @@ func TestApply_planWithVarFile(t *testing.T) {
906919
}
907920
}
908921

909-
func TestApply_planWithVarFilePreviouslyUnset(t *testing.T) {
910-
varFileDir := testTempDir(t)
911-
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
912-
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
913-
t.Fatalf("err: %s", err)
914-
}
915-
916-
// The value of foo is not set
917-
planPath := applyFixturePlanFile(t)
918-
statePath := testTempFile(t)
919-
920-
cwd, err := os.Getwd()
921-
if err != nil {
922-
t.Fatalf("err: %s", err)
923-
}
924-
if err := os.Chdir(varFileDir); err != nil {
925-
t.Fatalf("err: %s", err)
926-
}
927-
defer os.Chdir(cwd)
928-
929-
p := applyFixtureProvider()
930-
view, done := testView(t)
931-
c := &ApplyCommand{
932-
Meta: Meta{
933-
testingOverrides: metaOverridesForProvider(p),
934-
View: view,
935-
},
936-
}
937-
938-
args := []string{
939-
"-state-out", statePath,
940-
planPath,
941-
}
942-
code := c.Run(args)
943-
output := done(t)
944-
if code == 0 {
945-
t.Fatalf("expected to fail, but succeeded. \n\n%s", output.All())
946-
}
947-
948-
expectedTitle := "Can't set variable when applying a saved plan"
949-
if !strings.Contains(output.Stderr(), expectedTitle) {
950-
t.Fatalf("Expected stderr to contain %q, got %q", expectedTitle, output.Stderr())
951-
}
952-
}
953-
954922
func TestApply_planWithVarFileChangingVariableValue(t *testing.T) {
955923
varFileDir := testTempDir(t)
956924
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
957-
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
925+
if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
958926
t.Fatalf("err: %s", err)
959927
}
960928

961-
// The value of foo is differnet from the var file
929+
// The value of foo is different from the var file
962930
planPath := applyFixturePlanFileWithVariableValue(t, "lorem ipsum")
963931
statePath := testTempFile(t)
964932

@@ -1289,6 +1257,114 @@ foo = "bar"
12891257
}
12901258
}
12911259

1260+
// Variables can be passed to apply now for ephemeral usage, but we need to
1261+
// ensure that the legacy handling of undeclared variables remains intact
1262+
func TestApply_changedVars_applyTime(t *testing.T) {
1263+
t.Run("undeclared-config-var", func(t *testing.T) {
1264+
// an undeclared config variable is a warning, just like during plan
1265+
varFileDir := testTempDir(t)
1266+
varFilePath := filepath.Join(varFileDir, "terraform.tfvars")
1267+
if err := os.WriteFile(varFilePath, []byte(`undeclared = true`), 0644); err != nil {
1268+
t.Fatalf("err: %s", err)
1269+
}
1270+
1271+
// The value of foo is not set
1272+
planPath := applyFixturePlanFile(t)
1273+
statePath := testTempFile(t)
1274+
1275+
cwd, err := os.Getwd()
1276+
if err != nil {
1277+
t.Fatalf("err: %s", err)
1278+
}
1279+
if err := os.Chdir(varFileDir); err != nil {
1280+
t.Fatalf("err: %s", err)
1281+
}
1282+
defer os.Chdir(cwd)
1283+
1284+
p := applyFixtureProvider()
1285+
view, done := testView(t)
1286+
c := &ApplyCommand{
1287+
Meta: Meta{
1288+
testingOverrides: metaOverridesForProvider(p),
1289+
View: view,
1290+
},
1291+
}
1292+
1293+
args := []string{
1294+
"-state-out", statePath,
1295+
planPath,
1296+
}
1297+
code := c.Run(args)
1298+
output := done(t)
1299+
if code != 0 {
1300+
t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All())
1301+
}
1302+
1303+
if !strings.Contains(output.All(), `Value for undeclared variable`) {
1304+
t.Fatalf("missing undeclared warning:\n%s", output.All())
1305+
}
1306+
})
1307+
1308+
t.Run("undeclared-cli-var", func(t *testing.T) {
1309+
// an undeclared cli variable is an error, just like during plan
1310+
planPath := applyFixturePlanFile(t)
1311+
statePath := testTempFile(t)
1312+
1313+
p := applyFixtureProvider()
1314+
view, done := testView(t)
1315+
c := &ApplyCommand{
1316+
Meta: Meta{
1317+
testingOverrides: metaOverridesForProvider(p),
1318+
View: view,
1319+
},
1320+
}
1321+
1322+
args := []string{
1323+
"-var", "undeclared=true",
1324+
"-state-out", statePath,
1325+
planPath,
1326+
}
1327+
code := c.Run(args)
1328+
output := done(t)
1329+
if code != 1 {
1330+
t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All())
1331+
}
1332+
1333+
if !strings.Contains(output.Stderr(), `Value for undeclared variable`) {
1334+
t.Fatalf("missing undeclared warning:\n%s", output.All())
1335+
}
1336+
})
1337+
1338+
t.Run("changed-cli-var", func(t *testing.T) {
1339+
planPath := applyFixturePlanFileWithVariableValue(t, "orig")
1340+
statePath := testTempFile(t)
1341+
1342+
p := applyFixtureProvider()
1343+
view, done := testView(t)
1344+
c := &ApplyCommand{
1345+
Meta: Meta{
1346+
testingOverrides: metaOverridesForProvider(p),
1347+
View: view,
1348+
},
1349+
}
1350+
1351+
args := []string{
1352+
"-var", "foo=new",
1353+
"-state-out", statePath,
1354+
planPath,
1355+
}
1356+
code := c.Run(args)
1357+
output := done(t)
1358+
if code != 1 {
1359+
t.Fatalf("unexpected exit code %d:\n\n%s", code, output.All())
1360+
}
1361+
1362+
if !strings.Contains(output.Stderr(), `Can't change variable when applying a saved plan`) {
1363+
t.Fatalf("missing undeclared warning:\n%s", output.All())
1364+
}
1365+
})
1366+
}
1367+
12921368
// we should be able to apply a plan file with no other file dependencies
12931369
func TestApply_planNoModuleFiles(t *testing.T) {
12941370
// temporary data directory which we can remove between commands
@@ -1800,7 +1876,7 @@ func TestApply_varFileDefault(t *testing.T) {
18001876
defer testChdir(t, td)()
18011877

18021878
varFilePath := filepath.Join(td, "terraform.tfvars")
1803-
if err := ioutil.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
1879+
if err := os.WriteFile(varFilePath, []byte(applyVarFile), 0644); err != nil {
18041880
t.Fatalf("err: %s", err)
18051881
}
18061882

@@ -2595,10 +2671,10 @@ func applyFixturePlanFileMatchState(t *testing.T, stateMeta statemgr.SnapshotMet
25952671
// a single change to create the test_instance.foo and a variable value that is included in the
25962672
// "apply" test fixture, returning the location of that plan file.
25972673
func applyFixturePlanFileWithVariableValue(t *testing.T, value string) string {
2598-
_, snap := testModuleWithSnapshot(t, "apply")
2674+
_, snap := testModuleWithSnapshot(t, "apply-vars")
25992675
plannedVal := cty.ObjectVal(map[string]cty.Value{
2600-
"id": cty.UnknownVal(cty.String),
2601-
"ami": cty.StringVal("bar"),
2676+
"id": cty.UnknownVal(cty.String),
2677+
"value": cty.StringVal("bar"),
26022678
})
26032679
priorValRaw, err := plans.NewDynamicValue(cty.NullVal(plannedVal.Type()), plannedVal.Type())
26042680
if err != nil {

0 commit comments

Comments
 (0)