github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/provider_config.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package addrs 5 6 import ( 7 "fmt" 8 "strings" 9 10 "github.com/terramate-io/tf/tfdiags" 11 "github.com/zclconf/go-cty/cty" 12 13 "github.com/hashicorp/hcl/v2" 14 "github.com/hashicorp/hcl/v2/hclsyntax" 15 ) 16 17 // ProviderConfig is an interface type whose dynamic type can be either 18 // LocalProviderConfig or AbsProviderConfig, in order to represent situations 19 // where a value might either be module-local or absolute but the decision 20 // cannot be made until runtime. 21 // 22 // Where possible, use either LocalProviderConfig or AbsProviderConfig directly 23 // instead, to make intent more clear. ProviderConfig can be used only in 24 // situations where the recipient of the value has some out-of-band way to 25 // determine a "current module" to use if the value turns out to be 26 // a LocalProviderConfig. 27 // 28 // Recipients of non-nil ProviderConfig values that actually need 29 // AbsProviderConfig values should call ResolveAbsProviderAddr on the 30 // *configs.Config value representing the root module configuration, which 31 // handles the translation from local to fully-qualified using mapping tables 32 // defined in the configuration. 33 // 34 // Recipients of a ProviderConfig value can assume it can contain only a 35 // LocalProviderConfig value, an AbsProviderConfigValue, or nil to represent 36 // the absense of a provider config in situations where that is meaningful. 37 type ProviderConfig interface { 38 providerConfig() 39 } 40 41 // LocalProviderConfig is the address of a provider configuration from the 42 // perspective of references in a particular module. 43 // 44 // Finding the corresponding AbsProviderConfig will require looking up the 45 // LocalName in the providers table in the module's configuration; there is 46 // no syntax-only translation between these types. 47 type LocalProviderConfig struct { 48 LocalName string 49 50 // If not empty, Alias identifies which non-default (aliased) provider 51 // configuration this address refers to. 52 Alias string 53 } 54 55 var _ ProviderConfig = LocalProviderConfig{} 56 57 // NewDefaultLocalProviderConfig returns the address of the default (un-aliased) 58 // configuration for the provider with the given local type name. 59 func NewDefaultLocalProviderConfig(LocalNameName string) LocalProviderConfig { 60 return LocalProviderConfig{ 61 LocalName: LocalNameName, 62 } 63 } 64 65 // providerConfig Implements addrs.ProviderConfig. 66 func (pc LocalProviderConfig) providerConfig() {} 67 68 func (pc LocalProviderConfig) String() string { 69 if pc.LocalName == "" { 70 // Should never happen; always indicates a bug 71 return "provider.<invalid>" 72 } 73 74 if pc.Alias != "" { 75 return fmt.Sprintf("provider.%s.%s", pc.LocalName, pc.Alias) 76 } 77 78 return "provider." + pc.LocalName 79 } 80 81 // StringCompact is an alternative to String that returns the form that can 82 // be parsed by ParseProviderConfigCompact, without the "provider." prefix. 83 func (pc LocalProviderConfig) StringCompact() string { 84 if pc.Alias != "" { 85 return fmt.Sprintf("%s.%s", pc.LocalName, pc.Alias) 86 } 87 return pc.LocalName 88 } 89 90 // AbsProviderConfig is the absolute address of a provider configuration 91 // within a particular module instance. 92 type AbsProviderConfig struct { 93 Module Module 94 Provider Provider 95 Alias string 96 } 97 98 var _ ProviderConfig = AbsProviderConfig{} 99 100 // ParseAbsProviderConfig parses the given traversal as an absolute provider 101 // configuration address. The following are examples of traversals that can be 102 // successfully parsed as absolute provider configuration addresses: 103 // 104 // - provider["registry.terraform.io/hashicorp/aws"] 105 // - provider["registry.terraform.io/hashicorp/aws"].foo 106 // - module.bar.provider["registry.terraform.io/hashicorp/aws"] 107 // - module.bar.module.baz.provider["registry.terraform.io/hashicorp/aws"].foo 108 // 109 // This type of address is used, for example, to record the relationships 110 // between resources and provider configurations in the state structure. 111 // This type of address is typically not used prominently in the UI, except in 112 // error messages that refer to provider configurations. 113 func ParseAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { 114 modInst, remain, diags := parseModuleInstancePrefix(traversal) 115 var ret AbsProviderConfig 116 117 // Providers cannot resolve within module instances, so verify that there 118 // are no instance keys in the module path before converting to a Module. 119 for _, step := range modInst { 120 if step.InstanceKey != NoKey { 121 diags = diags.Append(&hcl.Diagnostic{ 122 Severity: hcl.DiagError, 123 Summary: "Invalid provider configuration address", 124 Detail: "Provider address cannot contain module indexes", 125 Subject: remain.SourceRange().Ptr(), 126 }) 127 return ret, diags 128 } 129 } 130 ret.Module = modInst.Module() 131 132 if len(remain) < 2 || remain.RootName() != "provider" { 133 diags = diags.Append(&hcl.Diagnostic{ 134 Severity: hcl.DiagError, 135 Summary: "Invalid provider configuration address", 136 Detail: "Provider address must begin with \"provider.\", followed by a provider type name.", 137 Subject: remain.SourceRange().Ptr(), 138 }) 139 return ret, diags 140 } 141 if len(remain) > 3 { 142 diags = diags.Append(&hcl.Diagnostic{ 143 Severity: hcl.DiagError, 144 Summary: "Invalid provider configuration address", 145 Detail: "Extraneous operators after provider configuration alias.", 146 Subject: hcl.Traversal(remain[3:]).SourceRange().Ptr(), 147 }) 148 return ret, diags 149 } 150 151 if tt, ok := remain[1].(hcl.TraverseIndex); ok { 152 if !tt.Key.Type().Equals(cty.String) { 153 diags = diags.Append(&hcl.Diagnostic{ 154 Severity: hcl.DiagError, 155 Summary: "Invalid provider configuration address", 156 Detail: "The prefix \"provider.\" must be followed by a provider type name.", 157 Subject: remain[1].SourceRange().Ptr(), 158 }) 159 return ret, diags 160 } 161 p, sourceDiags := ParseProviderSourceString(tt.Key.AsString()) 162 ret.Provider = p 163 if sourceDiags.HasErrors() { 164 diags = diags.Append(sourceDiags) 165 return ret, diags 166 } 167 } else { 168 diags = diags.Append(&hcl.Diagnostic{ 169 Severity: hcl.DiagError, 170 Summary: "Invalid provider configuration address", 171 Detail: "The prefix \"provider.\" must be followed by a provider type name.", 172 Subject: remain[1].SourceRange().Ptr(), 173 }) 174 return ret, diags 175 } 176 177 if len(remain) == 3 { 178 if tt, ok := remain[2].(hcl.TraverseAttr); ok { 179 ret.Alias = tt.Name 180 } else { 181 diags = diags.Append(&hcl.Diagnostic{ 182 Severity: hcl.DiagError, 183 Summary: "Invalid provider configuration address", 184 Detail: "Provider type name must be followed by a configuration alias name.", 185 Subject: remain[2].SourceRange().Ptr(), 186 }) 187 return ret, diags 188 } 189 } 190 191 return ret, diags 192 } 193 194 // ParseAbsProviderConfigStr is a helper wrapper around ParseAbsProviderConfig 195 // that takes a string and parses it with the HCL native syntax traversal parser 196 // before interpreting it. 197 // 198 // This should be used only in specialized situations since it will cause the 199 // created references to not have any meaningful source location information. 200 // If a reference string is coming from a source that should be identified in 201 // error messages then the caller should instead parse it directly using a 202 // suitable function from the HCL API and pass the traversal itself to 203 // ParseAbsProviderConfig. 204 // 205 // Error diagnostics are returned if either the parsing fails or the analysis 206 // of the traversal fails. There is no way for the caller to distinguish the 207 // two kinds of diagnostics programmatically. If error diagnostics are returned 208 // the returned address is invalid. 209 func ParseAbsProviderConfigStr(str string) (AbsProviderConfig, tfdiags.Diagnostics) { 210 var diags tfdiags.Diagnostics 211 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 212 diags = diags.Append(parseDiags) 213 if parseDiags.HasErrors() { 214 return AbsProviderConfig{}, diags 215 } 216 addr, addrDiags := ParseAbsProviderConfig(traversal) 217 diags = diags.Append(addrDiags) 218 return addr, diags 219 } 220 221 func ParseLegacyAbsProviderConfigStr(str string) (AbsProviderConfig, tfdiags.Diagnostics) { 222 var diags tfdiags.Diagnostics 223 224 traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(str), "", hcl.Pos{Line: 1, Column: 1}) 225 diags = diags.Append(parseDiags) 226 if parseDiags.HasErrors() { 227 return AbsProviderConfig{}, diags 228 } 229 230 addr, addrDiags := ParseLegacyAbsProviderConfig(traversal) 231 diags = diags.Append(addrDiags) 232 return addr, diags 233 } 234 235 // ParseLegacyAbsProviderConfig parses the given traversal as an absolute 236 // provider address in the legacy form used by Terraform v0.12 and earlier. 237 // The following are examples of traversals that can be successfully parsed as 238 // legacy absolute provider configuration addresses: 239 // 240 // - provider.aws 241 // - provider.aws.foo 242 // - module.bar.provider.aws 243 // - module.bar.module.baz.provider.aws.foo 244 // 245 // We can encounter this kind of address in a historical state snapshot that 246 // hasn't yet been upgraded by refreshing or applying a plan with 247 // Terraform v0.13. Later versions of Terraform reject state snapshots using 248 // this format, and so users must follow the Terraform v0.13 upgrade guide 249 // in that case. 250 // 251 // We will not use this address form for any new file formats. 252 func ParseLegacyAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { 253 modInst, remain, diags := parseModuleInstancePrefix(traversal) 254 var ret AbsProviderConfig 255 256 // Providers cannot resolve within module instances, so verify that there 257 // are no instance keys in the module path before converting to a Module. 258 for _, step := range modInst { 259 if step.InstanceKey != NoKey { 260 diags = diags.Append(&hcl.Diagnostic{ 261 Severity: hcl.DiagError, 262 Summary: "Invalid provider configuration address", 263 Detail: "Provider address cannot contain module indexes", 264 Subject: remain.SourceRange().Ptr(), 265 }) 266 return ret, diags 267 } 268 } 269 ret.Module = modInst.Module() 270 271 if len(remain) < 2 || remain.RootName() != "provider" { 272 diags = diags.Append(&hcl.Diagnostic{ 273 Severity: hcl.DiagError, 274 Summary: "Invalid provider configuration address", 275 Detail: "Provider address must begin with \"provider.\", followed by a provider type name.", 276 Subject: remain.SourceRange().Ptr(), 277 }) 278 return ret, diags 279 } 280 if len(remain) > 3 { 281 diags = diags.Append(&hcl.Diagnostic{ 282 Severity: hcl.DiagError, 283 Summary: "Invalid provider configuration address", 284 Detail: "Extraneous operators after provider configuration alias.", 285 Subject: hcl.Traversal(remain[3:]).SourceRange().Ptr(), 286 }) 287 return ret, diags 288 } 289 290 // We always assume legacy-style providers in legacy state ... 291 if tt, ok := remain[1].(hcl.TraverseAttr); ok { 292 // ... unless it's the builtin "terraform" provider, a special case. 293 if tt.Name == "terraform" { 294 ret.Provider = NewBuiltInProvider(tt.Name) 295 } else { 296 ret.Provider = NewLegacyProvider(tt.Name) 297 } 298 } else { 299 diags = diags.Append(&hcl.Diagnostic{ 300 Severity: hcl.DiagError, 301 Summary: "Invalid provider configuration address", 302 Detail: "The prefix \"provider.\" must be followed by a provider type name.", 303 Subject: remain[1].SourceRange().Ptr(), 304 }) 305 return ret, diags 306 } 307 308 if len(remain) == 3 { 309 if tt, ok := remain[2].(hcl.TraverseAttr); ok { 310 ret.Alias = tt.Name 311 } else { 312 diags = diags.Append(&hcl.Diagnostic{ 313 Severity: hcl.DiagError, 314 Summary: "Invalid provider configuration address", 315 Detail: "Provider type name must be followed by a configuration alias name.", 316 Subject: remain[2].SourceRange().Ptr(), 317 }) 318 return ret, diags 319 } 320 } 321 322 return ret, diags 323 } 324 325 // ProviderConfigDefault returns the address of the default provider config of 326 // the given type inside the recieving module instance. 327 func (m ModuleInstance) ProviderConfigDefault(provider Provider) AbsProviderConfig { 328 return AbsProviderConfig{ 329 Module: m.Module(), 330 Provider: provider, 331 } 332 } 333 334 // ProviderConfigAliased returns the address of an aliased provider config of 335 // the given type and alias inside the recieving module instance. 336 func (m ModuleInstance) ProviderConfigAliased(provider Provider, alias string) AbsProviderConfig { 337 return AbsProviderConfig{ 338 Module: m.Module(), 339 Provider: provider, 340 Alias: alias, 341 } 342 } 343 344 // providerConfig Implements addrs.ProviderConfig. 345 func (pc AbsProviderConfig) providerConfig() {} 346 347 // Inherited returns an address that the receiving configuration address might 348 // inherit from in a parent module. The second bool return value indicates if 349 // such inheritance is possible, and thus whether the returned address is valid. 350 // 351 // Inheritance is possible only for default (un-aliased) providers in modules 352 // other than the root module. Even if a valid address is returned, inheritence 353 // may not be performed for other reasons, such as if the calling module 354 // provided explicit provider configurations within the call for this module. 355 // The ProviderTransformer graph transform in the main terraform module has the 356 // authoritative logic for provider inheritance, and this method is here mainly 357 // just for its benefit. 358 func (pc AbsProviderConfig) Inherited() (AbsProviderConfig, bool) { 359 // Can't inherit if we're already in the root. 360 if len(pc.Module) == 0 { 361 return AbsProviderConfig{}, false 362 } 363 364 // Can't inherit if we have an alias. 365 if pc.Alias != "" { 366 return AbsProviderConfig{}, false 367 } 368 369 // Otherwise, we might inherit from a configuration with the same 370 // provider type in the parent module instance. 371 parentMod := pc.Module.Parent() 372 return AbsProviderConfig{ 373 Module: parentMod, 374 Provider: pc.Provider, 375 }, true 376 377 } 378 379 // LegacyString() returns a legacy-style AbsProviderConfig string and should only be used for legacy state shimming. 380 func (pc AbsProviderConfig) LegacyString() string { 381 if pc.Alias != "" { 382 if len(pc.Module) == 0 { 383 return fmt.Sprintf("%s.%s.%s", "provider", pc.Provider.LegacyString(), pc.Alias) 384 } else { 385 return fmt.Sprintf("%s.%s.%s.%s", pc.Module.String(), "provider", pc.Provider.LegacyString(), pc.Alias) 386 } 387 } 388 if len(pc.Module) == 0 { 389 return fmt.Sprintf("%s.%s", "provider", pc.Provider.LegacyString()) 390 } 391 return fmt.Sprintf("%s.%s.%s", pc.Module.String(), "provider", pc.Provider.LegacyString()) 392 } 393 394 // String() returns a string representation of an AbsProviderConfig in a format like the following examples: 395 // 396 // - provider["example.com/namespace/name"] 397 // - provider["example.com/namespace/name"].alias 398 // - module.module-name.provider["example.com/namespace/name"] 399 // - module.module-name.provider["example.com/namespace/name"].alias 400 func (pc AbsProviderConfig) String() string { 401 var parts []string 402 if len(pc.Module) > 0 { 403 parts = append(parts, pc.Module.String()) 404 } 405 406 parts = append(parts, fmt.Sprintf("provider[%q]", pc.Provider)) 407 408 if pc.Alias != "" { 409 parts = append(parts, pc.Alias) 410 } 411 412 return strings.Join(parts, ".") 413 }