github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/addrs/provider.go (about) 1 package addrs 2 3 import ( 4 "fmt" 5 "strings" 6 7 "github.com/hashicorp/hcl/v2" 8 svchost "github.com/hashicorp/terraform-svchost" 9 "github.com/hashicorp/terraform/tfdiags" 10 "golang.org/x/net/idna" 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 struct { 16 Type string 17 Namespace string 18 Hostname svchost.Hostname 19 } 20 21 // DefaultRegistryHost is the hostname used for provider addresses that do 22 // not have an explicit hostname. 23 const DefaultRegistryHost = svchost.Hostname("registry.terraform.io") 24 25 // LegacyProviderNamespace is the special string used in the Namespace field 26 // of type Provider to mark a legacy provider address. This special namespace 27 // value would normally be invalid, and can be used only when the hostname is 28 // DefaultRegistryHost because that host owns the mapping from legacy name to 29 // FQN. 30 const LegacyProviderNamespace = "-" 31 32 // String returns an FQN string, indended for use in output. 33 func (pt Provider) String() string { 34 return pt.Hostname.ForDisplay() + "/" + pt.Namespace + "/" + pt.Type 35 } 36 37 // NewDefaultProvider returns the default address of a HashiCorp-maintained, 38 // Registry-hosted provider. 39 func NewDefaultProvider(name string) Provider { 40 return Provider{ 41 Type: name, 42 Namespace: "hashicorp", 43 Hostname: "registry.terraform.io", 44 } 45 } 46 47 // NewLegacyProvider returns a mock address for a provider. 48 // This will be removed when ProviderType is fully integrated. 49 func NewLegacyProvider(name string) Provider { 50 return Provider{ 51 Type: name, 52 Namespace: LegacyProviderNamespace, 53 Hostname: DefaultRegistryHost, 54 } 55 } 56 57 // LegacyString returns the provider type, which is frequently used 58 // interchangeably with provider name. This function can and should be removed 59 // when provider type is fully integrated. As a safeguard for future 60 // refactoring, this function panics if the Provider is not a legacy provider. 61 func (pt Provider) LegacyString() string { 62 if pt.Namespace != "-" { 63 panic("not a legacy Provider") 64 } 65 return pt.Type 66 } 67 68 // ParseProviderSourceString parses the source attribute and returns a provider. 69 // This is intended primarily to parse the FQN-like strings returned by 70 // terraform-config-inspect. 71 // 72 // The following are valid source string formats: 73 // name 74 // namespace/name 75 // hostname/namespace/name 76 func ParseProviderSourceString(str string) (Provider, tfdiags.Diagnostics) { 77 var ret Provider 78 var diags tfdiags.Diagnostics 79 80 // split the source string into individual components 81 parts := strings.Split(str, "/") 82 if len(parts) == 0 || len(parts) > 3 { 83 diags = diags.Append(&hcl.Diagnostic{ 84 Severity: hcl.DiagError, 85 Summary: "Invalid provider source string", 86 Detail: `The "source" attribute must be in the format "[hostname/][namespace/]name"`, 87 }) 88 return ret, diags 89 } 90 91 // check for an invalid empty string in any part 92 for i := range parts { 93 if parts[i] == "" { 94 diags = diags.Append(&hcl.Diagnostic{ 95 Severity: hcl.DiagError, 96 Summary: "Invalid provider source string", 97 Detail: `The "source" attribute must be in the format "[hostname/][namespace/]name"`, 98 }) 99 return ret, diags 100 } 101 } 102 103 // check the 'name' portion, which is always the last part 104 givenName := parts[len(parts)-1] 105 name, err := ParseProviderPart(givenName) 106 if err != nil { 107 diags = diags.Append(&hcl.Diagnostic{ 108 Severity: hcl.DiagError, 109 Summary: "Invalid provider type", 110 Detail: fmt.Sprintf(`Invalid provider type %q in source %q: %s"`, givenName, str, err), 111 }) 112 return ret, diags 113 } 114 ret.Type = name 115 ret.Hostname = DefaultRegistryHost 116 117 if len(parts) == 1 { 118 return NewDefaultProvider(parts[0]), diags 119 } 120 121 if len(parts) >= 2 { 122 // the namespace is always the second-to-last part 123 givenNamespace := parts[len(parts)-2] 124 if givenNamespace == LegacyProviderNamespace { 125 // For now we're tolerating legacy provider addresses until we've 126 // finished updating the rest of the codebase to no longer use them, 127 // or else we'd get errors round-tripping through legacy subsystems. 128 ret.Namespace = LegacyProviderNamespace 129 } else { 130 namespace, err := ParseProviderPart(givenNamespace) 131 if err != nil { 132 diags = diags.Append(&hcl.Diagnostic{ 133 Severity: hcl.DiagError, 134 Summary: "Invalid provider namespace", 135 Detail: fmt.Sprintf(`Invalid provider namespace %q in source %q: %s"`, namespace, str, err), 136 }) 137 return Provider{}, diags 138 } 139 ret.Namespace = namespace 140 } 141 } 142 143 // Final Case: 3 parts 144 if len(parts) == 3 { 145 // the namespace is always the first part in a three-part source string 146 hn, err := svchost.ForComparison(parts[0]) 147 if err != nil { 148 diags = diags.Append(&hcl.Diagnostic{ 149 Severity: hcl.DiagError, 150 Summary: "Invalid provider source hostname", 151 Detail: fmt.Sprintf(`Invalid provider source hostname namespace %q in source %q: %s"`, hn, str, err), 152 }) 153 return Provider{}, diags 154 } 155 ret.Hostname = hn 156 } 157 158 if ret.Namespace == LegacyProviderNamespace && ret.Hostname != DefaultRegistryHost { 159 // Legacy provider addresses must always be on the default registry 160 // host, because the default registry host decides what actual FQN 161 // each one maps to. 162 diags = diags.Append(&hcl.Diagnostic{ 163 Severity: hcl.DiagError, 164 Summary: "Invalid provider namespace", 165 Detail: "The legacy provider namespace \"-\" can be used only with hostname " + DefaultRegistryHost.ForDisplay() + ".", 166 }) 167 return Provider{}, diags 168 } 169 170 return ret, diags 171 } 172 173 // ParseProviderPart processes an addrs.Provider namespace or type string 174 // provided by an end-user, producing a normalized version if possible or 175 // an error if the string contains invalid characters. 176 // 177 // A provider part is processed in the same way as an individual label in a DNS 178 // domain name: it is transformed to lowercase per the usual DNS case mapping 179 // and normalization rules and may contain only letters, digits, and dashes. 180 // Additionally, dashes may not appear at the start or end of the string. 181 // 182 // These restrictions are intended to allow these names to appear in fussy 183 // contexts such as directory/file names on case-insensitive filesystems, 184 // repository names on GitHub, etc. We're using the DNS rules in particular, 185 // rather than some similar rules defined locally, because the hostname part 186 // of an addrs.Provider is already a hostname and it's ideal to use exactly 187 // the same case folding and normalization rules for all of the parts. 188 // 189 // In practice a provider type string conventionally does not contain dashes 190 // either. Such names are permitted, but providers with such type names will be 191 // hard to use because their resource type names will not be able to contain 192 // the provider type name and thus each resource will need an explicit provider 193 // address specified. (A real-world example of such a provider is the 194 // "google-beta" variant of the GCP provider, which has resource types that 195 // start with the "google_" prefix instead.) 196 // 197 // It's valid to pass the result of this function as the argument to a 198 // subsequent call, in which case the result will be identical. 199 func ParseProviderPart(given string) (string, error) { 200 if len(given) == 0 { 201 return "", fmt.Errorf("must have at least one character") 202 } 203 204 // We're going to process the given name using the same "IDNA" library we 205 // use for the hostname portion, since it already implements the case 206 // folding rules we want. 207 // 208 // The idna library doesn't expose individual label parsing directly, but 209 // once we've verified it doesn't contain any dots we can just treat it 210 // like a top-level domain for this library's purposes. 211 if strings.ContainsRune(given, '.') { 212 return "", fmt.Errorf("dots are not allowed") 213 } 214 215 // We don't allow names containing multiple consecutive dashes, just as 216 // a matter of preference: they look weird, confusing, or incorrect. 217 // This also, as a side-effect, prevents the use of the "punycode" 218 // indicator prefix "xn--" that would cause the IDNA library to interpret 219 // the given name as punycode, because that would be weird and unexpected. 220 if strings.Contains(given, "--") { 221 return "", fmt.Errorf("cannot use multiple consecutive dashes") 222 } 223 224 result, err := idna.Lookup.ToUnicode(given) 225 if err != nil { 226 return "", fmt.Errorf("must contain only letters, digits, and dashes, and may not use leading or trailing dashes") 227 } 228 229 return result, nil 230 }