1package command
2
3import (
4	"context"
5	"fmt"
6	"os"
7	"path/filepath"
8	"sort"
9
10	"github.com/hashicorp/hcl/v2"
11	"github.com/hashicorp/hcl/v2/hclsyntax"
12	"github.com/hashicorp/terraform-config-inspect/tfconfig"
13	"github.com/hashicorp/terraform/internal/configs"
14	"github.com/hashicorp/terraform/internal/configs/configload"
15	"github.com/hashicorp/terraform/internal/configs/configschema"
16	"github.com/hashicorp/terraform/internal/earlyconfig"
17	"github.com/hashicorp/terraform/internal/initwd"
18	"github.com/hashicorp/terraform/internal/registry"
19	"github.com/hashicorp/terraform/internal/terraform"
20	"github.com/hashicorp/terraform/internal/tfdiags"
21	"github.com/zclconf/go-cty/cty"
22	"github.com/zclconf/go-cty/cty/convert"
23)
24
25// normalizePath normalizes a given path so that it is, if possible, relative
26// to the current working directory. This is primarily used to prepare
27// paths used to load configuration, because we want to prefer recording
28// relative paths in source code references within the configuration.
29func (m *Meta) normalizePath(path string) string {
30	var err error
31
32	// First we will make it absolute so that we have a consistent place
33	// to start.
34	path, err = filepath.Abs(path)
35	if err != nil {
36		// We'll just accept what we were given, then.
37		return path
38	}
39
40	cwd, err := os.Getwd()
41	if err != nil || !filepath.IsAbs(cwd) {
42		return path
43	}
44
45	ret, err := filepath.Rel(cwd, path)
46	if err != nil {
47		return path
48	}
49
50	return ret
51}
52
53// loadConfig reads a configuration from the given directory, which should
54// contain a root module and have already have any required descendent modules
55// installed.
56func (m *Meta) loadConfig(rootDir string) (*configs.Config, tfdiags.Diagnostics) {
57	var diags tfdiags.Diagnostics
58	rootDir = m.normalizePath(rootDir)
59
60	loader, err := m.initConfigLoader()
61	if err != nil {
62		diags = diags.Append(err)
63		return nil, diags
64	}
65
66	config, hclDiags := loader.LoadConfig(rootDir)
67	diags = diags.Append(hclDiags)
68	return config, diags
69}
70
71// loadSingleModule reads configuration from the given directory and returns
72// a description of that module only, without attempting to assemble a module
73// tree for referenced child modules.
74//
75// Most callers should use loadConfig. This method exists to support early
76// initialization use-cases where the root module must be inspected in order
77// to determine what else needs to be installed before the full configuration
78// can be used.
79func (m *Meta) loadSingleModule(dir string) (*configs.Module, tfdiags.Diagnostics) {
80	var diags tfdiags.Diagnostics
81	dir = m.normalizePath(dir)
82
83	loader, err := m.initConfigLoader()
84	if err != nil {
85		diags = diags.Append(err)
86		return nil, diags
87	}
88
89	module, hclDiags := loader.Parser().LoadConfigDir(dir)
90	diags = diags.Append(hclDiags)
91	return module, diags
92}
93
94// loadSingleModuleEarly is a variant of loadSingleModule that uses the special
95// "early config" loader that is more forgiving of unexpected constructs and
96// legacy syntax.
97//
98// Early-loaded config is not registered in the source code cache, so
99// diagnostics produced from it may render without source code snippets. In
100// practice this is not a big concern because the early config loader also
101// cannot generate detailed source locations, so it prefers to produce
102// diagnostics without explicit source location information and instead includes
103// approximate locations in the message text.
104//
105// Most callers should use loadConfig. This method exists to support early
106// initialization use-cases where the root module must be inspected in order
107// to determine what else needs to be installed before the full configuration
108// can be used.
109func (m *Meta) loadSingleModuleEarly(dir string) (*tfconfig.Module, tfdiags.Diagnostics) {
110	var diags tfdiags.Diagnostics
111	dir = m.normalizePath(dir)
112
113	module, moreDiags := earlyconfig.LoadModule(dir)
114	diags = diags.Append(moreDiags)
115
116	return module, diags
117}
118
119// dirIsConfigPath checks if the given path is a directory that contains at
120// least one Terraform configuration file (.tf or .tf.json), returning true
121// if so.
122//
123// In the unlikely event that the underlying config loader cannot be initalized,
124// this function optimistically returns true, assuming that the caller will
125// then do some other operation that requires the config loader and get an
126// error at that point.
127func (m *Meta) dirIsConfigPath(dir string) bool {
128	loader, err := m.initConfigLoader()
129	if err != nil {
130		return true
131	}
132
133	return loader.IsConfigDir(dir)
134}
135
136// loadBackendConfig reads configuration from the given directory and returns
137// the backend configuration defined by that module, if any. Nil is returned
138// if the specified module does not have an explicit backend configuration.
139//
140// This is a convenience method for command code that will delegate to the
141// configured backend to do most of its work, since in that case it is the
142// backend that will do the full configuration load.
143//
144// Although this method returns only the backend configuration, at present it
145// actually loads and validates the entire configuration first. Therefore errors
146// returned may be about other aspects of the configuration. This behavior may
147// change in future, so callers must not rely on it. (That is, they must expect
148// that a call to loadSingleModule or loadConfig could fail on the same
149// directory even if loadBackendConfig succeeded.)
150func (m *Meta) loadBackendConfig(rootDir string) (*configs.Backend, tfdiags.Diagnostics) {
151	mod, diags := m.loadSingleModule(rootDir)
152
153	// Only return error diagnostics at this point. Any warnings will be caught
154	// again later and duplicated in the output.
155	if diags.HasErrors() {
156		return nil, diags
157	}
158	return mod.Backend, nil
159}
160
161// loadHCLFile reads an arbitrary HCL file and returns the unprocessed body
162// representing its toplevel. Most callers should use one of the more
163// specialized "load..." methods to get a higher-level representation.
164func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) {
165	var diags tfdiags.Diagnostics
166	filename = m.normalizePath(filename)
167
168	loader, err := m.initConfigLoader()
169	if err != nil {
170		diags = diags.Append(err)
171		return nil, diags
172	}
173
174	body, hclDiags := loader.Parser().LoadHCLFile(filename)
175	diags = diags.Append(hclDiags)
176	return body, diags
177}
178
179// installModules reads a root module from the given directory and attempts
180// recursively install all of its descendent modules.
181//
182// The given hooks object will be notified of installation progress, which
183// can then be relayed to the end-user. The moduleUiInstallHooks type in
184// this package has a reasonable implementation for displaying notifications
185// via a provided cli.Ui.
186func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics {
187	var diags tfdiags.Diagnostics
188	rootDir = m.normalizePath(rootDir)
189
190	err := os.MkdirAll(m.modulesDir(), os.ModePerm)
191	if err != nil {
192		diags = diags.Append(fmt.Errorf("failed to create local modules directory: %s", err))
193		return diags
194	}
195
196	inst := m.moduleInstaller()
197	_, moreDiags := inst.InstallModules(rootDir, upgrade, hooks)
198	diags = diags.Append(moreDiags)
199	return diags
200}
201
202// initDirFromModule initializes the given directory (which should be
203// pre-verified as empty by the caller) by copying the source code from the
204// given module address.
205//
206// Internally this runs similar steps to installModules.
207// The given hooks object will be notified of installation progress, which
208// can then be relayed to the end-user. The moduleUiInstallHooks type in
209// this package has a reasonable implementation for displaying notifications
210// via a provided cli.Ui.
211func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics {
212	var diags tfdiags.Diagnostics
213	targetDir = m.normalizePath(targetDir)
214	moreDiags := initwd.DirFromModule(targetDir, m.modulesDir(), addr, m.registryClient(), hooks)
215	diags = diags.Append(moreDiags)
216	return diags
217}
218
219// inputForSchema uses interactive prompts to try to populate any
220// not-yet-populated required attributes in the given object value to
221// comply with the given schema.
222//
223// An error will be returned if input is disabled for this meta or if
224// values cannot be obtained for some other operational reason. Errors are
225// not returned for invalid input since the input loop itself will report
226// that interactively.
227//
228// It is not guaranteed that the result will be valid, since certain attribute
229// types and nested blocks are not supported for input.
230//
231// The given value must conform to the given schema. If not, this method will
232// panic.
233func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.Value, error) {
234	if given.IsNull() || !given.IsKnown() {
235		// This is not reasonable input, but we'll tolerate it anyway and
236		// just pass it through for the caller to handle downstream.
237		return given, nil
238	}
239
240	retVals := given.AsValueMap()
241	names := make([]string, 0, len(schema.Attributes))
242	for name, attrS := range schema.Attributes {
243		if attrS.Required && retVals[name].IsNull() && attrS.Type.IsPrimitiveType() {
244			names = append(names, name)
245		}
246	}
247	sort.Strings(names)
248
249	input := m.UIInput()
250	for _, name := range names {
251		attrS := schema.Attributes[name]
252
253		for {
254			strVal, err := input.Input(context.Background(), &terraform.InputOpts{
255				Id:          name,
256				Query:       name,
257				Description: attrS.Description,
258			})
259			if err != nil {
260				return cty.UnknownVal(schema.ImpliedType()), fmt.Errorf("%s: %s", name, err)
261			}
262
263			val := cty.StringVal(strVal)
264			val, err = convert.Convert(val, attrS.Type)
265			if err != nil {
266				m.showDiagnostics(fmt.Errorf("Invalid value: %s", err))
267				continue
268			}
269
270			retVals[name] = val
271			break
272		}
273	}
274
275	return cty.ObjectVal(retVals), nil
276}
277
278// configSources returns the source cache from the receiver's config loader,
279// which the caller must not modify.
280//
281// If a config loader has not yet been instantiated then no files could have
282// been loaded already, so this method returns a nil map in that case.
283func (m *Meta) configSources() map[string][]byte {
284	if m.configLoader == nil {
285		return nil
286	}
287
288	return m.configLoader.Sources()
289}
290
291func (m *Meta) modulesDir() string {
292	return filepath.Join(m.DataDir(), "modules")
293}
294
295// registerSynthConfigSource allows commands to add synthetic additional source
296// buffers to the config loader's cache of sources (as returned by
297// configSources), which is useful when a command is directly parsing something
298// from the command line that may produce diagnostics, so that diagnostic
299// snippets can still be produced.
300//
301// If this is called before a configLoader has been initialized then it will
302// try to initialize the loader but ignore any initialization failure, turning
303// the call into a no-op. (We presume that a caller will later call a different
304// function that also initializes the config loader as a side effect, at which
305// point those errors can be returned.)
306func (m *Meta) registerSynthConfigSource(filename string, src []byte) {
307	loader, err := m.initConfigLoader()
308	if err != nil || loader == nil {
309		return // treated as no-op, since this is best-effort
310	}
311	loader.Parser().ForceFileSource(filename, src)
312}
313
314// initConfigLoader initializes the shared configuration loader if it isn't
315// already initialized.
316//
317// If the loader cannot be created for some reason then an error is returned
318// and no loader is created. Subsequent calls will presumably see the same
319// error. Loader initialization errors will tend to prevent any further use
320// of most Terraform features, so callers should report any error and safely
321// terminate.
322func (m *Meta) initConfigLoader() (*configload.Loader, error) {
323	if m.configLoader == nil {
324		loader, err := configload.NewLoader(&configload.Config{
325			ModulesDir: m.modulesDir(),
326			Services:   m.Services,
327		})
328		if err != nil {
329			return nil, err
330		}
331		m.configLoader = loader
332		if m.View != nil {
333			m.View.SetConfigSources(loader.Sources)
334		}
335	}
336	return m.configLoader, nil
337}
338
339// moduleInstaller instantiates and returns a module installer for use by
340// "terraform init" (directly or indirectly).
341func (m *Meta) moduleInstaller() *initwd.ModuleInstaller {
342	reg := m.registryClient()
343	return initwd.NewModuleInstaller(m.modulesDir(), reg)
344}
345
346// registryClient instantiates and returns a new Terraform Registry client.
347func (m *Meta) registryClient() *registry.Client {
348	return registry.NewClient(m.Services, nil)
349}
350
351// configValueFromCLI parses a configuration value that was provided in a
352// context in the CLI where only strings can be provided, such as on the
353// command line or in an environment variable, and returns the resulting
354// value.
355func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) {
356	var diags tfdiags.Diagnostics
357
358	switch {
359	case wantType.IsPrimitiveType():
360		// Primitive types are handled as conversions from string.
361		val := cty.StringVal(rawValue)
362		var err error
363		val, err = convert.Convert(val, wantType)
364		if err != nil {
365			diags = diags.Append(tfdiags.Sourceless(
366				tfdiags.Error,
367				"Invalid backend configuration value",
368				fmt.Sprintf("Invalid backend configuration argument %s: %s", synthFilename, err),
369			))
370			val = cty.DynamicVal // just so we return something valid-ish
371		}
372		return val, diags
373	default:
374		// Non-primitives are parsed as HCL expressions
375		src := []byte(rawValue)
376		expr, hclDiags := hclsyntax.ParseExpression(src, synthFilename, hcl.Pos{Line: 1, Column: 1})
377		diags = diags.Append(hclDiags)
378		if hclDiags.HasErrors() {
379			return cty.DynamicVal, diags
380		}
381		val, hclDiags := expr.Value(nil)
382		diags = diags.Append(hclDiags)
383		if hclDiags.HasErrors() {
384			val = cty.DynamicVal
385		}
386		return val, diags
387	}
388}
389
390// rawFlags is a flag.Value implementation that just appends raw flag
391// names and values to a slice.
392type rawFlags struct {
393	flagName string
394	items    *[]rawFlag
395}
396
397func newRawFlags(flagName string) rawFlags {
398	var items []rawFlag
399	return rawFlags{
400		flagName: flagName,
401		items:    &items,
402	}
403}
404
405func (f rawFlags) Empty() bool {
406	if f.items == nil {
407		return true
408	}
409	return len(*f.items) == 0
410}
411
412func (f rawFlags) AllItems() []rawFlag {
413	if f.items == nil {
414		return nil
415	}
416	return *f.items
417}
418
419func (f rawFlags) Alias(flagName string) rawFlags {
420	return rawFlags{
421		flagName: flagName,
422		items:    f.items,
423	}
424}
425
426func (f rawFlags) String() string {
427	return ""
428}
429
430func (f rawFlags) Set(str string) error {
431	*f.items = append(*f.items, rawFlag{
432		Name:  f.flagName,
433		Value: str,
434	})
435	return nil
436}
437
438type rawFlag struct {
439	Name  string
440	Value string
441}
442
443func (f rawFlag) String() string {
444	return fmt.Sprintf("%s=%q", f.Name, f.Value)
445}
446