2019-12-04 10:30:20 -06:00
package addrs
2019-12-06 07:00:18 -06:00
import (
2020-02-05 08:27:32 -06:00
"fmt"
"strings"
2020-02-14 20:10:03 -06:00
"golang.org/x/net/idna"
2020-02-05 08:27:32 -06:00
"github.com/hashicorp/hcl/v2"
2019-12-06 07:00:18 -06:00
svchost "github.com/hashicorp/terraform-svchost"
2020-02-05 08:27:32 -06:00
"github.com/hashicorp/terraform/tfdiags"
2019-12-06 07:00:18 -06:00
)
2019-12-04 10:30:20 -06:00
// Provider encapsulates a single provider type. In the future this will be
// extended to include additional fields including Namespace and SourceHost
type Provider struct {
Type string
Namespace string
Hostname svchost . Hostname
}
2020-02-14 20:10:03 -06:00
// DefaultRegistryHost is the hostname used for provider addresses that do
// not have an explicit hostname.
const DefaultRegistryHost = svchost . Hostname ( "registry.terraform.io" )
2020-02-05 08:27:32 -06:00
2020-03-30 14:02:33 -05:00
// BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider
// namespace. Built-in provider addresses must also have their namespace set
// to BuiltInProviderNamespace in order to be considered as built-in.
const BuiltInProviderHost = svchost . Hostname ( "terraform.io" )
// BuiltInProviderNamespace is the provider namespace used for "built-in"
// providers. Built-in provider addresses must also have their hostname
// set to BuiltInProviderHost in order to be considered as built-in.
//
// The this namespace is literally named "builtin", in the hope that users
// who see FQNs containing this will be able to infer the way in which they are
// special, even if they haven't encountered the concept formally yet.
const BuiltInProviderNamespace = "builtin"
2020-02-14 20:10:03 -06:00
// LegacyProviderNamespace is the special string used in the Namespace field
// of type Provider to mark a legacy provider address. This special namespace
// value would normally be invalid, and can be used only when the hostname is
// DefaultRegistryHost because that host owns the mapping from legacy name to
// FQN.
const LegacyProviderNamespace = "-"
2020-02-05 08:27:32 -06:00
2020-04-03 07:37:40 -05:00
// String returns an FQN string, indended for use in machine-readable output.
2019-12-04 10:30:20 -06:00
func ( pt Provider ) String ( ) string {
2020-02-14 20:10:03 -06:00
if pt . IsZero ( ) {
panic ( "called String on zero-value addrs.Provider" )
}
2019-12-04 10:30:20 -06:00
return pt . Hostname . ForDisplay ( ) + "/" + pt . Namespace + "/" + pt . Type
}
2020-04-03 07:37:40 -05:00
// ForDisplay returns a user-friendly FQN string, simplified for readability. If
// the provider is using the default hostname, the hostname is omitted.
func ( pt Provider ) ForDisplay ( ) string {
if pt . IsZero ( ) {
panic ( "called ForDisplay on zero-value addrs.Provider" )
}
if pt . Hostname == DefaultRegistryHost {
return pt . Namespace + "/" + pt . Type
}
return pt . Hostname . ForDisplay ( ) + "/" + pt . Namespace + "/" + pt . Type
}
2020-02-14 20:10:03 -06:00
// NewProvider constructs a provider address from its parts, and normalizes
// the namespace and type parts to lowercase using unicode case folding rules
// so that resulting addrs.Provider values can be compared using standard
// Go equality rules (==).
//
// The hostname is given as a svchost.Hostname, which is required by the
// contract of that type to have already been normalized for equality testing.
//
// This function will panic if the given namespace or type name are not valid.
// When accepting namespace or type values from outside the program, use
// ParseProviderPart first to check that the given value is valid.
func NewProvider ( hostname svchost . Hostname , namespace , typeName string ) Provider {
if namespace == LegacyProviderNamespace {
// Legacy provider addresses must always be created via
// NewLegacyProvider so that we can use static analysis to find
// codepaths still working with those.
panic ( "attempt to create legacy provider address using NewProvider; use NewLegacyProvider instead" )
}
return Provider {
Type : MustParseProviderPart ( typeName ) ,
Namespace : MustParseProviderPart ( namespace ) ,
Hostname : hostname ,
}
}
2020-04-01 18:11:15 -05:00
// ImpliedProviderForUnqualifiedType represents the rules for inferring what
// provider FQN a user intended when only a naked type name is available.
//
// For all except the type name "terraform" this returns a so-called "default"
// provider, which is under the registry.terraform.io/hashicorp/ namespace.
//
// As a special case, the string "terraform" maps to
// "terraform.io/builtin/terraform" because that is the more likely user
// intent than the now-unmaintained "registry.terraform.io/hashicorp/terraform"
// which remains only for compatibility with older Terraform versions.
func ImpliedProviderForUnqualifiedType ( typeName string ) Provider {
switch typeName {
case "terraform" :
// Note for future maintainers: any additional strings we add here
// as implied to be builtin must never also be use as provider names
// in the registry.terraform.io/hashicorp/... namespace, because
// otherwise older versions of Terraform could implicitly select
// the registry name instead of the internal one.
return NewBuiltInProvider ( typeName )
default :
return NewDefaultProvider ( typeName )
}
}
2019-12-04 10:30:20 -06:00
// NewDefaultProvider returns the default address of a HashiCorp-maintained,
// Registry-hosted provider.
func NewDefaultProvider ( name string ) Provider {
return Provider {
2020-02-14 20:10:03 -06:00
Type : MustParseProviderPart ( name ) ,
2019-12-04 10:30:20 -06:00
Namespace : "hashicorp" ,
2020-02-05 08:27:32 -06:00
Hostname : DefaultRegistryHost ,
2019-12-04 10:30:20 -06:00
}
}
2020-03-30 14:02:33 -05:00
// NewBuiltInProvider returns the address of a "built-in" provider. See
// the docs for Provider.IsBuiltIn for more information.
func NewBuiltInProvider ( name string ) Provider {
return Provider {
Type : MustParseProviderPart ( name ) ,
Namespace : BuiltInProviderNamespace ,
Hostname : BuiltInProviderHost ,
}
}
2019-12-04 10:30:20 -06:00
// NewLegacyProvider returns a mock address for a provider.
// This will be removed when ProviderType is fully integrated.
func NewLegacyProvider ( name string ) Provider {
return Provider {
2020-02-14 20:10:03 -06:00
// We intentionally don't normalize and validate the legacy names,
// because existing code expects legacy provider names to pass through
// verbatim, even if not compliant with our new naming rules.
2019-12-04 10:30:20 -06:00
Type : name ,
2020-02-14 20:10:03 -06:00
Namespace : LegacyProviderNamespace ,
2020-02-05 08:27:32 -06:00
Hostname : DefaultRegistryHost ,
2019-12-04 10:30:20 -06:00
}
}
// LegacyString returns the provider type, which is frequently used
// interchangeably with provider name. This function can and should be removed
// when provider type is fully integrated. As a safeguard for future
// refactoring, this function panics if the Provider is not a legacy provider.
func ( pt Provider ) LegacyString ( ) string {
2020-02-14 20:10:03 -06:00
if pt . IsZero ( ) {
panic ( "called LegacyString on zero-value addrs.Provider" )
}
2020-08-14 13:56:22 -05:00
if pt . Namespace != LegacyProviderNamespace && pt . Namespace != BuiltInProviderNamespace {
panic ( pt . String ( ) + " cannot be represented as a legacy string" )
2019-12-04 10:30:20 -06:00
}
return pt . Type
}
2020-02-05 08:27:32 -06:00
2020-02-14 20:10:03 -06:00
// IsZero returns true if the receiver is the zero value of addrs.Provider.
//
// The zero value is not a valid addrs.Provider and calling other methods on
// such a value is likely to either panic or otherwise misbehave.
func ( pt Provider ) IsZero ( ) bool {
return pt == Provider { }
}
2020-03-30 14:02:33 -05:00
// IsBuiltIn returns true if the receiver is the address of a "built-in"
// provider. That is, a provider under terraform.io/builtin/ which is
// included as part of the Terraform binary itself rather than one to be
// installed from elsewhere.
//
// These are ignored by the provider installer because they are assumed to
// already be available without any further installation.
func ( pt Provider ) IsBuiltIn ( ) bool {
return pt . Hostname == BuiltInProviderHost && pt . Namespace == BuiltInProviderNamespace
}
2020-02-14 20:21:10 -06:00
// LessThan returns true if the receiver should sort before the other given
// address in an ordered list of provider addresses.
//
// This ordering is an arbitrary one just to allow deterministic results from
// functions that would otherwise have no natural ordering. It's subject
// to change in future.
func ( pt Provider ) LessThan ( other Provider ) bool {
switch {
case pt . Hostname != other . Hostname :
return pt . Hostname < other . Hostname
case pt . Namespace != other . Namespace :
return pt . Namespace < other . Namespace
default :
return pt . Type < other . Type
}
}
2020-03-23 08:50:35 -05:00
// IsLegacy returns true if the provider is a legacy-style provider
func ( pt Provider ) IsLegacy ( ) bool {
if pt . IsZero ( ) {
panic ( "called IsLegacy() on zero-value addrs.Provider" )
}
return pt . Hostname == DefaultRegistryHost && pt . Namespace == LegacyProviderNamespace
}
// IsDefault returns true if the provider is a default hashicorp provider
func ( pt Provider ) IsDefault ( ) bool {
if pt . IsZero ( ) {
panic ( "called IsDefault() on zero-value addrs.Provider" )
}
return pt . Hostname == DefaultRegistryHost && pt . Namespace == "hashicorp"
}
2020-03-12 11:00:00 -05:00
// Equals returns true if the receiver and other provider have the same attributes.
func ( pt Provider ) Equals ( other Provider ) bool {
return pt == other
}
2020-02-05 08:27:32 -06:00
// ParseProviderSourceString parses the source attribute and returns a provider.
// This is intended primarily to parse the FQN-like strings returned by
// terraform-config-inspect.
//
// The following are valid source string formats:
// name
// namespace/name
// hostname/namespace/name
func ParseProviderSourceString ( str string ) ( Provider , tfdiags . Diagnostics ) {
var ret Provider
var diags tfdiags . Diagnostics
// split the source string into individual components
parts := strings . Split ( str , "/" )
if len ( parts ) == 0 || len ( parts ) > 3 {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider source string" ,
Detail : ` The "source" attribute must be in the format "[hostname/][namespace/]name" ` ,
} )
return ret , diags
}
// check for an invalid empty string in any part
for i := range parts {
if parts [ i ] == "" {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider source string" ,
Detail : ` The "source" attribute must be in the format "[hostname/][namespace/]name" ` ,
} )
return ret , diags
}
}
// check the 'name' portion, which is always the last part
2020-02-14 20:10:03 -06:00
givenName := parts [ len ( parts ) - 1 ]
name , err := ParseProviderPart ( givenName )
if err != nil {
2020-02-05 08:27:32 -06:00
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider type" ,
2020-04-01 13:26:04 -05:00
Detail : fmt . Sprintf ( ` Invalid provider type %q in source %q: %s" ` , givenName , str , err ) ,
2020-02-05 08:27:32 -06:00
} )
return ret , diags
}
ret . Type = name
ret . Hostname = DefaultRegistryHost
if len ( parts ) == 1 {
2020-05-14 08:00:58 -05:00
return NewDefaultProvider ( parts [ 0 ] ) , diags
2020-02-05 08:27:32 -06:00
}
if len ( parts ) >= 2 {
// the namespace is always the second-to-last part
2020-02-14 20:10:03 -06:00
givenNamespace := parts [ len ( parts ) - 2 ]
if givenNamespace == LegacyProviderNamespace {
// For now we're tolerating legacy provider addresses until we've
// finished updating the rest of the codebase to no longer use them,
// or else we'd get errors round-tripping through legacy subsystems.
ret . Namespace = LegacyProviderNamespace
} else {
namespace , err := ParseProviderPart ( givenNamespace )
if err != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider namespace" ,
Detail : fmt . Sprintf ( ` Invalid provider namespace %q in source %q: %s" ` , namespace , str , err ) ,
} )
return Provider { } , diags
}
ret . Namespace = namespace
2020-02-05 08:27:32 -06:00
}
}
// Final Case: 3 parts
if len ( parts ) == 3 {
// the namespace is always the first part in a three-part source string
hn , err := svchost . ForComparison ( parts [ 0 ] )
if err != nil {
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider source hostname" ,
2020-02-14 20:10:03 -06:00
Detail : fmt . Sprintf ( ` Invalid provider source hostname namespace %q in source %q: %s" ` , hn , str , err ) ,
2020-02-05 08:27:32 -06:00
} )
return Provider { } , diags
}
ret . Hostname = hn
}
2020-02-14 20:10:03 -06:00
if ret . Namespace == LegacyProviderNamespace && ret . Hostname != DefaultRegistryHost {
// Legacy provider addresses must always be on the default registry
// host, because the default registry host decides what actual FQN
// each one maps to.
diags = diags . Append ( & hcl . Diagnostic {
Severity : hcl . DiagError ,
Summary : "Invalid provider namespace" ,
Detail : "The legacy provider namespace \"-\" can be used only with hostname " + DefaultRegistryHost . ForDisplay ( ) + "." ,
} )
return Provider { } , diags
}
2020-07-07 17:44:28 -05:00
// Due to how plugin executables are named and provider git repositories
// are conventionally named, it's a reasonable and
// apparently-somewhat-common user error to incorrectly use the
// "terraform-provider-" prefix in a provider source address. There is
// no good reason for a provider to have the prefix "terraform-" anyway,
// so we've made that invalid from the start both so we can give feedback
// to provider developers about the terraform- prefix being redundant
// and give specialized feedback to folks who incorrectly use the full
// terraform-provider- prefix to help them self-correct.
const redundantPrefix = "terraform-"
const userErrorPrefix = "terraform-provider-"
if strings . HasPrefix ( ret . Type , redundantPrefix ) {
if strings . HasPrefix ( ret . Type , userErrorPrefix ) {
// Likely user error. We only return this specialized error if
// whatever is after the prefix would otherwise be a
// syntactically-valid provider type, so we don't end up advising
// the user to try something that would be invalid for another
// reason anyway.
// (This is mainly just for robustness, because the validation
// we already did above should've rejected most/all ways for
// the suggestedType to end up invalid here.)
suggestedType := ret . Type [ len ( userErrorPrefix ) : ]
if _ , err := ParseProviderPart ( suggestedType ) ; err == nil {
suggestedAddr := ret
suggestedAddr . Type = suggestedType
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Invalid provider type" ,
fmt . Sprintf ( "Provider source %q has a type with the prefix %q, which isn't valid. Although that prefix is often used in the names of version control repositories for Terraform providers, provider source strings should not include it.\n\nDid you mean %q?" , ret . ForDisplay ( ) , userErrorPrefix , suggestedAddr . ForDisplay ( ) ) ,
) )
return Provider { } , diags
}
}
// Otherwise, probably instead an incorrectly-named provider, perhaps
// arising from a similar instinct to what causes there to be
// thousands of Python packages on PyPI with "python-"-prefixed
// names.
diags = diags . Append ( tfdiags . Sourceless (
tfdiags . Error ,
"Invalid provider type" ,
fmt . Sprintf ( "Provider source %q has a type with the prefix %q, which isn't allowed because it would be redundant to name a Terraform provider with that prefix. If you are the author of this provider, rename it to not include the prefix." , ret , redundantPrefix ) ,
) )
return Provider { } , diags
}
2020-02-05 08:27:32 -06:00
return ret , diags
}
2020-02-14 20:10:03 -06:00
2020-05-11 12:49:12 -05:00
// MustParseProviderSourceString is a wrapper around ParseProviderSourceString that panics if
// it returns an error.
func MustParseProviderSourceString ( str string ) Provider {
result , diags := ParseProviderSourceString ( str )
if diags . HasErrors ( ) {
panic ( diags . Err ( ) . Error ( ) )
}
return result
}
2020-02-14 20:10:03 -06:00
// ParseProviderPart processes an addrs.Provider namespace or type string
// provided by an end-user, producing a normalized version if possible or
// an error if the string contains invalid characters.
//
// A provider part is processed in the same way as an individual label in a DNS
// domain name: it is transformed to lowercase per the usual DNS case mapping
// and normalization rules and may contain only letters, digits, and dashes.
// Additionally, dashes may not appear at the start or end of the string.
//
// These restrictions are intended to allow these names to appear in fussy
// contexts such as directory/file names on case-insensitive filesystems,
// repository names on GitHub, etc. We're using the DNS rules in particular,
// rather than some similar rules defined locally, because the hostname part
// of an addrs.Provider is already a hostname and it's ideal to use exactly
// the same case folding and normalization rules for all of the parts.
//
// In practice a provider type string conventionally does not contain dashes
// either. Such names are permitted, but providers with such type names will be
// hard to use because their resource type names will not be able to contain
// the provider type name and thus each resource will need an explicit provider
// address specified. (A real-world example of such a provider is the
// "google-beta" variant of the GCP provider, which has resource types that
// start with the "google_" prefix instead.)
//
// It's valid to pass the result of this function as the argument to a
// subsequent call, in which case the result will be identical.
func ParseProviderPart ( given string ) ( string , error ) {
if len ( given ) == 0 {
return "" , fmt . Errorf ( "must have at least one character" )
}
// We're going to process the given name using the same "IDNA" library we
// use for the hostname portion, since it already implements the case
// folding rules we want.
//
// The idna library doesn't expose individual label parsing directly, but
// once we've verified it doesn't contain any dots we can just treat it
// like a top-level domain for this library's purposes.
if strings . ContainsRune ( given , '.' ) {
return "" , fmt . Errorf ( "dots are not allowed" )
}
// We don't allow names containing multiple consecutive dashes, just as
// a matter of preference: they look weird, confusing, or incorrect.
// This also, as a side-effect, prevents the use of the "punycode"
// indicator prefix "xn--" that would cause the IDNA library to interpret
// the given name as punycode, because that would be weird and unexpected.
if strings . Contains ( given , "--" ) {
return "" , fmt . Errorf ( "cannot use multiple consecutive dashes" )
}
result , err := idna . Lookup . ToUnicode ( given )
if err != nil {
return "" , fmt . Errorf ( "must contain only letters, digits, and dashes, and may not use leading or trailing dashes" )
}
return result , nil
}
// MustParseProviderPart is a wrapper around ParseProviderPart that panics if
// it returns an error.
func MustParseProviderPart ( given string ) string {
result , err := ParseProviderPart ( given )
if err != nil {
panic ( err . Error ( ) )
}
return result
}
2020-05-14 08:00:58 -05:00
// IsProviderPartNormalized compares a given string to the result of ParseProviderPart(string)
func IsProviderPartNormalized ( str string ) ( bool , error ) {
normalized , err := ParseProviderPart ( str )
if err != nil {
return false , err
}
if str == normalized {
return true , nil
}
return false , nil
}