Skip to content

Commit bb73fb1

Browse files
authored
stacks: don't validate input variables during the apply phase (#35561)
1 parent 1d770d6 commit bb73fb1

File tree

2 files changed

+23
-210
lines changed

2 files changed

+23
-210
lines changed

internal/stacks/stackruntime/apply_test.go

Lines changed: 2 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1707,132 +1707,6 @@ func TestApplyWithStateManipulation(t *testing.T) {
17071707
}
17081708
}
17091709

1710-
func TestApplyWithChangedComponentValues(t *testing.T) {
1711-
ctx := context.Background()
1712-
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid"))
1713-
1714-
fakePlanTimestamp, err := time.Parse(time.RFC3339, "1991-08-25T20:57:08Z")
1715-
if err != nil {
1716-
t.Fatal(err)
1717-
}
1718-
1719-
changesCh := make(chan stackplan.PlannedChange)
1720-
diagsCh := make(chan tfdiags.Diagnostic)
1721-
lock := depsfile.NewLocks()
1722-
lock.SetProvider(
1723-
addrs.NewDefaultProvider("testing"),
1724-
providerreqs.MustParseVersion("0.0.0"),
1725-
providerreqs.MustParseVersionConstraints("=0.0.0"),
1726-
providerreqs.PreferredHashes([]providerreqs.Hash{}),
1727-
)
1728-
req := PlanRequest{
1729-
Config: cfg,
1730-
ProviderFactories: map[addrs.Provider]providers.Factory{
1731-
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
1732-
return stacks_testing_provider.NewProvider(), nil
1733-
},
1734-
},
1735-
DependencyLocks: *lock,
1736-
1737-
ForcePlanTimestamp: &fakePlanTimestamp,
1738-
1739-
InputValues: map[stackaddrs.InputVariable]ExternalInputValue{
1740-
stackaddrs.InputVariable{Name: "input"}: {
1741-
Value: cty.StringVal("hello"),
1742-
},
1743-
},
1744-
}
1745-
resp := PlanResponse{
1746-
PlannedChanges: changesCh,
1747-
Diagnostics: diagsCh,
1748-
}
1749-
1750-
go Plan(ctx, &req, &resp)
1751-
planChanges, diags := collectPlanOutput(changesCh, diagsCh)
1752-
if len(diags) > 0 {
1753-
t.Fatalf("expected no diagnostics, got %s", diags.ErrWithWarnings())
1754-
}
1755-
1756-
planLoader := stackplan.NewLoader()
1757-
for _, change := range planChanges {
1758-
proto, err := change.PlannedChangeProto()
1759-
if err != nil {
1760-
t.Fatal(err)
1761-
}
1762-
1763-
for _, rawMsg := range proto.Raw {
1764-
err = planLoader.AddRaw(rawMsg)
1765-
if err != nil {
1766-
t.Fatal(err)
1767-
}
1768-
}
1769-
}
1770-
plan, err := planLoader.Plan()
1771-
if err != nil {
1772-
t.Fatal(err)
1773-
}
1774-
1775-
// Now deliberately edit the plan so that it'll produce different outputs.
1776-
plan.RootInputValues[stackaddrs.InputVariable{Name: "input"}] = cty.StringVal("world")
1777-
1778-
applyReq := ApplyRequest{
1779-
Config: cfg,
1780-
Plan: plan,
1781-
ProviderFactories: map[addrs.Provider]providers.Factory{
1782-
addrs.NewDefaultProvider("testing"): func() (providers.Interface, error) {
1783-
return stacks_testing_provider.NewProvider(), nil
1784-
},
1785-
},
1786-
DependencyLocks: *lock,
1787-
}
1788-
1789-
applyChangesCh := make(chan stackstate.AppliedChange)
1790-
diagsCh = make(chan tfdiags.Diagnostic)
1791-
1792-
applyResp := ApplyResponse{
1793-
AppliedChanges: applyChangesCh,
1794-
Diagnostics: diagsCh,
1795-
}
1796-
1797-
go Apply(ctx, &applyReq, &applyResp)
1798-
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
1799-
if len(applyDiags) != 1 {
1800-
t.Fatalf("expected exactly one diagnostic, got %s", applyDiags.ErrWithWarnings())
1801-
}
1802-
1803-
gotSeverity, wantSeverity := applyDiags[0].Severity(), tfdiags.Error
1804-
gotSummary, wantSummary := applyDiags[0].Description().Summary, "Planned input variable value changed"
1805-
gotDetail, wantDetail := applyDiags[0].Description().Detail, "The planned value for input variable \"input\" has changed between the planning and apply phases for component.self. This is a bug in Terraform - please report it."
1806-
1807-
if gotSeverity != wantSeverity {
1808-
t.Errorf("expected severity %q, got %q", wantSeverity, gotSeverity)
1809-
}
1810-
if gotSummary != wantSummary {
1811-
t.Errorf("expected summary %q, got %q", wantSummary, gotSummary)
1812-
}
1813-
if gotDetail != wantDetail {
1814-
t.Errorf("expected detail %q, got %q", wantDetail, gotDetail)
1815-
}
1816-
1817-
wantChanges := []stackstate.AppliedChange{
1818-
&stackstate.AppliedChangeComponentInstance{
1819-
ComponentAddr: mustAbsComponent("component.self"),
1820-
ComponentInstanceAddr: mustAbsComponentInstance("component.self"),
1821-
OutputValues: make(map[addrs.OutputValue]cty.Value),
1822-
},
1823-
// no resources should have been created because the input variable was
1824-
// invalid.
1825-
}
1826-
1827-
sort.SliceStable(applyChanges, func(i, j int) bool {
1828-
return appliedChangeSortKey(applyChanges[i]) < appliedChangeSortKey(applyChanges[j])
1829-
})
1830-
1831-
if diff := cmp.Diff(wantChanges, applyChanges, ctydebug.CmpOptions, cmpCollectionsSet); diff != "" {
1832-
t.Errorf("wrong changes\n%s", diff)
1833-
}
1834-
}
1835-
18361710
func TestApplyWithChangedInputValues(t *testing.T) {
18371711
ctx := context.Background()
18381712
cfg := loadMainBundleConfigForTest(t, filepath.Join("with-single-input", "valid"))
@@ -1927,7 +1801,7 @@ func TestApplyWithChangedInputValues(t *testing.T) {
19271801

19281802
go Apply(ctx, &applyReq, &applyResp)
19291803
applyChanges, applyDiags := collectApplyOutput(applyChangesCh, diagsCh)
1930-
if len(applyDiags) != 2 {
1804+
if len(applyDiags) != 1 {
19311805
t.Fatalf("expected exactly two diagnostics, got %s", applyDiags.ErrWithWarnings())
19321806
}
19331807

@@ -1937,10 +1811,7 @@ func TestApplyWithChangedInputValues(t *testing.T) {
19371811
tfdiags.Error,
19381812
"Inconsistent value for input variable during apply",
19391813
"The value for non-ephemeral input variable \"input\" was set to a different value during apply than was set during plan. Only ephemeral input variables can change between the plan and apply phases."),
1940-
expectDiagnostic(
1941-
tfdiags.Error,
1942-
"Planned input variable value changed",
1943-
"The planned value for input variable \"input\" has changed between the planning and apply phases for component.self. This is a bug in Terraform - please report it."))
1814+
)
19441815

19451816
wantChanges := []stackstate.AppliedChange{
19461817
&stackstate.AppliedChangeComponentInstance{

internal/stacks/stackruntime/internal/stackeval/component_instance.go

Lines changed: 21 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import (
1717
"github.com/hashicorp/terraform/internal/instances"
1818
"github.com/hashicorp/terraform/internal/lang/marks"
1919
"github.com/hashicorp/terraform/internal/plans"
20-
"github.com/hashicorp/terraform/internal/plans/objchange"
2120
"github.com/hashicorp/terraform/internal/promising"
2221
"github.com/hashicorp/terraform/internal/providers"
2322
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
@@ -91,7 +90,20 @@ func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase
9190

9291
// We actually checked the errors statically already, so we only care about
9392
// the value here.
94-
return EvalComponentInputVariables(ctx, varDecls, wantTy, defs, decl, phase, c)
93+
val, diags := EvalComponentInputVariables(ctx, varDecls, wantTy, defs, decl, phase, c)
94+
if phase == ApplyPhase {
95+
if !val.IsWhollyKnown() {
96+
// We can't apply a configuration that has unknown values in it.
97+
// This means an error has occured somewhere else, while gathering
98+
// the input variables. We return a nil value here, whatever caused
99+
// the error should have raised an error diagnostic separately.
100+
return cty.NilVal, diags
101+
}
102+
103+
// Note, that unknown values during the planning phase are totally fine.
104+
}
105+
106+
return val, diags
95107
}
96108

97109
// inputValuesForModulesRuntime adapts the result of
@@ -104,17 +116,12 @@ func (c *ComponentInstance) CheckInputVariableValues(ctx context.Context, phase
104116
//
105117
// During the planning phase, the expectedValues should be nil, as they will
106118
// only be checked during the apply phase.
107-
func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, previousValues map[string]plans.DynamicValue, phase EvalPhase) (terraform.InputValues, tfdiags.Diagnostics) {
108-
var diags tfdiags.Diagnostics
109-
119+
func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, phase EvalPhase) terraform.InputValues {
110120
valsObj := c.InputVariableValues(ctx, phase)
111121
if valsObj == cty.NilVal {
112-
return nil, diags
122+
return nil
113123
}
114124

115-
// module is the configuration for the root module of this component.
116-
module := c.call.Config(ctx).ModuleTree(ctx).Module
117-
118125
// valsObj might be an unknown value during the planning phase, in which
119126
// case we'll return an InputValues with all of the expected variables
120127
// defined as unknown values of their expected type constraints. To
@@ -124,7 +131,7 @@ func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, pr
124131
if wantTy == cty.NilType {
125132
// The configuration is too invalid for us to know what type we're
126133
// expecting, so we'll just bail.
127-
return nil, diags
134+
return nil
128135
}
129136
wantAttrs := wantTy.AttributeTypes()
130137
ret := make(terraform.InputValues, len(wantAttrs))
@@ -139,71 +146,8 @@ func (c *ComponentInstance) inputValuesForModulesRuntime(ctx context.Context, pr
139146
Value: v,
140147
SourceType: terraform.ValueFromCaller,
141148
}
142-
143-
// While we're here, we'll just add a diagnostic if the value has
144-
// somehow changed between the planning and apply phases. All of these
145-
// diagnostics acknowledge that the root cause here is a bug in
146-
// Terraform.
147-
if phase == ApplyPhase {
148-
config := module.Variables[name]
149-
if config.Ephemeral {
150-
// Ephemeral variables are allowed to change between the plan
151-
// and apply stages, so we won't bother checking them.
152-
continue
153-
}
154-
155-
raw, ok := previousValues[name]
156-
if !ok {
157-
// This shouldn't happen because we should have a value for
158-
// every input variable that we have a value for in the plan.
159-
diags = diags.Append(&hcl.Diagnostic{
160-
Severity: hcl.DiagError,
161-
Summary: "Missing input variable value",
162-
Detail: fmt.Sprintf(
163-
"The input variable %q is required but was not set in the plan for %s. This is a bug in Terraform - please report it.",
164-
name, c.Addr(),
165-
),
166-
Subject: c.call.Declaration(ctx).DeclRange.ToHCL().Ptr(),
167-
})
168-
continue
169-
}
170-
171-
plannedValue, err := raw.Decode(cty.DynamicPseudoType)
172-
if err != nil {
173-
// Then something has gone wrong when decoding the value.
174-
diags = diags.Append(&hcl.Diagnostic{
175-
Severity: hcl.DiagError,
176-
Summary: "Invalid planned input variable value",
177-
Detail: fmt.Sprintf("Failed to decode the planned value for input variable %q: %s. This is a bug in Terraform - please report it.", name, err),
178-
Subject: c.call.Declaration(ctx).DeclRange.ToHCL().Ptr(),
179-
})
180-
continue
181-
}
182-
183-
// We're unmarking the value here so that we can compare it to the
184-
// planned value. We're only checking for equality from here on out,
185-
// so we don't need to worry about the marks.
186-
applyValue, _ := v.UnmarkDeep()
187-
188-
if errs := objchange.AssertValueCompatible(plannedValue, applyValue); len(errs) > 0 {
189-
// Then the value has changed between the planning and apply
190-
// phases. This is a bug in Terraform. We don't want to expose
191-
// the actual values here as they could contain sensitive
192-
// information. This should be a rare error message, so we
193-
// don't need to be too verbose.
194-
diags = diags.Append(&hcl.Diagnostic{
195-
Severity: hcl.DiagError,
196-
Summary: "Planned input variable value changed",
197-
Detail: fmt.Sprintf(
198-
"The planned value for input variable %q has changed between the planning and apply phases for %s. This is a bug in Terraform - please report it.",
199-
name, c.Addr(),
200-
),
201-
Subject: c.call.Declaration(ctx).DeclRange.ToHCL().Ptr(),
202-
})
203-
}
204-
}
205149
}
206-
return ret, diags
150+
return ret
207151
}
208152

209153
// Providers evaluates the "providers" argument from the component
@@ -615,8 +559,7 @@ func (c *ComponentInstance) CheckModuleTreePlan(ctx context.Context) (*plans.Pla
615559
}
616560

617561
stackPlanOpts := c.main.PlanningOpts()
618-
inputValues, inputValueDiags := c.inputValuesForModulesRuntime(ctx, nil, PlanPhase)
619-
diags = diags.Append(inputValueDiags)
562+
inputValues := c.inputValuesForModulesRuntime(ctx, PlanPhase)
620563
if inputValues == nil || diags.HasErrors() {
621564
return nil, diags
622565
}
@@ -868,9 +811,8 @@ func (c *ComponentInstance) ApplyModuleTreePlan(ctx context.Context, plan *plans
868811
// shallow-copy it. This is NOT a deep copy, so don't modify anything
869812
// that's reachable through any pointers without copying those first too.
870813
modifiedPlan := *plan
871-
inputValues, inputValueDiags := c.inputValuesForModulesRuntime(ctx, plan.VariableValues, ApplyPhase)
872-
diags = diags.Append(inputValueDiags)
873-
if inputValues == nil || inputValueDiags.HasErrors() {
814+
inputValues := c.inputValuesForModulesRuntime(ctx, ApplyPhase)
815+
if inputValues == nil {
874816
// inputValuesForModulesRuntime uses nil (as opposed to a
875817
// non-nil zerolen map) to represent that the definition of
876818
// the input variables was so invalid that we cannot do

0 commit comments

Comments
 (0)