/
component.go
351 lines (304 loc) · 12 KB
/
component.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackeval
import (
"context"
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackplan"
"github.com/hashicorp/terraform/internal/stacks/stackruntime/hooks"
"github.com/hashicorp/terraform/internal/stacks/stackstate"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type Component struct {
addr stackaddrs.AbsComponent
main *Main
forEachValue perEvalPhase[promising.Once[withDiagnostics[cty.Value]]]
instances perEvalPhase[promising.Once[withDiagnostics[map[addrs.InstanceKey]*ComponentInstance]]]
}
var _ Plannable = (*Component)(nil)
var _ Referenceable = (*Component)(nil)
func newComponent(main *Main, addr stackaddrs.AbsComponent) *Component {
return &Component{
addr: addr,
main: main,
}
}
func (c *Component) Addr() stackaddrs.AbsComponent {
return c.addr
}
func (c *Component) Config(ctx context.Context) *ComponentConfig {
configAddr := stackaddrs.ConfigForAbs(c.Addr())
stackConfig := c.main.StackConfig(ctx, configAddr.Stack)
if stackConfig == nil {
return nil
}
return stackConfig.Component(ctx, configAddr.Item)
}
func (c *Component) Declaration(ctx context.Context) *stackconfig.Component {
cfg := c.Config(ctx)
if cfg == nil {
return nil
}
return cfg.Declaration(ctx)
}
func (c *Component) Stack(ctx context.Context) *Stack {
// Unchecked because we should've been constructed from the same stack
// object we're about to return, and so this should be valid unless
// the original construction was from an invalid object itself.
return c.main.StackUnchecked(ctx, c.Addr().Stack)
}
// ForEachValue returns the result of evaluating the "for_each" expression
// for this stack call, with the following exceptions:
// - If the stack call doesn't use "for_each" at all, returns [cty.NilVal].
// - If the for_each expression is present but too invalid to evaluate,
// returns [cty.DynamicVal] to represent that the for_each value cannot
// be determined.
//
// A present and valid "for_each" expression produces a result that's
// guaranteed to be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (only the top-level value)
// - Not sensitive (only the top-level value)
func (c *Component) ForEachValue(ctx context.Context, phase EvalPhase) cty.Value {
ret, _ := c.CheckForEachValue(ctx, phase)
return ret
}
// CheckForEachValue evaluates the "for_each" expression if present, validates
// that its value is valid, and then returns that value.
//
// If this call does not use "for_each" then this immediately returns cty.NilVal
// representing the absense of the value.
//
// If the diagnostics does not include errors and the result is not cty.NilVal
// then callers can assume that the result value will be:
// - Either a set of strings, a map of any element type, or an object type
// - Known and not null (except for nested map/object element values)
// - Not sensitive (only the top-level value)
//
// If the diagnostics _does_ include errors then the result might be
// [cty.DynamicVal], which represents that the for_each expression was so invalid
// that we cannot know the for_each value.
func (c *Component) CheckForEachValue(ctx context.Context, phase EvalPhase) (cty.Value, tfdiags.Diagnostics) {
val, diags := doOnceWithDiags(
ctx, c.forEachValue.For(phase), c.main,
func(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
cfg := c.Declaration(ctx)
switch {
case cfg.ForEach != nil:
result, moreDiags := evaluateForEachExpr(ctx, cfg.ForEach, phase, c.Stack(ctx), "component")
diags = diags.Append(moreDiags)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
return result.Value, diags
default:
// This stack config doesn't use for_each at all
return cty.NilVal, diags
}
},
)
if val == cty.NilVal && diags.HasErrors() {
// We use cty.DynamicVal as the placeholder for an invalid for_each,
// to represent "unknown for_each value" as distinct from "no for_each
// expression at all".
val = cty.DynamicVal
}
return val, diags
}
// Instances returns all of the instances of the call known to be declared
// by the configuration.
//
// Calcluating this involves evaluating the call's for_each expression if any,
// and so this call may block on evaluation of other objects in the
// configuration.
//
// If the configuration has an invalid definition of the instances then the
// result will be nil. Callers that need to distinguish between invalid
// definitions and valid definitions of zero instances can rely on the
// result being a non-nil zero-length map in the latter case.
//
// This function doesn't return any diagnostics describing ways in which the
// for_each expression is invalid because we assume that the main plan walk
// will visit the stack call directly and ask it to check itself, and that
// call will be the one responsible for returning any diagnostics.
func (c *Component) Instances(ctx context.Context, phase EvalPhase) map[addrs.InstanceKey]*ComponentInstance {
ret, _ := c.CheckInstances(ctx, phase)
return ret
}
func (c *Component) CheckInstances(ctx context.Context, phase EvalPhase) (map[addrs.InstanceKey]*ComponentInstance, tfdiags.Diagnostics) {
return doOnceWithDiags(
ctx, c.instances.For(phase), c.main,
func(ctx context.Context) (map[addrs.InstanceKey]*ComponentInstance, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
forEachVal, forEachValueDiags := c.CheckForEachValue(ctx, phase)
// We can not create an instance map if the for each vaulu is not valid.
// We don't want to report on forEachValueDiags here because we
// already do this in checkValid.
if forEachValueDiags.HasErrors() {
return nil, diags
}
ret := instancesMap(forEachVal, func(ik addrs.InstanceKey, rd instances.RepetitionData) *ComponentInstance {
return newComponentInstance(c, ik, rd)
}, true)
addrs := make([]stackaddrs.AbsComponentInstance, 0, len(ret))
for _, ci := range ret {
addrs = append(addrs, ci.Addr())
}
h := hooksFromContext(ctx)
hookSingle(ctx, h.ComponentExpanded, &hooks.ComponentInstances{
ComponentAddr: c.Addr(),
InstanceAddrs: addrs,
})
return ret, diags
},
)
}
func (c *Component) ResultValue(ctx context.Context, phase EvalPhase) cty.Value {
decl := c.Declaration(ctx)
insts := c.Instances(ctx, phase)
switch {
case decl.ForEach != nil:
// NOTE: Unlike with StackCall, we must return object types rather than
// map types here since the main Terraform language does not require
// exact type constraints for its output values and so each instance of
// a component can potentially produce a different object type.
if insts == nil {
// If we don't even know what instances we have then we can't
// predict anything about our result.
return cty.DynamicVal
}
// We expect that the instances all have string keys, which will
// become the keys of a map that we're returning.
elems := make(map[string]cty.Value, len(insts))
for instKey, inst := range insts {
k, ok := instKey.(addrs.StringKey)
if !ok {
panic(fmt.Sprintf("stack call with for_each has invalid instance key of type %T", instKey))
}
elems[string(k)] = inst.ResultValue(ctx, phase)
}
return cty.ObjectVal(elems)
default:
if insts == nil {
// If we don't even know what instances we have then we can't
// predict anything about our result.
return cty.DynamicVal
}
if len(insts) != 1 {
// Should not happen: we should have exactly one instance with addrs.NoKey
panic("single-instance stack call does not have exactly one instance")
}
inst, ok := insts[addrs.NoKey]
if !ok {
panic("single-instance stack call does not have an addrs.NoKey instance")
}
return inst.ResultValue(ctx, phase)
}
}
// PlanIsComplete can be called only during the planning phase, and returns
// true only if all instances of this component have "complete" plans.
//
// A component instance plan is "incomplete" if it was either created with
// resource targets set in its planning options or if the modules runtime
// decided it needed to defer at least one action for a future round.
func (c *Component) PlanIsComplete(ctx context.Context) bool {
if !c.main.Planning() {
panic("PlanIsComplete used when not in the planning phase")
}
insts := c.Instances(ctx, PlanPhase)
if insts == nil {
// Suggests that the configuration was not even valid enough to
// decide what the instances are, so we'll return false to be
// conservative and let the error be returned by a different path.
return false
}
if insts[addrs.WildcardKey] != nil {
// If the wildcard key is used the instance originates from an unknown
// for_each value, which means the result is unknown.
return false
}
for _, inst := range insts {
plan := inst.ModuleTreePlan(ctx)
if plan == nil {
// Seems that we weren't even able to create a plan for this
// one, so we'll just assume it was incomplete to be conservative,
// and assume that whatever errors caused this nil result will
// get returned by a different return path.
return false
}
return plan.Complete
}
// If we get here without returning false then we can say that
// all of the instance plans are complete.
return true
}
// ExprReferenceValue implements Referenceable.
func (c *Component) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value {
return c.ResultValue(ctx, phase)
}
func (c *Component) checkValid(ctx context.Context, phase EvalPhase) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
_, moreDiags := c.CheckForEachValue(ctx, phase)
diags = diags.Append(moreDiags)
_, moreDiags = c.CheckInstances(ctx, phase)
diags = diags.Append(moreDiags)
return diags
}
// PlanChanges implements Plannable by performing plan-time validation of
// the component call itself.
//
// The plan walk driver must call [Component.Instances] and also call
// PlanChanges for each instance separately in order to produce a complete
// plan.
func (c *Component) PlanChanges(ctx context.Context) ([]stackplan.PlannedChange, tfdiags.Diagnostics) {
return nil, c.checkValid(ctx, PlanPhase)
}
// References implements Referrer
func (c *Component) References(ctx context.Context) []stackaddrs.AbsReference {
cfg := c.Declaration(ctx)
var ret []stackaddrs.Reference
ret = append(ret, ReferencesInExpr(ctx, cfg.ForEach)...)
ret = append(ret, ReferencesInExpr(ctx, cfg.Inputs)...)
for _, expr := range cfg.ProviderConfigs {
ret = append(ret, ReferencesInExpr(ctx, expr)...)
}
return makeReferencesAbsolute(ret, c.Addr().Stack)
}
// RequiredComponents implements Applyable
func (c *Component) RequiredComponents(ctx context.Context) collections.Set[stackaddrs.AbsComponent] {
return c.main.requiredComponentsForReferrer(ctx, c, PlanPhase)
}
// CheckApply implements ApplyChecker.
func (c *Component) CheckApply(ctx context.Context) ([]stackstate.AppliedChange, tfdiags.Diagnostics) {
return nil, c.checkValid(ctx, ApplyPhase)
}
// ApplySuccessful blocks until all instances of this component have
// completed their apply step and returns whether the apply was successful,
// or panics if called not during the apply phase.
func (c *Component) ApplySuccessful(ctx context.Context) bool {
if !c.main.Applying() {
panic("ApplySuccessful when not applying")
}
// Apply is successful if all of our instances fully completed their
// apply phases.
for _, inst := range c.Instances(ctx, ApplyPhase) {
result := inst.ApplyResult(ctx)
if result == nil || !result.Complete {
return false
}
}
// If we get here then either we had no instances at all or they all
// applied completely, and so our aggregate result is success.
return true
}
func (c *Component) tracingName() string {
return c.Addr().String()
}