github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/addrs/provider.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package addrs 5 6 import ( 7 "github.com/hashicorp/hcl/v2" 8 tfaddr "github.com/hashicorp/terraform-registry-address" 9 svchost "github.com/hashicorp/terraform-svchost" 10 "github.com/terramate-io/tf/tfdiags" 11 ) 12 13 // Provider encapsulates a single provider type. In the future this will be 14 // extended to include additional fields including Namespace and SourceHost 15 type Provider = tfaddr.Provider 16 17 // DefaultProviderRegistryHost is the hostname used for provider addresses that do 18 // not have an explicit hostname. 19 const DefaultProviderRegistryHost = tfaddr.DefaultProviderRegistryHost 20 21 // BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider 22 // namespace. Built-in provider addresses must also have their namespace set 23 // to BuiltInProviderNamespace in order to be considered as built-in. 24 const BuiltInProviderHost = tfaddr.BuiltInProviderHost 25 26 // BuiltInProviderNamespace is the provider namespace used for "built-in" 27 // providers. Built-in provider addresses must also have their hostname 28 // set to BuiltInProviderHost in order to be considered as built-in. 29 // 30 // The this namespace is literally named "builtin", in the hope that users 31 // who see FQNs containing this will be able to infer the way in which they are 32 // special, even if they haven't encountered the concept formally yet. 33 const BuiltInProviderNamespace = tfaddr.BuiltInProviderNamespace 34 35 // LegacyProviderNamespace is the special string used in the Namespace field 36 // of type Provider to mark a legacy provider address. This special namespace 37 // value would normally be invalid, and can be used only when the hostname is 38 // DefaultRegistryHost because that host owns the mapping from legacy name to 39 // FQN. 40 const LegacyProviderNamespace = tfaddr.LegacyProviderNamespace 41 42 func IsDefaultProvider(addr Provider) bool { 43 return addr.Hostname == DefaultProviderRegistryHost && addr.Namespace == "hashicorp" 44 } 45 46 // NewProvider constructs a provider address from its parts, and normalizes 47 // the namespace and type parts to lowercase using unicode case folding rules 48 // so that resulting addrs.Provider values can be compared using standard 49 // Go equality rules (==). 50 // 51 // The hostname is given as a svchost.Hostname, which is required by the 52 // contract of that type to have already been normalized for equality testing. 53 // 54 // This function will panic if the given namespace or type name are not valid. 55 // When accepting namespace or type values from outside the program, use 56 // ParseProviderPart first to check that the given value is valid. 57 func NewProvider(hostname svchost.Hostname, namespace, typeName string) Provider { 58 return tfaddr.NewProvider(hostname, namespace, typeName) 59 } 60 61 // ImpliedProviderForUnqualifiedType represents the rules for inferring what 62 // provider FQN a user intended when only a naked type name is available. 63 // 64 // For all except the type name "terraform" this returns a so-called "default" 65 // provider, which is under the registry.terraform.io/hashicorp/ namespace. 66 // 67 // As a special case, the string "terraform" maps to 68 // "terraform.io/builtin/terraform" because that is the more likely user 69 // intent than the now-unmaintained "registry.terraform.io/hashicorp/terraform" 70 // which remains only for compatibility with older Terraform versions. 71 func ImpliedProviderForUnqualifiedType(typeName string) Provider { 72 switch typeName { 73 case "terraform": 74 // Note for future maintainers: any additional strings we add here 75 // as implied to be builtin must never also be use as provider names 76 // in the registry.terraform.io/hashicorp/... namespace, because 77 // otherwise older versions of Terraform could implicitly select 78 // the registry name instead of the internal one. 79 return NewBuiltInProvider(typeName) 80 default: 81 return NewDefaultProvider(typeName) 82 } 83 } 84 85 // NewDefaultProvider returns the default address of a HashiCorp-maintained, 86 // Registry-hosted provider. 87 func NewDefaultProvider(name string) Provider { 88 return tfaddr.Provider{ 89 Type: MustParseProviderPart(name), 90 Namespace: "hashicorp", 91 Hostname: DefaultProviderRegistryHost, 92 } 93 } 94 95 // NewBuiltInProvider returns the address of a "built-in" provider. See 96 // the docs for Provider.IsBuiltIn for more information. 97 func NewBuiltInProvider(name string) Provider { 98 return tfaddr.Provider{ 99 Type: MustParseProviderPart(name), 100 Namespace: BuiltInProviderNamespace, 101 Hostname: BuiltInProviderHost, 102 } 103 } 104 105 // NewLegacyProvider returns a mock address for a provider. 106 // This will be removed when ProviderType is fully integrated. 107 func NewLegacyProvider(name string) Provider { 108 return Provider{ 109 // We intentionally don't normalize and validate the legacy names, 110 // because existing code expects legacy provider names to pass through 111 // verbatim, even if not compliant with our new naming rules. 112 Type: name, 113 Namespace: LegacyProviderNamespace, 114 Hostname: DefaultProviderRegistryHost, 115 } 116 } 117 118 // ParseProviderSourceString parses a value of the form expected in the "source" 119 // argument of a required_providers entry and returns the corresponding 120 // fully-qualified provider address. This is intended primarily to parse the 121 // FQN-like strings returned by terraform-config-inspect. 122 // 123 // The following are valid source string formats: 124 // 125 // - name 126 // - namespace/name 127 // - hostname/namespace/name 128 func ParseProviderSourceString(str string) (tfaddr.Provider, tfdiags.Diagnostics) { 129 var diags tfdiags.Diagnostics 130 131 ret, err := tfaddr.ParseProviderSource(str) 132 if pe, ok := err.(*tfaddr.ParserError); ok { 133 diags = diags.Append(&hcl.Diagnostic{ 134 Severity: hcl.DiagError, 135 Summary: pe.Summary, 136 Detail: pe.Detail, 137 }) 138 return ret, diags 139 } 140 141 if !ret.HasKnownNamespace() { 142 ret.Namespace = "hashicorp" 143 } 144 145 return ret, nil 146 } 147 148 // MustParseProviderSourceString is a wrapper around ParseProviderSourceString that panics if 149 // it returns an error. 150 func MustParseProviderSourceString(str string) Provider { 151 result, diags := ParseProviderSourceString(str) 152 if diags.HasErrors() { 153 panic(diags.Err().Error()) 154 } 155 return result 156 } 157 158 // ParseProviderPart processes an addrs.Provider namespace or type string 159 // provided by an end-user, producing a normalized version if possible or 160 // an error if the string contains invalid characters. 161 // 162 // A provider part is processed in the same way as an individual label in a DNS 163 // domain name: it is transformed to lowercase per the usual DNS case mapping 164 // and normalization rules and may contain only letters, digits, and dashes. 165 // Additionally, dashes may not appear at the start or end of the string. 166 // 167 // These restrictions are intended to allow these names to appear in fussy 168 // contexts such as directory/file names on case-insensitive filesystems, 169 // repository names on GitHub, etc. We're using the DNS rules in particular, 170 // rather than some similar rules defined locally, because the hostname part 171 // of an addrs.Provider is already a hostname and it's ideal to use exactly 172 // the same case folding and normalization rules for all of the parts. 173 // 174 // In practice a provider type string conventionally does not contain dashes 175 // either. Such names are permitted, but providers with such type names will be 176 // hard to use because their resource type names will not be able to contain 177 // the provider type name and thus each resource will need an explicit provider 178 // address specified. (A real-world example of such a provider is the 179 // "google-beta" variant of the GCP provider, which has resource types that 180 // start with the "google_" prefix instead.) 181 // 182 // It's valid to pass the result of this function as the argument to a 183 // subsequent call, in which case the result will be identical. 184 func ParseProviderPart(given string) (string, error) { 185 return tfaddr.ParseProviderPart(given) 186 } 187 188 // MustParseProviderPart is a wrapper around ParseProviderPart that panics if 189 // it returns an error. 190 func MustParseProviderPart(given string) string { 191 result, err := ParseProviderPart(given) 192 if err != nil { 193 panic(err.Error()) 194 } 195 return result 196 } 197 198 // IsProviderPartNormalized compares a given string to the result of ParseProviderPart(string) 199 func IsProviderPartNormalized(str string) (bool, error) { 200 normalized, err := ParseProviderPart(str) 201 if err != nil { 202 return false, err 203 } 204 if str == normalized { 205 return true, nil 206 } 207 return false, nil 208 }