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