/
move_validate.go
360 lines (331 loc) · 12.3 KB
/
move_validate.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
352
353
354
355
356
357
358
359
360
package refactoring
import (
"fmt"
"sort"
"strings"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/dag"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// ValidateMoves tests whether all of the given move statements comply with
// both the single-statement validation rules and the "big picture" rules
// that constrain statements in relation to one another.
//
// The validation rules are primarily in terms of the configuration, but
// ValidateMoves also takes the expander that resulted from creating a plan
// so that it can see which instances are defined for each module and resource,
// to precisely validate move statements involving specific-instance addresses.
//
// Because validation depends on the planning result but move execution must
// happen _before_ planning, we have the unusual situation where sibling
// function ApplyMoves must run before ValidateMoves and must therefore
// tolerate and ignore any invalid statements. The plan walk will then
// construct in incorrect plan (because it'll be starting from the wrong
// prior state) but ValidateMoves will block actually showing that invalid
// plan to the user.
func ValidateMoves(stmts []MoveStatement, rootCfg *configs.Config, declaredInsts instances.Set) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
if len(stmts) == 0 {
return diags
}
g := buildMoveStatementGraph(stmts)
// We need to track the absolute versions of our endpoint addresses in
// order to detect when there are ambiguous moves.
type AbsMoveEndpoint struct {
Other addrs.AbsMoveable
StmtRange tfdiags.SourceRange
}
stmtFrom := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
stmtTo := addrs.MakeMap[addrs.AbsMoveable, AbsMoveEndpoint]()
for _, stmt := range stmts {
// Earlier code that constructs MoveStatement values should ensure that
// both stmt.From and stmt.To always belong to the same statement.
fromMod, _ := stmt.From.ModuleCallTraversals()
for _, fromModInst := range declaredInsts.InstancesForModule(fromMod) {
absFrom := stmt.From.InModuleInstance(fromModInst)
absTo := stmt.To.InModuleInstance(fromModInst)
if addrs.Equivalent(absFrom, absTo) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Redundant move statement",
Detail: fmt.Sprintf(
"This statement declares a move from %s to the same address, which is the same as not declaring this move at all.",
absFrom,
),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
continue
}
var noun string
var shortNoun string
switch absFrom.(type) {
case addrs.ModuleInstance:
noun = "module instance"
shortNoun = "instance"
case addrs.AbsModuleCall:
noun = "module call"
shortNoun = "call"
case addrs.AbsResourceInstance:
noun = "resource instance"
shortNoun = "instance"
case addrs.AbsResource:
noun = "resource"
shortNoun = "resource"
default:
// The above cases should cover all of the AbsMoveable types
panic("unsupported AbsMoveable address type")
}
// It's invalid to have a move statement whose "from" address
// refers to something that is still declared in the configuration.
if moveableObjectExists(absFrom, declaredInsts) {
conflictRange, hasRange := movableObjectDeclRange(absFrom, rootCfg)
declaredAt := ""
if hasRange {
// NOTE: It'd be pretty weird to _not_ have a range, since
// we're only in this codepath because the plan phase
// thought this object existed in the configuration.
declaredAt = fmt.Sprintf(" at %s", conflictRange.StartString())
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Moved object still exists",
Detail: fmt.Sprintf(
"This statement declares a move from %s, but that %s is still declared%s.\n\nChange your configuration so that this %s will be declared as %s instead.",
absFrom, noun, declaredAt, shortNoun, absTo,
),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
}
// There can only be one destination for each source address.
if existing, exists := stmtFrom.GetOk(absFrom); exists {
if !addrs.Equivalent(existing.Other, absTo) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ambiguous move statements",
Detail: fmt.Sprintf(
"A statement at %s declared that %s moved to %s, but this statement instead declares that it moved to %s.\n\nEach %s can move to only one destination %s.",
existing.StmtRange.StartString(), absFrom, existing.Other, absTo,
noun, shortNoun,
),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
}
} else {
stmtFrom.Put(absFrom, AbsMoveEndpoint{
Other: absTo,
StmtRange: stmt.DeclRange,
})
}
// There can only be one source for each destination address.
if existing, exists := stmtTo.GetOk(absTo); exists {
if !addrs.Equivalent(existing.Other, absFrom) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ambiguous move statements",
Detail: fmt.Sprintf(
"A statement at %s declared that %s moved to %s, but this statement instead declares that %s moved there.\n\nEach %s can have moved from only one source %s.",
existing.StmtRange.StartString(), existing.Other, absTo, absFrom,
noun, shortNoun,
),
Subject: stmt.DeclRange.ToHCL().Ptr(),
})
}
} else {
stmtTo.Put(absTo, AbsMoveEndpoint{
Other: absFrom,
StmtRange: stmt.DeclRange,
})
}
// Resource types must match.
if resourceTypesDiffer(absFrom, absTo) {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Resource type mismatch",
Detail: fmt.Sprintf(
"This statement declares a move from %s to %s, which is a %s of a different type.", absFrom, absTo, noun,
),
})
}
}
}
// If we're not already returning other errors then we'll also check for
// and report cycles.
//
// Cycles alone are difficult to report in a helpful way because we don't
// have enough context to guess the user's intent. However, some particular
// mistakes that might lead to a cycle can also be caught by other
// validation rules above where we can make better suggestions, and so
// we'll use a cycle report only as a last resort.
if !diags.HasErrors() {
diags = diags.Append(validateMoveStatementGraph(g))
}
return diags
}
func validateMoveStatementGraph(g *dag.AcyclicGraph) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
for _, cycle := range g.Cycles() {
// Reporting cycles is awkward because there isn't any definitive
// way to decide which of the objects in the cycle is the cause of
// the problem. Therefore we'll just list them all out and leave
// the user to figure it out. :(
stmtStrs := make([]string, 0, len(cycle))
for _, stmtI := range cycle {
// move statement graph nodes are pointers to move statements
stmt := stmtI.(*MoveStatement)
stmtStrs = append(stmtStrs, fmt.Sprintf(
"\n - %s: %s → %s",
stmt.DeclRange.StartString(),
stmt.From.String(),
stmt.To.String(),
))
}
sort.Strings(stmtStrs) // just to make the order deterministic
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Cyclic dependency in move statements",
fmt.Sprintf(
"The following chained move statements form a cycle, and so there is no final location to move objects to:%s\n\nA chain of move statements must end with an address that doesn't appear in any other statements, and which typically also refers to an object still declared in the configuration.",
strings.Join(stmtStrs, ""),
),
))
}
// Look for cycles to self.
// A user shouldn't be able to create self-references, but we cannot
// correctly process a graph with them.
for _, e := range g.Edges() {
src := e.Source()
if src == e.Target() {
diags = diags.Append(tfdiags.Sourceless(
tfdiags.Error,
"Self reference in move statements",
fmt.Sprintf(
"The move statement %s refers to itself the move dependency graph, which is invalid. This is a bug in Terraform; please report it!",
src.(*MoveStatement).Name(),
),
))
}
}
return diags
}
func moveableObjectExists(addr addrs.AbsMoveable, in instances.Set) bool {
switch addr := addr.(type) {
case addrs.ModuleInstance:
return in.HasModuleInstance(addr)
case addrs.AbsModuleCall:
return in.HasModuleCall(addr)
case addrs.AbsResourceInstance:
return in.HasResourceInstance(addr)
case addrs.AbsResource:
return in.HasResource(addr)
default:
// The above cases should cover all of the AbsMoveable types
panic("unsupported AbsMoveable address type")
}
}
func resourceTypesDiffer(absFrom, absTo addrs.AbsMoveable) bool {
switch absFrom := absFrom.(type) {
case addrs.AbsMoveableResource:
// addrs.UnifyMoveEndpoints guarantees that both addresses are of the
// same kind, so at this point we can assume that absTo is also an
// addrs.AbsResourceInstance or addrs.AbsResource.
absTo := absTo.(addrs.AbsMoveableResource)
return absFrom.AffectedAbsResource().Resource.Type != absTo.AffectedAbsResource().Resource.Type
default:
return false
}
}
func movableObjectDeclRange(addr addrs.AbsMoveable, cfg *configs.Config) (tfdiags.SourceRange, bool) {
switch addr := addr.(type) {
case addrs.ModuleInstance:
// For a module instance we're actually looking for the call that
// declared it, which belongs to the parent module.
// (NOTE: This assumes "addr" can never be the root module instance,
// because the root module is never moveable.)
parentAddr, callAddr := addr.Call()
modCfg := cfg.DescendentForInstance(parentAddr)
if modCfg == nil {
return tfdiags.SourceRange{}, false
}
call := modCfg.Module.ModuleCalls[callAddr.Name]
if call == nil {
return tfdiags.SourceRange{}, false
}
// If the call has either count or for_each set then we'll "blame"
// that expression, rather than the block as a whole, because it's
// the expression that decides which instances are available.
switch {
case call.ForEach != nil:
return tfdiags.SourceRangeFromHCL(call.ForEach.Range()), true
case call.Count != nil:
return tfdiags.SourceRangeFromHCL(call.Count.Range()), true
default:
return tfdiags.SourceRangeFromHCL(call.DeclRange), true
}
case addrs.AbsModuleCall:
modCfg := cfg.DescendentForInstance(addr.Module)
if modCfg == nil {
return tfdiags.SourceRange{}, false
}
call := modCfg.Module.ModuleCalls[addr.Call.Name]
if call == nil {
return tfdiags.SourceRange{}, false
}
return tfdiags.SourceRangeFromHCL(call.DeclRange), true
case addrs.AbsResourceInstance:
modCfg := cfg.DescendentForInstance(addr.Module)
if modCfg == nil {
return tfdiags.SourceRange{}, false
}
rc := modCfg.Module.ResourceByAddr(addr.Resource.Resource)
if rc == nil {
return tfdiags.SourceRange{}, false
}
// If the resource has either count or for_each set then we'll "blame"
// that expression, rather than the block as a whole, because it's
// the expression that decides which instances are available.
switch {
case rc.ForEach != nil:
return tfdiags.SourceRangeFromHCL(rc.ForEach.Range()), true
case rc.Count != nil:
return tfdiags.SourceRangeFromHCL(rc.Count.Range()), true
default:
return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
}
case addrs.AbsResource:
modCfg := cfg.DescendentForInstance(addr.Module)
if modCfg == nil {
return tfdiags.SourceRange{}, false
}
rc := modCfg.Module.ResourceByAddr(addr.Resource)
if rc == nil {
return tfdiags.SourceRange{}, false
}
return tfdiags.SourceRangeFromHCL(rc.DeclRange), true
default:
// The above cases should cover all of the AbsMoveable types
panic("unsupported AbsMoveable address type")
}
}
func callsThroughModulePackage(modCfg *configs.Config, callSteps []addrs.ModuleCall) addrs.ModuleSource {
var sourceAddr addrs.ModuleSource
current := modCfg
for _, step := range callSteps {
call := current.Module.ModuleCalls[step.Name]
if call == nil {
break
}
if call.EntersNewPackage() {
sourceAddr = call.SourceAddr
}
current = modCfg.Children[step.Name]
if current == nil {
// Weird to have a call but not a config, but we'll tolerate
// it to avoid crashing here.
break
}
}
return sourceAddr
}