]>
Commit | Line | Data |
---|---|---|
bae9f6d2 JC |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | "log" | |
6 | "reflect" | |
bae9f6d2 JC |
7 | "strings" |
8 | ||
107c1cdb ND |
9 | "github.com/hashicorp/hcl2/hcl" |
10 | "github.com/hashicorp/terraform/configs" | |
11 | ||
12 | "github.com/hashicorp/terraform/addrs" | |
13 | ||
bae9f6d2 JC |
14 | "github.com/hashicorp/terraform/config" |
15 | "github.com/hashicorp/terraform/config/module" | |
107c1cdb ND |
16 | "github.com/zclconf/go-cty/cty" |
17 | "github.com/zclconf/go-cty/cty/convert" | |
bae9f6d2 JC |
18 | ) |
19 | ||
20 | // EvalTypeCheckVariable is an EvalNode which ensures that the variable | |
21 | // values which are assigned as inputs to a module (including the root) | |
22 | // match the types which are either declared for the variables explicitly | |
23 | // or inferred from the default values. | |
24 | // | |
25 | // In order to achieve this three things are required: | |
26 | // - a map of the proposed variable values | |
27 | // - the configuration tree of the module in which the variable is | |
28 | // declared | |
29 | // - the path to the module (so we know which part of the tree to | |
30 | // compare the values against). | |
31 | type EvalTypeCheckVariable struct { | |
32 | Variables map[string]interface{} | |
33 | ModulePath []string | |
34 | ModuleTree *module.Tree | |
35 | } | |
36 | ||
37 | func (n *EvalTypeCheckVariable) Eval(ctx EvalContext) (interface{}, error) { | |
38 | currentTree := n.ModuleTree | |
39 | for _, pathComponent := range n.ModulePath[1:] { | |
40 | currentTree = currentTree.Children()[pathComponent] | |
41 | } | |
42 | targetConfig := currentTree.Config() | |
43 | ||
44 | prototypes := make(map[string]config.VariableType) | |
45 | for _, variable := range targetConfig.Variables { | |
46 | prototypes[variable.Name] = variable.Type() | |
47 | } | |
48 | ||
49 | // Only display a module in an error message if we are not in the root module | |
50 | modulePathDescription := fmt.Sprintf(" in module %s", strings.Join(n.ModulePath[1:], ".")) | |
51 | if len(n.ModulePath) == 1 { | |
52 | modulePathDescription = "" | |
53 | } | |
54 | ||
55 | for name, declaredType := range prototypes { | |
56 | proposedValue, ok := n.Variables[name] | |
57 | if !ok { | |
58 | // This means the default value should be used as no overriding value | |
59 | // has been set. Therefore we should continue as no check is necessary. | |
60 | continue | |
61 | } | |
62 | ||
63 | if proposedValue == config.UnknownVariableValue { | |
64 | continue | |
65 | } | |
66 | ||
67 | switch declaredType { | |
68 | case config.VariableTypeString: | |
69 | switch proposedValue.(type) { | |
70 | case string: | |
71 | continue | |
72 | default: | |
73 | return nil, fmt.Errorf("variable %s%s should be type %s, got %s", | |
74 | name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue)) | |
75 | } | |
76 | case config.VariableTypeMap: | |
77 | switch proposedValue.(type) { | |
78 | case map[string]interface{}: | |
79 | continue | |
80 | default: | |
81 | return nil, fmt.Errorf("variable %s%s should be type %s, got %s", | |
82 | name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue)) | |
83 | } | |
84 | case config.VariableTypeList: | |
85 | switch proposedValue.(type) { | |
86 | case []interface{}: | |
87 | continue | |
88 | default: | |
89 | return nil, fmt.Errorf("variable %s%s should be type %s, got %s", | |
90 | name, modulePathDescription, declaredType.Printable(), hclTypeName(proposedValue)) | |
91 | } | |
92 | default: | |
93 | return nil, fmt.Errorf("variable %s%s should be type %s, got type string", | |
94 | name, modulePathDescription, declaredType.Printable()) | |
95 | } | |
96 | } | |
97 | ||
98 | return nil, nil | |
99 | } | |
100 | ||
107c1cdb ND |
101 | // EvalSetModuleCallArguments is an EvalNode implementation that sets values |
102 | // for arguments of a child module call, for later retrieval during | |
103 | // expression evaluation. | |
104 | type EvalSetModuleCallArguments struct { | |
105 | Module addrs.ModuleCallInstance | |
106 | Values map[string]cty.Value | |
bae9f6d2 JC |
107 | } |
108 | ||
109 | // TODO: test | |
107c1cdb ND |
110 | func (n *EvalSetModuleCallArguments) Eval(ctx EvalContext) (interface{}, error) { |
111 | ctx.SetModuleCallArguments(n.Module, n.Values) | |
bae9f6d2 JC |
112 | return nil, nil |
113 | } | |
114 | ||
107c1cdb ND |
115 | // EvalModuleCallArgument is an EvalNode implementation that produces the value |
116 | // for a particular variable as will be used by a child module instance. | |
117 | // | |
118 | // The result is written into the map given in Values, with its key | |
119 | // set to the local name of the variable, disregarding the module instance | |
120 | // address. Any existing values in that map are deleted first. This weird | |
121 | // interface is a result of trying to be convenient for use with | |
122 | // EvalContext.SetModuleCallArguments, which expects a map to merge in with | |
123 | // any existing arguments. | |
124 | type EvalModuleCallArgument struct { | |
125 | Addr addrs.InputVariable | |
126 | Config *configs.Variable | |
127 | Expr hcl.Expression | |
128 | ||
129 | // If this flag is set, any diagnostics are discarded and this operation | |
130 | // will always succeed, though may produce an unknown value in the | |
131 | // event of an error. | |
132 | IgnoreDiagnostics bool | |
133 | ||
134 | Values map[string]cty.Value | |
bae9f6d2 JC |
135 | } |
136 | ||
107c1cdb | 137 | func (n *EvalModuleCallArgument) Eval(ctx EvalContext) (interface{}, error) { |
bae9f6d2 | 138 | // Clear out the existing mapping |
107c1cdb ND |
139 | for k := range n.Values { |
140 | delete(n.Values, k) | |
bae9f6d2 JC |
141 | } |
142 | ||
107c1cdb ND |
143 | wantType := n.Config.Type |
144 | name := n.Addr.Name | |
145 | expr := n.Expr | |
146 | ||
147 | if expr == nil { | |
148 | // Should never happen, but we'll bail out early here rather than | |
149 | // crash in case it does. We set no value at all in this case, | |
150 | // making a subsequent call to EvalContext.SetModuleCallArguments | |
151 | // a no-op. | |
152 | log.Printf("[ERROR] attempt to evaluate %s with nil expression", n.Addr.String()) | |
153 | return nil, nil | |
bae9f6d2 JC |
154 | } |
155 | ||
107c1cdb ND |
156 | val, diags := ctx.EvaluateExpr(expr, cty.DynamicPseudoType, nil) |
157 | ||
158 | // We intentionally passed DynamicPseudoType to EvaluateExpr above because | |
159 | // now we can do our own local type conversion and produce an error message | |
160 | // with better context if it fails. | |
161 | var convErr error | |
162 | val, convErr = convert.Convert(val, wantType) | |
163 | if convErr != nil { | |
164 | diags = diags.Append(&hcl.Diagnostic{ | |
165 | Severity: hcl.DiagError, | |
166 | Summary: "Invalid value for module argument", | |
167 | Detail: fmt.Sprintf( | |
168 | "The given value is not suitable for child module variable %q defined at %s: %s.", | |
169 | name, n.Config.DeclRange.String(), convErr, | |
170 | ), | |
171 | Subject: expr.Range().Ptr(), | |
172 | }) | |
173 | // We'll return a placeholder unknown value to avoid producing | |
174 | // redundant downstream errors. | |
175 | val = cty.UnknownVal(wantType) | |
bae9f6d2 JC |
176 | } |
177 | ||
107c1cdb ND |
178 | n.Values[name] = val |
179 | if n.IgnoreDiagnostics { | |
180 | return nil, nil | |
bae9f6d2 | 181 | } |
107c1cdb | 182 | return nil, diags.ErrWithWarnings() |
bae9f6d2 JC |
183 | } |
184 | ||
185 | // hclTypeName returns the name of the type that would represent this value in | |
186 | // a config file, or falls back to the Go type name if there's no corresponding | |
187 | // HCL type. This is used for formatted output, not for comparing types. | |
188 | func hclTypeName(i interface{}) string { | |
189 | switch k := reflect.Indirect(reflect.ValueOf(i)).Kind(); k { | |
190 | case reflect.Bool: | |
191 | return "boolean" | |
192 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, | |
193 | reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, | |
194 | reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64: | |
195 | return "number" | |
196 | case reflect.Array, reflect.Slice: | |
197 | return "list" | |
198 | case reflect.Map: | |
199 | return "map" | |
200 | case reflect.String: | |
201 | return "string" | |
202 | default: | |
203 | // fall back to the Go type if there's no match | |
204 | return k.String() | |
205 | } | |
206 | } |