]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package terraform |
2 | ||
3 | import ( | |
4 | "context" | |
5 | "fmt" | |
6 | "log" | |
7 | "sort" | |
8 | ||
9 | "github.com/hashicorp/hcl2/hcl" | |
10 | "github.com/hashicorp/hcl2/hcldec" | |
11 | "github.com/zclconf/go-cty/cty" | |
12 | ||
13 | "github.com/hashicorp/terraform/addrs" | |
14 | "github.com/hashicorp/terraform/configs" | |
15 | "github.com/hashicorp/terraform/tfdiags" | |
16 | ) | |
17 | ||
18 | // Input asks for input to fill variables and provider configurations. | |
19 | // This modifies the configuration in-place, so asking for Input twice | |
20 | // may result in different UI output showing different current values. | |
21 | func (c *Context) Input(mode InputMode) tfdiags.Diagnostics { | |
22 | var diags tfdiags.Diagnostics | |
23 | defer c.acquireRun("input")() | |
24 | ||
25 | if c.uiInput == nil { | |
26 | log.Printf("[TRACE] Context.Input: uiInput is nil, so skipping") | |
27 | return diags | |
28 | } | |
29 | ||
30 | ctx := context.Background() | |
31 | ||
32 | if mode&InputModeVar != 0 { | |
33 | log.Printf("[TRACE] Context.Input: Prompting for variables") | |
34 | ||
35 | // Walk the variables first for the root module. We walk them in | |
36 | // alphabetical order for UX reasons. | |
37 | configs := c.config.Module.Variables | |
38 | names := make([]string, 0, len(configs)) | |
39 | for name := range configs { | |
40 | names = append(names, name) | |
41 | } | |
42 | sort.Strings(names) | |
43 | Variables: | |
44 | for _, n := range names { | |
45 | v := configs[n] | |
46 | ||
47 | // If we only care about unset variables, then we should set any | |
48 | // variable that is already set. | |
49 | if mode&InputModeVarUnset != 0 { | |
50 | if _, isSet := c.variables[n]; isSet { | |
51 | continue | |
52 | } | |
53 | } | |
54 | ||
55 | // this should only happen during tests | |
56 | if c.uiInput == nil { | |
57 | log.Println("[WARN] Context.uiInput is nil during input walk") | |
58 | continue | |
59 | } | |
60 | ||
61 | // Ask the user for a value for this variable | |
62 | var rawValue string | |
63 | retry := 0 | |
64 | for { | |
65 | var err error | |
66 | rawValue, err = c.uiInput.Input(ctx, &InputOpts{ | |
67 | Id: fmt.Sprintf("var.%s", n), | |
68 | Query: fmt.Sprintf("var.%s", n), | |
69 | Description: v.Description, | |
70 | }) | |
71 | if err != nil { | |
72 | diags = diags.Append(tfdiags.Sourceless( | |
73 | tfdiags.Error, | |
74 | "Failed to request interactive input", | |
75 | fmt.Sprintf("Terraform attempted to request a value for var.%s interactively, but encountered an error: %s.", n, err), | |
76 | )) | |
77 | return diags | |
78 | } | |
79 | ||
80 | if rawValue == "" && v.Default == cty.NilVal { | |
81 | // Redo if it is required, but abort if we keep getting | |
82 | // blank entries | |
83 | if retry > 2 { | |
84 | diags = diags.Append(tfdiags.Sourceless( | |
85 | tfdiags.Error, | |
86 | "Required variable not assigned", | |
87 | fmt.Sprintf("The variable %q is required, so Terraform cannot proceed without a defined value for it.", n), | |
88 | )) | |
89 | continue Variables | |
90 | } | |
91 | retry++ | |
92 | continue | |
93 | } | |
94 | ||
95 | break | |
96 | } | |
97 | ||
98 | val, valDiags := v.ParsingMode.Parse(n, rawValue) | |
99 | diags = diags.Append(valDiags) | |
100 | if diags.HasErrors() { | |
101 | continue | |
102 | } | |
103 | ||
104 | c.variables[n] = &InputValue{ | |
105 | Value: val, | |
106 | SourceType: ValueFromInput, | |
107 | } | |
108 | } | |
109 | } | |
110 | ||
111 | if mode&InputModeProvider != 0 { | |
112 | log.Printf("[TRACE] Context.Input: Prompting for provider arguments") | |
113 | ||
114 | // We prompt for input only for provider configurations defined in | |
115 | // the root module. At the time of writing that is an arbitrary | |
116 | // restriction, but we have future plans to support "count" and | |
117 | // "for_each" on modules that will then prevent us from supporting | |
118 | // input for child module configurations anyway (since we'd need to | |
119 | // dynamic-expand first), and provider configurations in child modules | |
120 | // are not recommended since v0.11 anyway, so this restriction allows | |
121 | // us to keep this relatively simple without significant hardship. | |
122 | ||
123 | pcs := make(map[string]*configs.Provider) | |
124 | pas := make(map[string]addrs.ProviderConfig) | |
125 | for _, pc := range c.config.Module.ProviderConfigs { | |
126 | addr := pc.Addr() | |
127 | pcs[addr.String()] = pc | |
128 | pas[addr.String()] = addr | |
129 | log.Printf("[TRACE] Context.Input: Provider %s declared at %s", addr, pc.DeclRange) | |
130 | } | |
131 | // We also need to detect _implied_ provider configs from resources. | |
132 | // These won't have *configs.Provider objects, but they will still | |
133 | // exist in the map and we'll just treat them as empty below. | |
134 | for _, rc := range c.config.Module.ManagedResources { | |
135 | pa := rc.ProviderConfigAddr() | |
136 | if pa.Alias != "" { | |
137 | continue // alias configurations cannot be implied | |
138 | } | |
139 | if _, exists := pcs[pa.String()]; !exists { | |
140 | pcs[pa.String()] = nil | |
141 | pas[pa.String()] = pa | |
142 | log.Printf("[TRACE] Context.Input: Provider %s implied by resource block at %s", pa, rc.DeclRange) | |
143 | } | |
144 | } | |
145 | for _, rc := range c.config.Module.DataResources { | |
146 | pa := rc.ProviderConfigAddr() | |
147 | if pa.Alias != "" { | |
148 | continue // alias configurations cannot be implied | |
149 | } | |
150 | if _, exists := pcs[pa.String()]; !exists { | |
151 | pcs[pa.String()] = nil | |
152 | pas[pa.String()] = pa | |
153 | log.Printf("[TRACE] Context.Input: Provider %s implied by data block at %s", pa, rc.DeclRange) | |
154 | } | |
155 | } | |
156 | ||
157 | for pk, pa := range pas { | |
158 | pc := pcs[pk] // will be nil if this is an implied config | |
159 | ||
160 | // Wrap the input into a namespace | |
161 | input := &PrefixUIInput{ | |
162 | IdPrefix: pk, | |
163 | QueryPrefix: pk + ".", | |
164 | UIInput: c.uiInput, | |
165 | } | |
166 | ||
167 | schema := c.schemas.ProviderConfig(pa.Type) | |
168 | if schema == nil { | |
169 | // Could either be an incorrect config or just an incomplete | |
170 | // mock in tests. We'll let a later pass decide, and just | |
171 | // ignore this for the purposes of gathering input. | |
172 | log.Printf("[TRACE] Context.Input: No schema available for provider type %q", pa.Type) | |
173 | continue | |
174 | } | |
175 | ||
176 | // For our purposes here we just want to detect if attrbutes are | |
177 | // set in config at all, so rather than doing a full decode | |
178 | // (which would require us to prepare an evalcontext, etc) we'll | |
179 | // use the low-level HCL API to process only the top-level | |
180 | // structure. | |
181 | var attrExprs hcl.Attributes // nil if there is no config | |
182 | if pc != nil && pc.Config != nil { | |
183 | lowLevelSchema := schemaForInputSniffing(hcldec.ImpliedSchema(schema.DecoderSpec())) | |
184 | content, _, diags := pc.Config.PartialContent(lowLevelSchema) | |
185 | if diags.HasErrors() { | |
186 | log.Printf("[TRACE] Context.Input: %s has decode error, so ignoring: %s", pa, diags.Error()) | |
187 | continue | |
188 | } | |
189 | attrExprs = content.Attributes | |
190 | } | |
191 | ||
192 | keys := make([]string, 0, len(schema.Attributes)) | |
193 | for key := range schema.Attributes { | |
194 | keys = append(keys, key) | |
195 | } | |
196 | sort.Strings(keys) | |
197 | ||
198 | vals := map[string]cty.Value{} | |
199 | for _, key := range keys { | |
200 | attrS := schema.Attributes[key] | |
201 | if attrS.Optional { | |
202 | continue | |
203 | } | |
204 | if attrExprs != nil { | |
205 | if _, exists := attrExprs[key]; exists { | |
206 | continue | |
207 | } | |
208 | } | |
209 | if !attrS.Type.Equals(cty.String) { | |
210 | continue | |
211 | } | |
212 | ||
213 | log.Printf("[TRACE] Context.Input: Prompting for %s argument %s", pa, key) | |
214 | rawVal, err := input.Input(ctx, &InputOpts{ | |
215 | Id: key, | |
216 | Query: key, | |
217 | Description: attrS.Description, | |
218 | }) | |
219 | if err != nil { | |
220 | log.Printf("[TRACE] Context.Input: Failed to prompt for %s argument %s: %s", pa, key, err) | |
221 | continue | |
222 | } | |
223 | ||
224 | vals[key] = cty.StringVal(rawVal) | |
225 | } | |
226 | ||
227 | c.providerInputConfig[pk] = vals | |
228 | log.Printf("[TRACE] Context.Input: Input for %s: %#v", pk, vals) | |
229 | } | |
230 | } | |
231 | ||
232 | return diags | |
233 | } | |
234 | ||
235 | // schemaForInputSniffing returns a transformed version of a given schema | |
236 | // that marks all attributes as optional, which the Context.Input method can | |
237 | // use to detect whether a required argument is set without missing arguments | |
238 | // themselves generating errors. | |
239 | func schemaForInputSniffing(schema *hcl.BodySchema) *hcl.BodySchema { | |
240 | ret := &hcl.BodySchema{ | |
241 | Attributes: make([]hcl.AttributeSchema, len(schema.Attributes)), | |
242 | Blocks: schema.Blocks, | |
243 | } | |
244 | ||
245 | for i, attrS := range schema.Attributes { | |
246 | ret.Attributes[i] = attrS | |
247 | ret.Attributes[i].Required = false | |
248 | } | |
249 | ||
250 | return ret | |
251 | } |