]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package configs |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | ||
6 | "github.com/hashicorp/hcl2/ext/typeexpr" | |
7 | "github.com/hashicorp/hcl2/gohcl" | |
8 | "github.com/hashicorp/hcl2/hcl" | |
9 | "github.com/hashicorp/hcl2/hcl/hclsyntax" | |
10 | "github.com/zclconf/go-cty/cty" | |
11 | "github.com/zclconf/go-cty/cty/convert" | |
12 | ||
13 | "github.com/hashicorp/terraform/addrs" | |
14 | ) | |
15 | ||
16 | // A consistent detail message for all "not a valid identifier" diagnostics. | |
17 | const badIdentifierDetail = "A name must start with a letter and may contain only letters, digits, underscores, and dashes." | |
18 | ||
19 | // Variable represents a "variable" block in a module or file. | |
20 | type Variable struct { | |
21 | Name string | |
22 | Description string | |
23 | Default cty.Value | |
24 | Type cty.Type | |
25 | ParsingMode VariableParsingMode | |
26 | ||
27 | DescriptionSet bool | |
28 | ||
29 | DeclRange hcl.Range | |
30 | } | |
31 | ||
32 | func decodeVariableBlock(block *hcl.Block, override bool) (*Variable, hcl.Diagnostics) { | |
33 | v := &Variable{ | |
34 | Name: block.Labels[0], | |
35 | DeclRange: block.DefRange, | |
36 | } | |
37 | ||
38 | // Unless we're building an override, we'll set some defaults | |
39 | // which we might override with attributes below. We leave these | |
40 | // as zero-value in the override case so we can recognize whether | |
41 | // or not they are set when we merge. | |
42 | if !override { | |
43 | v.Type = cty.DynamicPseudoType | |
44 | v.ParsingMode = VariableParseLiteral | |
45 | } | |
46 | ||
47 | content, diags := block.Body.Content(variableBlockSchema) | |
48 | ||
49 | if !hclsyntax.ValidIdentifier(v.Name) { | |
50 | diags = append(diags, &hcl.Diagnostic{ | |
51 | Severity: hcl.DiagError, | |
52 | Summary: "Invalid variable name", | |
53 | Detail: badIdentifierDetail, | |
54 | Subject: &block.LabelRanges[0], | |
55 | }) | |
56 | } | |
57 | ||
58 | // Don't allow declaration of variables that would conflict with the | |
59 | // reserved attribute and block type names in a "module" block, since | |
60 | // these won't be usable for child modules. | |
61 | for _, attr := range moduleBlockSchema.Attributes { | |
62 | if attr.Name == v.Name { | |
63 | diags = append(diags, &hcl.Diagnostic{ | |
64 | Severity: hcl.DiagError, | |
65 | Summary: "Invalid variable name", | |
66 | Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", attr.Name), | |
67 | Subject: &block.LabelRanges[0], | |
68 | }) | |
69 | } | |
70 | } | |
71 | for _, blockS := range moduleBlockSchema.Blocks { | |
72 | if blockS.Type == v.Name { | |
73 | diags = append(diags, &hcl.Diagnostic{ | |
74 | Severity: hcl.DiagError, | |
75 | Summary: "Invalid variable name", | |
76 | Detail: fmt.Sprintf("The variable name %q is reserved due to its special meaning inside module blocks.", blockS.Type), | |
77 | Subject: &block.LabelRanges[0], | |
78 | }) | |
79 | } | |
80 | } | |
81 | ||
82 | if attr, exists := content.Attributes["description"]; exists { | |
83 | valDiags := gohcl.DecodeExpression(attr.Expr, nil, &v.Description) | |
84 | diags = append(diags, valDiags...) | |
85 | v.DescriptionSet = true | |
86 | } | |
87 | ||
88 | if attr, exists := content.Attributes["type"]; exists { | |
89 | ty, parseMode, tyDiags := decodeVariableType(attr.Expr) | |
90 | diags = append(diags, tyDiags...) | |
91 | v.Type = ty | |
92 | v.ParsingMode = parseMode | |
93 | } | |
94 | ||
95 | if attr, exists := content.Attributes["default"]; exists { | |
96 | val, valDiags := attr.Expr.Value(nil) | |
97 | diags = append(diags, valDiags...) | |
98 | ||
99 | // Convert the default to the expected type so we can catch invalid | |
100 | // defaults early and allow later code to assume validity. | |
101 | // Note that this depends on us having already processed any "type" | |
102 | // attribute above. | |
103 | // However, we can't do this if we're in an override file where | |
104 | // the type might not be set; we'll catch that during merge. | |
105 | if v.Type != cty.NilType { | |
106 | var err error | |
107 | val, err = convert.Convert(val, v.Type) | |
108 | if err != nil { | |
109 | diags = append(diags, &hcl.Diagnostic{ | |
110 | Severity: hcl.DiagError, | |
111 | Summary: "Invalid default value for variable", | |
112 | Detail: fmt.Sprintf("This default value is not compatible with the variable's type constraint: %s.", err), | |
113 | Subject: attr.Expr.Range().Ptr(), | |
114 | }) | |
115 | val = cty.DynamicVal | |
116 | } | |
117 | } | |
118 | ||
119 | v.Default = val | |
120 | } | |
121 | ||
122 | return v, diags | |
123 | } | |
124 | ||
125 | func decodeVariableType(expr hcl.Expression) (cty.Type, VariableParsingMode, hcl.Diagnostics) { | |
126 | if exprIsNativeQuotedString(expr) { | |
127 | // Here we're accepting the pre-0.12 form of variable type argument where | |
128 | // the string values "string", "list" and "map" are accepted has a hint | |
129 | // about the type used primarily for deciding how to parse values | |
130 | // given on the command line and in environment variables. | |
131 | // Only the native syntax ends up in this codepath; we handle the | |
132 | // JSON syntax (which is, of course, quoted even in the new format) | |
133 | // in the normal codepath below. | |
134 | val, diags := expr.Value(nil) | |
135 | if diags.HasErrors() { | |
136 | return cty.DynamicPseudoType, VariableParseHCL, diags | |
137 | } | |
138 | str := val.AsString() | |
139 | switch str { | |
140 | case "string": | |
141 | return cty.String, VariableParseLiteral, diags | |
142 | case "list": | |
143 | return cty.List(cty.DynamicPseudoType), VariableParseHCL, diags | |
144 | case "map": | |
145 | return cty.Map(cty.DynamicPseudoType), VariableParseHCL, diags | |
146 | default: | |
147 | return cty.DynamicPseudoType, VariableParseHCL, hcl.Diagnostics{{ | |
148 | Severity: hcl.DiagError, | |
149 | Summary: "Invalid legacy variable type hint", | |
150 | Detail: `The legacy variable type hint form, using a quoted string, allows only the values "string", "list", and "map". To provide a full type expression, remove the surrounding quotes and give the type expression directly.`, | |
151 | Subject: expr.Range().Ptr(), | |
152 | }} | |
153 | } | |
154 | } | |
155 | ||
156 | // First we'll deal with some shorthand forms that the HCL-level type | |
157 | // expression parser doesn't include. These both emulate pre-0.12 behavior | |
158 | // of allowing a list or map of any element type as long as all of the | |
159 | // elements are consistent. This is the same as list(any) or map(any). | |
160 | switch hcl.ExprAsKeyword(expr) { | |
161 | case "list": | |
162 | return cty.List(cty.DynamicPseudoType), VariableParseHCL, nil | |
163 | case "map": | |
164 | return cty.Map(cty.DynamicPseudoType), VariableParseHCL, nil | |
165 | } | |
166 | ||
167 | ty, diags := typeexpr.TypeConstraint(expr) | |
168 | if diags.HasErrors() { | |
169 | return cty.DynamicPseudoType, VariableParseHCL, diags | |
170 | } | |
171 | ||
172 | switch { | |
173 | case ty.IsPrimitiveType(): | |
174 | // Primitive types use literal parsing. | |
175 | return ty, VariableParseLiteral, diags | |
176 | default: | |
177 | // Everything else uses HCL parsing | |
178 | return ty, VariableParseHCL, diags | |
179 | } | |
180 | } | |
181 | ||
182 | // VariableParsingMode defines how values of a particular variable given by | |
183 | // text-only mechanisms (command line arguments and environment variables) | |
184 | // should be parsed to produce the final value. | |
185 | type VariableParsingMode rune | |
186 | ||
187 | // VariableParseLiteral is a variable parsing mode that just takes the given | |
188 | // string directly as a cty.String value. | |
189 | const VariableParseLiteral VariableParsingMode = 'L' | |
190 | ||
191 | // VariableParseHCL is a variable parsing mode that attempts to parse the given | |
192 | // string as an HCL expression and returns the result. | |
193 | const VariableParseHCL VariableParsingMode = 'H' | |
194 | ||
195 | // Parse uses the receiving parsing mode to process the given variable value | |
196 | // string, returning the result along with any diagnostics. | |
197 | // | |
198 | // A VariableParsingMode does not know the expected type of the corresponding | |
199 | // variable, so it's the caller's responsibility to attempt to convert the | |
200 | // result to the appropriate type and return to the user any diagnostics that | |
201 | // conversion may produce. | |
202 | // | |
203 | // The given name is used to create a synthetic filename in case any diagnostics | |
204 | // must be generated about the given string value. This should be the name | |
205 | // of the root module variable whose value will be populated from the given | |
206 | // string. | |
207 | // | |
208 | // If the returned diagnostics has errors, the returned value may not be | |
209 | // valid. | |
210 | func (m VariableParsingMode) Parse(name, value string) (cty.Value, hcl.Diagnostics) { | |
211 | switch m { | |
212 | case VariableParseLiteral: | |
213 | return cty.StringVal(value), nil | |
214 | case VariableParseHCL: | |
215 | fakeFilename := fmt.Sprintf("<value for var.%s>", name) | |
216 | expr, diags := hclsyntax.ParseExpression([]byte(value), fakeFilename, hcl.Pos{Line: 1, Column: 1}) | |
217 | if diags.HasErrors() { | |
218 | return cty.DynamicVal, diags | |
219 | } | |
220 | val, valDiags := expr.Value(nil) | |
221 | diags = append(diags, valDiags...) | |
222 | return val, diags | |
223 | default: | |
224 | // Should never happen | |
225 | panic(fmt.Errorf("Parse called on invalid VariableParsingMode %#v", m)) | |
226 | } | |
227 | } | |
228 | ||
229 | // Output represents an "output" block in a module or file. | |
230 | type Output struct { | |
231 | Name string | |
232 | Description string | |
233 | Expr hcl.Expression | |
234 | DependsOn []hcl.Traversal | |
235 | Sensitive bool | |
236 | ||
237 | DescriptionSet bool | |
238 | SensitiveSet bool | |
239 | ||
240 | DeclRange hcl.Range | |
241 | } | |
242 | ||
243 | func decodeOutputBlock(block *hcl.Block, override bool) (*Output, hcl.Diagnostics) { | |
244 | o := &Output{ | |
245 | Name: block.Labels[0], | |
246 | DeclRange: block.DefRange, | |
247 | } | |
248 | ||
249 | schema := outputBlockSchema | |
250 | if override { | |
251 | schema = schemaForOverrides(schema) | |
252 | } | |
253 | ||
254 | content, diags := block.Body.Content(schema) | |
255 | ||
256 | if !hclsyntax.ValidIdentifier(o.Name) { | |
257 | diags = append(diags, &hcl.Diagnostic{ | |
258 | Severity: hcl.DiagError, | |
259 | Summary: "Invalid output name", | |
260 | Detail: badIdentifierDetail, | |
261 | Subject: &block.LabelRanges[0], | |
262 | }) | |
263 | } | |
264 | ||
265 | if attr, exists := content.Attributes["description"]; exists { | |
266 | valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Description) | |
267 | diags = append(diags, valDiags...) | |
268 | o.DescriptionSet = true | |
269 | } | |
270 | ||
271 | if attr, exists := content.Attributes["value"]; exists { | |
272 | o.Expr = attr.Expr | |
273 | } | |
274 | ||
275 | if attr, exists := content.Attributes["sensitive"]; exists { | |
276 | valDiags := gohcl.DecodeExpression(attr.Expr, nil, &o.Sensitive) | |
277 | diags = append(diags, valDiags...) | |
278 | o.SensitiveSet = true | |
279 | } | |
280 | ||
281 | if attr, exists := content.Attributes["depends_on"]; exists { | |
282 | deps, depsDiags := decodeDependsOn(attr) | |
283 | diags = append(diags, depsDiags...) | |
284 | o.DependsOn = append(o.DependsOn, deps...) | |
285 | } | |
286 | ||
287 | return o, diags | |
288 | } | |
289 | ||
290 | // Local represents a single entry from a "locals" block in a module or file. | |
291 | // The "locals" block itself is not represented, because it serves only to | |
292 | // provide context for us to interpret its contents. | |
293 | type Local struct { | |
294 | Name string | |
295 | Expr hcl.Expression | |
296 | ||
297 | DeclRange hcl.Range | |
298 | } | |
299 | ||
300 | func decodeLocalsBlock(block *hcl.Block) ([]*Local, hcl.Diagnostics) { | |
301 | attrs, diags := block.Body.JustAttributes() | |
302 | if len(attrs) == 0 { | |
303 | return nil, diags | |
304 | } | |
305 | ||
306 | locals := make([]*Local, 0, len(attrs)) | |
307 | for name, attr := range attrs { | |
308 | if !hclsyntax.ValidIdentifier(name) { | |
309 | diags = append(diags, &hcl.Diagnostic{ | |
310 | Severity: hcl.DiagError, | |
311 | Summary: "Invalid local value name", | |
312 | Detail: badIdentifierDetail, | |
313 | Subject: &attr.NameRange, | |
314 | }) | |
315 | } | |
316 | ||
317 | locals = append(locals, &Local{ | |
318 | Name: name, | |
319 | Expr: attr.Expr, | |
320 | DeclRange: attr.Range, | |
321 | }) | |
322 | } | |
323 | return locals, diags | |
324 | } | |
325 | ||
326 | // Addr returns the address of the local value declared by the receiver, | |
327 | // relative to its containing module. | |
328 | func (l *Local) Addr() addrs.LocalValue { | |
329 | return addrs.LocalValue{ | |
330 | Name: l.Name, | |
331 | } | |
332 | } | |
333 | ||
334 | var variableBlockSchema = &hcl.BodySchema{ | |
335 | Attributes: []hcl.AttributeSchema{ | |
336 | { | |
337 | Name: "description", | |
338 | }, | |
339 | { | |
340 | Name: "default", | |
341 | }, | |
342 | { | |
343 | Name: "type", | |
344 | }, | |
345 | }, | |
346 | } | |
347 | ||
348 | var outputBlockSchema = &hcl.BodySchema{ | |
349 | Attributes: []hcl.AttributeSchema{ | |
350 | { | |
351 | Name: "description", | |
352 | }, | |
353 | { | |
354 | Name: "value", | |
355 | Required: true, | |
356 | }, | |
357 | { | |
358 | Name: "depends_on", | |
359 | }, | |
360 | { | |
361 | Name: "sensitive", | |
362 | }, | |
363 | }, | |
364 | } |