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