-
Notifications
You must be signed in to change notification settings - Fork 9.5k
/
move_statement.go
186 lines (163 loc) · 7.02 KB
/
move_statement.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
package refactoring
import (
"fmt"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/states"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type MoveStatement struct {
From, To *addrs.MoveEndpointInModule
DeclRange tfdiags.SourceRange
// Implied is true for statements produced by ImpliedMoveStatements, and
// false for statements produced by FindMoveStatements.
//
// An "implied" statement is one that has no explicit "moved" block in
// the configuration and was instead generated automatically based on a
// comparison between current configuration and previous run state.
// For implied statements, the DeclRange field contains the source location
// of something in the source code that implied the statement, in which
// case it would probably be confusing to show that source range to the
// user, e.g. in an error message, without clearly mentioning that it's
// related to an implied move statement.
Implied bool
}
// FindMoveStatements recurses through the modules of the given configuration
// and returns a flat set of all "moved" blocks defined within, in a
// deterministic but undefined order.
func FindMoveStatements(rootCfg *configs.Config) []MoveStatement {
return findMoveStatements(rootCfg, nil)
}
func findMoveStatements(cfg *configs.Config, into []MoveStatement) []MoveStatement {
modAddr := cfg.Path
for _, mc := range cfg.Module.Moved {
fromAddr, toAddr := addrs.UnifyMoveEndpoints(modAddr, mc.From, mc.To)
if fromAddr == nil || toAddr == nil {
// Invalid combination should've been caught during original
// configuration decoding, in the configs package.
panic(fmt.Sprintf("incompatible move endpoints in %s", mc.DeclRange))
}
into = append(into, MoveStatement{
From: fromAddr,
To: toAddr,
DeclRange: tfdiags.SourceRangeFromHCL(mc.DeclRange),
Implied: false,
})
}
for _, childCfg := range cfg.Children {
into = findMoveStatements(childCfg, into)
}
return into
}
// ImpliedMoveStatements compares addresses in the given state with addresses
// in the given configuration and potentially returns additional MoveStatement
// objects representing moves we infer automatically, even though they aren't
// explicitly recorded in the configuration.
//
// We do this primarily for backward compatibility with behaviors of Terraform
// versions prior to introducing explicit "moved" blocks. Specifically, this
// function aims to achieve the same result as the "NodeCountBoundary"
// heuristic from Terraform v1.0 and earlier, where adding or removing the
// "count" meta-argument from an already-created resource can automatically
// preserve the zeroth or the NoKey instance, depending on the direction of
// the change. We do this only for resources that aren't mentioned already
// in at least one explicit move statement.
//
// As with the previous-version heuristics it replaces, this is a best effort
// and doesn't handle all situations. An explicit move statement is always
// preferred, but our goal here is to match exactly the same cases that the
// old heuristic would've matched, to retain compatibility for existing modules.
//
// We should think very hard before adding any _new_ implication rules for
// moved statements.
func ImpliedMoveStatements(rootCfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement) []MoveStatement {
return impliedMoveStatements(rootCfg, prevRunState, explicitStmts, nil)
}
func impliedMoveStatements(cfg *configs.Config, prevRunState *states.State, explicitStmts []MoveStatement, into []MoveStatement) []MoveStatement {
modAddr := cfg.Path
// There can be potentially many instances of the module, so we need
// to consider each of them separately.
for _, modState := range prevRunState.ModuleInstances(modAddr) {
// What we're looking for here is either a no-key resource instance
// where the configuration has count set or a zero-key resource
// instance where the configuration _doesn't_ have count set.
// If so, we'll generate a statement replacing no-key with zero-key or
// vice-versa.
for _, rState := range modState.Resources {
rAddr := rState.Addr
rCfg := cfg.Module.ResourceByAddr(rAddr.Resource)
if rCfg == nil {
// If there's no configuration at all then there can't be any
// automatic move fixup to do.
continue
}
approxSrcRange := tfdiags.SourceRangeFromHCL(rCfg.DeclRange)
// NOTE: We're intentionally not checking to see whether the
// "to" addresses in our implied statements already have
// instances recorded in state, because ApplyMoves should
// deal with such conflicts in a deterministic way for both
// explicit and implicit moves, and we'd rather have that
// handled all in one place.
var fromKey, toKey addrs.InstanceKey
switch {
case rCfg.Count != nil:
// If we have a count expression then we'll use _that_ as
// a slightly-more-precise approximate source range.
approxSrcRange = tfdiags.SourceRangeFromHCL(rCfg.Count.Range())
if riState := rState.Instances[addrs.NoKey]; riState != nil {
fromKey = addrs.NoKey
toKey = addrs.IntKey(0)
}
case rCfg.Count == nil && rCfg.ForEach == nil: // no repetition at all
if riState := rState.Instances[addrs.IntKey(0)]; riState != nil {
fromKey = addrs.IntKey(0)
toKey = addrs.NoKey
}
}
if fromKey != toKey {
// We mustn't generate an impied statement if the user already
// wrote an explicit statement referring to this resource,
// because they may wish to select an instance key other than
// zero as the one to retain.
if !haveMoveStatementForResource(rAddr, explicitStmts) {
into = append(into, MoveStatement{
From: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(fromKey), approxSrcRange),
To: addrs.ImpliedMoveStatementEndpoint(rAddr.Instance(toKey), approxSrcRange),
DeclRange: approxSrcRange,
Implied: true,
})
}
}
}
}
for _, childCfg := range cfg.Children {
into = impliedMoveStatements(childCfg, prevRunState, explicitStmts, into)
}
return into
}
func (s *MoveStatement) ObjectKind() addrs.MoveEndpointKind {
// addrs.UnifyMoveEndpoints guarantees that both of our addresses have
// the same kind, so we can just arbitrary use From and assume To will
// match it.
return s.From.ObjectKind()
}
// Name is used internally for displaying the statement graph
func (s *MoveStatement) Name() string {
return fmt.Sprintf("%s->%s", s.From, s.To)
}
func haveMoveStatementForResource(addr addrs.AbsResource, stmts []MoveStatement) bool {
// This is not a particularly optimal way to answer this question,
// particularly since our caller calls this function in a loop already,
// but we expect the total number of explicit statements to be small
// in any reasonable Terraform configuration and so a more complicated
// approach wouldn't be justified here.
for _, stmt := range stmts {
if stmt.From.SelectsResource(addr) {
return true
}
if stmt.To.SelectsResource(addr) {
return true
}
}
return false
}