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