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