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  }