-
Notifications
You must be signed in to change notification settings - Fork 1
/
attribute.go
232 lines (199 loc) · 6.93 KB
/
attribute.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
package tfplanparse
import (
"fmt"
"strconv"
"strings"
)
const (
ATTRIBUTE_CHANGE_DELIMITER = " -> "
ATTRIBUTE_DEFINITON_DELIMITER = " = "
SENSITIVE_VALUE = "(sensitive value)"
COMPUTED_VALUE = "(known after apply)"
)
type attributeChange interface {
GetName() string
GetUpdateType() UpdateType
GetBefore(opts ...GetBeforeAfterOptions) interface{}
GetAfter(opts ...GetBeforeAfterOptions) interface{}
IsComputed() bool
IsSensitive() bool
IsNoOp() bool
}
type AttributeChange struct {
Name string
OldValue interface{}
NewValue interface{}
UpdateType UpdateType
}
var _ attributeChange = &AttributeChange{}
// IsAttributeChangeLine returns true if the line is a valid attribute change
// This requires the line to start with "+", "-" or "~", and not be followed with "resource"
func IsAttributeChangeLine(line string) bool {
line = strings.TrimSpace(line)
attribute := strings.SplitN(removeChangeTypeCharacters(line), ATTRIBUTE_DEFINITON_DELIMITER, 2)
// validPrefix := strings.HasPrefix(line, "+") || strings.HasPrefix(line, "-") || strings.HasPrefix(line, "~")
multilineAttribute := strings.HasSuffix(line, "(") || strings.HasSuffix(line, "{")
return len(attribute) == 2 && !multilineAttribute && !IsResourceChangeLine(line)
}
// IsAttributeChangeArrayItem returns true if the line is a valid attribute change in an array
func IsAttributeChangeArrayItem(line string) bool {
line = strings.TrimSpace(line)
validSuffix := strings.HasSuffix(line, ",")
multilineAttribute := strings.HasSuffix(line, "(") || strings.HasSuffix(line, "{")
return validSuffix && !multilineAttribute && !IsResourceChangeLine(line)
}
// NewAttributeChangeFromLine initializes an AttributeChange from a line containing an attribute change
// It expects a line that passes the IsAttributeChangeLine check
func NewAttributeChangeFromLine(line string) (*AttributeChange, error) {
line = strings.TrimSpace(line)
if !IsAttributeChangeLine(line) {
return nil, fmt.Errorf("%s is not a valid line to initialize an attributeChange", line)
}
attribute := strings.SplitN(removeChangeTypeCharacters(line), ATTRIBUTE_DEFINITON_DELIMITER, 2)
if strings.HasPrefix(line, "+") {
// add
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: nil,
NewValue: doTypeConversion(attribute[1]),
UpdateType: NewResource,
}, nil
} else if strings.HasPrefix(line, "-") {
// destroy
values := strings.Split(attribute[1], ATTRIBUTE_CHANGE_DELIMITER)
if len(values) != 2 {
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: doTypeConversion(strings.TrimSpace(attribute[1])),
NewValue: nil,
UpdateType: DestroyResource,
}, nil
}
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: doTypeConversion(values[0]),
NewValue: nil,
UpdateType: DestroyResource,
}, nil
} else if strings.HasPrefix(line, "~") {
// replace
updateType := UpdateInPlaceResource
if strings.HasSuffix(attribute[1], " # forces replacement") {
updateType = ForceReplaceResource
attribute[1] = strings.TrimSuffix(attribute[1], " # forces replacement")
}
values := strings.Split(attribute[1], ATTRIBUTE_CHANGE_DELIMITER)
if len(values) != 2 {
if values[0] != SENSITIVE_VALUE {
return nil, fmt.Errorf("failed to read attribute change from line %s", line)
}
values = append(values, SENSITIVE_VALUE)
}
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: doTypeConversion(values[0]),
NewValue: doTypeConversion(values[1]),
UpdateType: updateType,
}, nil
} else {
return &AttributeChange{
Name: dequote(strings.TrimSpace(attribute[0])),
OldValue: doTypeConversion(attribute[1]),
NewValue: doTypeConversion(attribute[1]),
UpdateType: NoOpResource,
}, nil
}
}
// NewAttributeChangeFromLine initializes an AttributeChange from a line within an Array attribute
// In an array resource, the attribute change does not have a name
func NewAttributeChangeFromArray(line string) (*AttributeChange, error) {
line = strings.TrimSpace(line)
if line == "" || line == "}" || IsResourceChangeLine(line) {
return nil, fmt.Errorf("%s is not a valid line to initialize an attributeChange", line)
}
if strings.HasPrefix(line, "+") {
// add
return &AttributeChange{
OldValue: nil,
NewValue: normalizeArrayAttribute(line),
UpdateType: NewResource,
}, nil
} else if strings.HasPrefix(line, "-") {
// destroy
return &AttributeChange{
OldValue: normalizeArrayAttribute(line),
NewValue: nil,
UpdateType: DestroyResource,
}, nil
} else if strings.HasPrefix(line, "~") {
// replace
// TODO: confirm this is possible? I think array entries are immutable
return nil, fmt.Errorf("unexpected replace single attribute in array %s", line)
} else {
return &AttributeChange{
OldValue: normalizeArrayAttribute(line),
NewValue: normalizeArrayAttribute(line),
UpdateType: NoOpResource,
}, nil
}
}
// GetName returns the name of the attribute
func (a *AttributeChange) GetName() string {
return a.Name
}
// GetUpdateType returns the UpdateType of the attribute
func (a *AttributeChange) GetUpdateType() UpdateType {
return a.UpdateType
}
// GetBefore returns the initial value of the attribute
func (a *AttributeChange) GetBefore(opts ...GetBeforeAfterOptions) interface{} {
return a.OldValue
}
// GetAfter returns the planned value of the attribute
func (a *AttributeChange) GetAfter(opts ...GetBeforeAfterOptions) interface{} {
return a.NewValue
}
// IsSensitive returns true if the attribute contains a sensitive value
func (a *AttributeChange) IsSensitive() bool {
return a.OldValue == SENSITIVE_VALUE || a.NewValue == SENSITIVE_VALUE
}
// IsComputed returns true if the attribute contains a computed value
func (a *AttributeChange) IsComputed() bool {
return a.OldValue == COMPUTED_VALUE || a.NewValue == COMPUTED_VALUE
}
// IsNoOp returns true if the attribute has not changed
func (a *AttributeChange) IsNoOp() bool {
return a.UpdateType == NoOpResource
}
func doTypeConversion(input string) interface{} {
// if it has quotes, assume it is a string and return it without quotes
if strings.HasPrefix(input, `"`) && strings.HasSuffix(input, `"`) {
return dequote(input)
}
if input == "{}" {
return nil
}
if input == "true" || input == "false" {
b, err := strconv.ParseBool(input)
if err != nil {
return input
}
return b
}
if i, err := strconv.Atoi(input); err == nil {
return i
}
if f, err := strconv.ParseFloat(input, 64); err == nil {
return f
}
return input
}
func normalizeArrayAttribute(line string) interface{} {
return doTypeConversion(strings.TrimRight(removeChangeTypeCharacters(line), ","))
}
func removeChangeTypeCharacters(line string) string {
return strings.TrimLeft(line, "+/-~<= ")
}
func dequote(line string) string {
return strings.TrimPrefix(strings.TrimSuffix(line, "\""), "\"")
}