github.com/jaredpalmer/terraform@v1.1.0-alpha20210908.0.20210911170307-88705c943a03/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 return mod.Backend, nil 140 } 141 142 // loadHCLFile reads an arbitrary HCL file and returns the unprocessed body 143 // representing its toplevel. Most callers should use one of the more 144 // specialized "load..." methods to get a higher-level representation. 145 func (m *Meta) loadHCLFile(filename string) (hcl.Body, tfdiags.Diagnostics) { 146 var diags tfdiags.Diagnostics 147 filename = m.normalizePath(filename) 148 149 loader, err := m.initConfigLoader() 150 if err != nil { 151 diags = diags.Append(err) 152 return nil, diags 153 } 154 155 body, hclDiags := loader.Parser().LoadHCLFile(filename) 156 diags = diags.Append(hclDiags) 157 return body, diags 158 } 159 160 // installModules reads a root module from the given directory and attempts 161 // recursively install all of its descendent modules. 162 // 163 // The given hooks object will be notified of installation progress, which 164 // can then be relayed to the end-user. The moduleUiInstallHooks type in 165 // this package has a reasonable implementation for displaying notifications 166 // via a provided cli.Ui. 167 func (m *Meta) installModules(rootDir string, upgrade bool, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics { 168 var diags tfdiags.Diagnostics 169 rootDir = m.normalizePath(rootDir) 170 171 err := os.MkdirAll(m.modulesDir(), os.ModePerm) 172 if err != nil { 173 diags = diags.Append(fmt.Errorf("failed to create local modules directory: %s", err)) 174 return diags 175 } 176 177 inst := m.moduleInstaller() 178 _, moreDiags := inst.InstallModules(rootDir, upgrade, hooks) 179 diags = diags.Append(moreDiags) 180 return diags 181 } 182 183 // initDirFromModule initializes the given directory (which should be 184 // pre-verified as empty by the caller) by copying the source code from the 185 // given module address. 186 // 187 // Internally this runs similar steps to installModules. 188 // The given hooks object will be notified of installation progress, which 189 // can then be relayed to the end-user. The moduleUiInstallHooks type in 190 // this package has a reasonable implementation for displaying notifications 191 // via a provided cli.Ui. 192 func (m *Meta) initDirFromModule(targetDir string, addr string, hooks initwd.ModuleInstallHooks) tfdiags.Diagnostics { 193 var diags tfdiags.Diagnostics 194 targetDir = m.normalizePath(targetDir) 195 moreDiags := initwd.DirFromModule(targetDir, m.modulesDir(), addr, m.registryClient(), hooks) 196 diags = diags.Append(moreDiags) 197 return diags 198 } 199 200 // inputForSchema uses interactive prompts to try to populate any 201 // not-yet-populated required attributes in the given object value to 202 // comply with the given schema. 203 // 204 // An error will be returned if input is disabled for this meta or if 205 // values cannot be obtained for some other operational reason. Errors are 206 // not returned for invalid input since the input loop itself will report 207 // that interactively. 208 // 209 // It is not guaranteed that the result will be valid, since certain attribute 210 // types and nested blocks are not supported for input. 211 // 212 // The given value must conform to the given schema. If not, this method will 213 // panic. 214 func (m *Meta) inputForSchema(given cty.Value, schema *configschema.Block) (cty.Value, error) { 215 if given.IsNull() || !given.IsKnown() { 216 // This is not reasonable input, but we'll tolerate it anyway and 217 // just pass it through for the caller to handle downstream. 218 return given, nil 219 } 220 221 retVals := given.AsValueMap() 222 names := make([]string, 0, len(schema.Attributes)) 223 for name, attrS := range schema.Attributes { 224 if attrS.Required && retVals[name].IsNull() && attrS.Type.IsPrimitiveType() { 225 names = append(names, name) 226 } 227 } 228 sort.Strings(names) 229 230 input := m.UIInput() 231 for _, name := range names { 232 attrS := schema.Attributes[name] 233 234 for { 235 strVal, err := input.Input(context.Background(), &terraform.InputOpts{ 236 Id: name, 237 Query: name, 238 Description: attrS.Description, 239 }) 240 if err != nil { 241 return cty.UnknownVal(schema.ImpliedType()), fmt.Errorf("%s: %s", name, err) 242 } 243 244 val := cty.StringVal(strVal) 245 val, err = convert.Convert(val, attrS.Type) 246 if err != nil { 247 m.showDiagnostics(fmt.Errorf("Invalid value: %s", err)) 248 continue 249 } 250 251 retVals[name] = val 252 break 253 } 254 } 255 256 return cty.ObjectVal(retVals), nil 257 } 258 259 // configSources returns the source cache from the receiver's config loader, 260 // which the caller must not modify. 261 // 262 // If a config loader has not yet been instantiated then no files could have 263 // been loaded already, so this method returns a nil map in that case. 264 func (m *Meta) configSources() map[string][]byte { 265 if m.configLoader == nil { 266 return nil 267 } 268 269 return m.configLoader.Sources() 270 } 271 272 func (m *Meta) modulesDir() string { 273 return filepath.Join(m.DataDir(), "modules") 274 } 275 276 // registerSynthConfigSource allows commands to add synthetic additional source 277 // buffers to the config loader's cache of sources (as returned by 278 // configSources), which is useful when a command is directly parsing something 279 // from the command line that may produce diagnostics, so that diagnostic 280 // snippets can still be produced. 281 // 282 // If this is called before a configLoader has been initialized then it will 283 // try to initialize the loader but ignore any initialization failure, turning 284 // the call into a no-op. (We presume that a caller will later call a different 285 // function that also initializes the config loader as a side effect, at which 286 // point those errors can be returned.) 287 func (m *Meta) registerSynthConfigSource(filename string, src []byte) { 288 loader, err := m.initConfigLoader() 289 if err != nil || loader == nil { 290 return // treated as no-op, since this is best-effort 291 } 292 loader.Parser().ForceFileSource(filename, src) 293 } 294 295 // initConfigLoader initializes the shared configuration loader if it isn't 296 // already initialized. 297 // 298 // If the loader cannot be created for some reason then an error is returned 299 // and no loader is created. Subsequent calls will presumably see the same 300 // error. Loader initialization errors will tend to prevent any further use 301 // of most Terraform features, so callers should report any error and safely 302 // terminate. 303 func (m *Meta) initConfigLoader() (*configload.Loader, error) { 304 if m.configLoader == nil { 305 loader, err := configload.NewLoader(&configload.Config{ 306 ModulesDir: m.modulesDir(), 307 Services: m.Services, 308 }) 309 if err != nil { 310 return nil, err 311 } 312 m.configLoader = loader 313 if m.View != nil { 314 m.View.SetConfigSources(loader.Sources) 315 } 316 } 317 return m.configLoader, nil 318 } 319 320 // moduleInstaller instantiates and returns a module installer for use by 321 // "terraform init" (directly or indirectly). 322 func (m *Meta) moduleInstaller() *initwd.ModuleInstaller { 323 reg := m.registryClient() 324 return initwd.NewModuleInstaller(m.modulesDir(), reg) 325 } 326 327 // registryClient instantiates and returns a new Terraform Registry client. 328 func (m *Meta) registryClient() *registry.Client { 329 return registry.NewClient(m.Services, nil) 330 } 331 332 // configValueFromCLI parses a configuration value that was provided in a 333 // context in the CLI where only strings can be provided, such as on the 334 // command line or in an environment variable, and returns the resulting 335 // value. 336 func configValueFromCLI(synthFilename, rawValue string, wantType cty.Type) (cty.Value, tfdiags.Diagnostics) { 337 var diags tfdiags.Diagnostics 338 339 switch { 340 case wantType.IsPrimitiveType(): 341 // Primitive types are handled as conversions from string. 342 val := cty.StringVal(rawValue) 343 var err error 344 val, err = convert.Convert(val, wantType) 345 if err != nil { 346 diags = diags.Append(tfdiags.Sourceless( 347 tfdiags.Error, 348 "Invalid backend configuration value", 349 fmt.Sprintf("Invalid backend configuration argument %s: %s", synthFilename, err), 350 )) 351 val = cty.DynamicVal // just so we return something valid-ish 352 } 353 return val, diags 354 default: 355 // Non-primitives are parsed as HCL expressions 356 src := []byte(rawValue) 357 expr, hclDiags := hclsyntax.ParseExpression(src, synthFilename, hcl.Pos{Line: 1, Column: 1}) 358 diags = diags.Append(hclDiags) 359 if hclDiags.HasErrors() { 360 return cty.DynamicVal, diags 361 } 362 val, hclDiags := expr.Value(nil) 363 diags = diags.Append(hclDiags) 364 if hclDiags.HasErrors() { 365 val = cty.DynamicVal 366 } 367 return val, diags 368 } 369 } 370 371 // rawFlags is a flag.Value implementation that just appends raw flag 372 // names and values to a slice. 373 type rawFlags struct { 374 flagName string 375 items *[]rawFlag 376 } 377 378 func newRawFlags(flagName string) rawFlags { 379 var items []rawFlag 380 return rawFlags{ 381 flagName: flagName, 382 items: &items, 383 } 384 } 385 386 func (f rawFlags) Empty() bool { 387 if f.items == nil { 388 return true 389 } 390 return len(*f.items) == 0 391 } 392 393 func (f rawFlags) AllItems() []rawFlag { 394 if f.items == nil { 395 return nil 396 } 397 return *f.items 398 } 399 400 func (f rawFlags) Alias(flagName string) rawFlags { 401 return rawFlags{ 402 flagName: flagName, 403 items: f.items, 404 } 405 } 406 407 func (f rawFlags) String() string { 408 return "" 409 } 410 411 func (f rawFlags) Set(str string) error { 412 *f.items = append(*f.items, rawFlag{ 413 Name: f.flagName, 414 Value: str, 415 }) 416 return nil 417 } 418 419 type rawFlag struct { 420 Name string 421 Value string 422 } 423 424 func (f rawFlag) String() string { 425 return fmt.Sprintf("%s=%q", f.Name, f.Value) 426 }