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