diff --git a/README.md b/README.md index 091ff70..bb7e80a 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,147 @@ # terraform-registry-address -This package helps with representation, comparison and parsing of -Terraform Registry addresses, such as -`registry.terraform.io/grafana/grafana` or `hashicorp/aws`. +This module enables parsing, comparison and canonical representation of +[Terraform Registry](https://registry.terraform.io/) **provider** addresses +(such as `registry.terraform.io/grafana/grafana` or `hashicorp/aws`) +and **module** addresses (such as `hashicorp/subnets/cidr`). -The most common source of these addresses outside of Terraform Core -is JSON representation of state, plan, or schemas as obtained -via [`hashicorp/terraform-exec`](https://github.com/hashicorp/terraform-exec). +**Provider** addresses can be found in -## Parsing Provider Addresses + - [`terraform show -json `](https://www.terraform.io/internals/json-format#configuration-representation) (`full_name`) + - [`terraform version -json`](https://www.terraform.io/cli/commands/version#example) (`provider_selections`) + - [`terraform providers schema -json`](https://www.terraform.io/cli/commands/providers/schema#providers-schema-representation) (keys of `provider_schemas`) + - within `required_providers` block in Terraform configuration (`*.tf`) -### Example +**Module** addresses can be found within `source` argument +of `module` block in Terraform configuration (`*.tf`) +and parts of the address (namespace and name) in the Registry API. + +## Compatibility + +The module assumes compatibility with Terraform v0.12 and later, +which have the mentioned JSON output produced by corresponding CLI flags. + +We recommend carefully reading the [ambigouous provider addresses](#Ambiguous-Provider-Addresses) +section below which may impact versions `0.12` and `0.13`. + +## Related Libraries + +Other libraries which may help with consuming most of the above Terraform +outputs in automation: + + - [`hashicorp/terraform-exec`](https://github.com/hashicorp/terraform-exec) + - [`hashicorp/terraform-json`](https://github.com/hashicorp/terraform-json) + +## Usage + +### Provider ```go -p, err := ParseRawProviderSourceString("hashicorp/aws") +pAddr, err := ParseProviderSource("hashicorp/aws") if err != nil { // deal with error } -// p == Provider{ +// pAddr == Provider{ // Type: "aws", // Namespace: "hashicorp", -// Hostname: svchost.Hostname("registry.terraform.io"), +// Hostname: DefaultProviderRegistryHost, // } ``` -### Legacy address +### Module + +```go +mAddr, err := ParseModuleSource("hashicorp/consul/aws//modules/consul-cluster") +if err != nil { + // deal with error +} + +// mAddr == Module{ +// Package: ModulePackage{ +// Host: DefaultProviderRegistryHost, +// Namespace: "hashicorp", +// Name: "consul", +// TargetSystem: "aws", +// }, +// Subdir: "modules/consul-cluster", +// }, +``` + +## Other Module Address Formats + +Modules can also be sourced from [other sources](https://www.terraform.io/language/modules/sources) +and these other sources (outside of Terraform Registry) +have different address formats, such as `./local` or +`github.com/hashicorp/example`. + +This library does _not_ recognize such other address formats +and it will return error upon parsing these. + +## Ambiguous Provider Addresses + +Qualified addresses with namespace (such as `hashicorp/aws`) +are used exclusively in all recent versions (`0.14+`) of Terraform. +If you only work with Terraform `v0.14.0+` configuration/output, you may +safely ignore the rest of this section and related part of the API. + +There are a few types of ambiguous addresses you may comes accross: + + - Terraform `v0.12` uses "namespace-less address", such as `aws`. + - Terraform `v0.13` may use `-` as a placeholder for the unknown namespace, + resulting in address such as `-/aws`. + - Terraform `v0.14+` _configuration_ still allows ambiguous providers + through `provider "" {}` block _without_ corresponding + entry inside `required_providers`, but these providers are always + resolved as `hashicorp/` and all JSON outputs only use that + resolved address. + +Both ambiguous address formats are accepted by `ParseProviderSource()` + +```go +pAddr, err := ParseProviderSource("aws") +if err != nil { + // deal with error +} + +// pAddr == Provider{ +// Type: "aws", +// Namespace: UnknownProviderNamespace, // "?" +// Hostname: DefaultProviderRegistryHost, // "registry.terraform.io" +// } +pAddr.HasKnownNamespace() // == false +pAddr.IsLegacy() // == false +``` +```go +pAddr, err := ParseProviderSource("-/aws") +if err != nil { + // deal with error +} -A legacy address is by itself (without more context) ambiguous. -For example `aws` may represent either the official `hashicorp/aws` -or just any custom-built provider called `aws`. +// pAddr == Provider{ +// Type: "aws", +// Namespace: LegacyProviderNamespace, // "-" +// Hostname: DefaultProviderRegistryHost, // "registry.terraform.io" +// } +pAddr.HasKnownNamespace() // == true +pAddr.IsLegacy() // == true +``` -Such ambiguous address can be produced by Terraform `<=0.12`. You can -just use `ImpliedProviderForUnqualifiedType` if you know for sure -the address was produced by an affected version. +However `NewProvider()` will panic if you pass an empty namespace +or any placeholder indicating unknown namespace. -If you do not have that context you should parse the string via -`ParseRawProviderSourceString` and then check `addr.IsLegacy()`. +```go +NewProvider(DefaultProviderRegistryHost, "aws", "") // panic +NewProvider(DefaultProviderRegistryHost, "aws", "-") // panic +NewProvider(DefaultProviderRegistryHost, "aws", "?") // panic +``` -#### What to do with a legacy address? +If you come across an ambiguous address, you should resolve +it to a fully qualified one and use that one instead. -Ask the Registry API whether and where the provider was moved to +### Resolving Ambiguous Address -(`-` represents the legacy, basically unknown namespace) +The Registry API provides the safest way of resolving an ambiguous address. ```sh # grafana (redirected to its own namespace) @@ -56,28 +155,15 @@ $ curl -s https://registry.terraform.io/v1/providers/-/aws/versions | jq '(.id, null ``` -Then: - - - Reparse the _new_ address (`moved_to`) of any _moved_ provider (e.g. `grafana/grafana`) via `ParseRawProviderSourceString` - - Reparse the full address (`id`) of any other provider (e.g. `hashicorp/aws`) - -Depending on context (legacy) `terraform` may need to be parsed separately. -Read more about this provider below. - -If for some reason you cannot ask the Registry API you may also use -`ParseAndInferProviderSourceString` which assumes that any legacy address -(including `terraform`) belongs to the `hashicorp` namespace. - -If you cache results (which you should), ensure you have invalidation -mechanism in place because target (migrated) namespace may change. -Hard-coding migrations anywhere in code is strongly discouraged. +When you cache results, ensure you have invalidation +mechanism in place as target (migrated) namespace may change. #### `terraform` provider Like any other legacy address `terraform` is also ambiguous. Such address may (most unlikely) represent a custom-built provider called `terraform`, or the now archived [`hashicorp/terraform` provider in the registry](https://registry.terraform.io/providers/hashicorp/terraform/latest), -or (most likely) the `terraform` provider built into 0.12+, which is +or (most likely) the `terraform` provider built into 0.11+, which is represented via a dedicated FQN of `terraform.io/builtin/terraform` in 0.13+. You may be able to differentiate between these different providers if you @@ -87,25 +173,7 @@ Alternatively you may just treat the address as the builtin provider, i.e. assume all of its logic including schema is contained within Terraform Core. -In such case you should just use `NewBuiltInProvider("terraform")`. - -## Parsing Module Addresses - -### Example - +In such case you should construct the address in the following way ```go -registry, err := ParseRawModuleSourceRegistry("hashicorp/subnets/cidr") -if err != nil { - // deal with error -} - -// registry == ModuleSourceRegistry{ -// PackageAddr: ModuleRegistryPackage{ -// Host: svchost.Hostname("registry.terraform.io"), -// Namespace: "hashicorp", -// Name: "subnets", -// TargetSystem: "cidr", -// }, -// Subdir: "", -// }, +pAddr := NewProvider(BuiltInProviderHost, BuiltInProviderNamespace, "terraform") ``` diff --git a/module.go b/module.go index f90279e..6af0c59 100644 --- a/module.go +++ b/module.go @@ -9,14 +9,14 @@ import ( svchost "github.com/hashicorp/terraform-svchost" ) -// ModuleSourceRegistry is representing a module listed in a Terraform module +// Module is representing a module listed in a Terraform module // registry. -type ModuleSourceRegistry struct { - // PackageAddr is the registry package that the target module belongs to. +type Module struct { + // Package is the registry package that the target module belongs to. // The module installer must translate this into a ModuleSourceRemote // using the registry API and then take that underlying address's - // PackageAddr in order to find the actual package location. - PackageAddr ModuleRegistryPackage + // Package in order to find the actual package location. + Package ModulePackage // If Subdir is non-empty then it represents a sub-directory within the // remote package that the registry address eventually resolves to. @@ -36,22 +36,22 @@ const DefaultModuleRegistryHost = svchost.Hostname("registry.terraform.io") var moduleRegistryNamePattern = regexp.MustCompile("^[0-9A-Za-z](?:[0-9A-Za-z-_]{0,62}[0-9A-Za-z])?$") var moduleRegistryTargetSystemPattern = regexp.MustCompile("^[0-9a-z]{1,64}$") -// ParseRawModuleSourceRegistry only accepts module registry addresses, and +// ParseModuleSource only accepts module registry addresses, and // will reject any other address type. -func ParseRawModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) { +func ParseModuleSource(raw string) (Module, error) { var err error var subDir string raw, subDir = splitPackageSubdir(raw) if strings.HasPrefix(subDir, "../") { - return ModuleSourceRegistry{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir) + return Module{}, fmt.Errorf("subdirectory path %q leads outside of the module package", subDir) } parts := strings.Split(raw, "/") // A valid registry address has either three or four parts, because the // leading hostname part is optional. if len(parts) != 3 && len(parts) != 4 { - return ModuleSourceRegistry{}, fmt.Errorf("a module registry source address must have either three or four slash-separated components") + return Module{}, fmt.Errorf("a module registry source address must have either three or four slash-separated components") } host := DefaultModuleRegistryHost @@ -64,20 +64,20 @@ func ParseRawModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) { case strings.Contains(parts[0], "--"): // Looks like possibly punycode, which we don't allow here // to ensure that source addresses are written readably. - return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname %q; internationalized domain names must be given as direct unicode characters, not in punycode", parts[0]) + return Module{}, fmt.Errorf("invalid module registry hostname %q; internationalized domain names must be given as direct unicode characters, not in punycode", parts[0]) default: - return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname %q", parts[0]) + return Module{}, fmt.Errorf("invalid module registry hostname %q", parts[0]) } } if !strings.Contains(host.String(), ".") { - return ModuleSourceRegistry{}, fmt.Errorf("invalid module registry hostname: must contain at least one dot") + return Module{}, fmt.Errorf("invalid module registry hostname: must contain at least one dot") } // Discard the hostname prefix now that we've processed it parts = parts[1:] } - ret := ModuleSourceRegistry{ - PackageAddr: ModuleRegistryPackage{ + ret := Module{ + Package: ModulePackage{ Host: host, }, @@ -88,7 +88,7 @@ func ParseRawModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) { return ret, fmt.Errorf("can't use %q as a module registry host, because it's reserved for installing directly from version control repositories", host) } - if ret.PackageAddr.Namespace, err = parseModuleRegistryName(parts[0]); err != nil { + if ret.Package.Namespace, err = parseModuleRegistryName(parts[0]); err != nil { if strings.Contains(parts[0], ".") { // Seems like the user omitted one of the latter components in // an address with an explicit hostname. @@ -96,10 +96,10 @@ func ParseRawModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) { } return ret, fmt.Errorf("invalid namespace %q: %s", parts[0], err) } - if ret.PackageAddr.Name, err = parseModuleRegistryName(parts[1]); err != nil { + if ret.Package.Name, err = parseModuleRegistryName(parts[1]); err != nil { return ret, fmt.Errorf("invalid module name %q: %s", parts[1], err) } - if ret.PackageAddr.TargetSystem, err = parseModuleRegistryTargetSystem(parts[2]); err != nil { + if ret.Package.TargetSystem, err = parseModuleRegistryTargetSystem(parts[2]); err != nil { if strings.Contains(parts[2], "?") { // The user was trying to include a query string, probably? return ret, fmt.Errorf("module registry addresses may not include a query string portion") @@ -110,6 +110,16 @@ func ParseRawModuleSourceRegistry(raw string) (ModuleSourceRegistry, error) { return ret, nil } +// MustParseModuleSource is a wrapper around ParseModuleSource that panics if +// it returns an error. +func MustParseModuleSource(raw string) (Module) { + mod, err := ParseModuleSource(raw) + if err != nil { + panic(err) + } + return mod +} + // parseModuleRegistryName validates and normalizes a string in either the // "namespace" or "name" position of a module registry source address. func parseModuleRegistryName(given string) (string, error) { @@ -163,11 +173,11 @@ func parseModuleRegistryTargetSystem(given string) (string, error) { // We typically use this longer representation in error message, in case // the inclusion of normally-omitted components is helpful in debugging // unexpected behavior. -func (s ModuleSourceRegistry) String() string { +func (s Module) String() string { if s.Subdir != "" { - return s.PackageAddr.String() + "//" + s.Subdir + return s.Package.String() + "//" + s.Subdir } - return s.PackageAddr.String() + return s.Package.String() } // ForDisplay is similar to String but instead returns a representation of @@ -177,11 +187,11 @@ func (s ModuleSourceRegistry) String() string { // // We typically use this shorter representation in informational messages, // such as the note that we're about to start downloading a package. -func (s ModuleSourceRegistry) ForDisplay() string { +func (s Module) ForDisplay() string { if s.Subdir != "" { - return s.PackageAddr.ForDisplay() + "//" + s.Subdir + return s.Package.ForDisplay() + "//" + s.Subdir } - return s.PackageAddr.ForDisplay() + return s.Package.ForDisplay() } // splitPackageSubdir detects whether the given address string has a diff --git a/module_package.go b/module_package.go index b019bfd..d8ad253 100644 --- a/module_package.go +++ b/module_package.go @@ -6,24 +6,24 @@ import ( svchost "github.com/hashicorp/terraform-svchost" ) -// A ModuleRegistryPackage is an extra indirection over a ModulePackage where +// A ModulePackage is an extra indirection over a ModulePackage where // we use a module registry to translate a more symbolic address (and // associated version constraint given out of band) into a physical source // location. // -// ModuleRegistryPackage is distinct from ModulePackage because they have +// ModulePackage is distinct from ModulePackage because they have // disjoint use-cases: registry package addresses are only used to query a // registry in order to find a real module package address. These being // distinct is intended to help future maintainers more easily follow the // series of steps in the module installer, with the help of the type checker. -type ModuleRegistryPackage struct { +type ModulePackage struct { Host svchost.Hostname Namespace string Name string TargetSystem string } -func (s ModuleRegistryPackage) String() string { +func (s ModulePackage) String() string { // Note: we're using the "display" form of the hostname here because // for our service hostnames "for display" means something different: // it means to render non-ASCII characters directly as Unicode @@ -33,7 +33,7 @@ func (s ModuleRegistryPackage) String() string { return s.Host.ForDisplay() + "/" + s.ForRegistryProtocol() } -func (s ModuleRegistryPackage) ForDisplay() string { +func (s ModulePackage) ForDisplay() string { if s.Host == DefaultModuleRegistryHost { return s.ForRegistryProtocol() } @@ -47,7 +47,7 @@ func (s ModuleRegistryPackage) ForDisplay() string { // This is primarily intended for generating addresses to send to the // registry in question via the registry protocol, since the protocol // skips sending the registry its own hostname as part of identifiers. -func (s ModuleRegistryPackage) ForRegistryProtocol() string { +func (s ModulePackage) ForRegistryProtocol() string { var buf strings.Builder buf.WriteString(s.Namespace) buf.WriteByte('/') diff --git a/module_test.go b/module_test.go index 0ca9155..6ef0a38 100644 --- a/module_test.go +++ b/module_test.go @@ -7,16 +7,16 @@ import ( svchost "github.com/hashicorp/terraform-svchost" ) -func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { +func TestParseRawModule_Simple(t *testing.T) { tests := map[string]struct { input string - want ModuleSourceRegistry + want Module wantErr string }{ "main registry implied": { input: "hashicorp/subnets/cidr", - want: ModuleSourceRegistry{ - PackageAddr: ModuleRegistryPackage{ + want: Module{ + Package: ModulePackage{ Host: svchost.Hostname("registry.terraform.io"), Namespace: "hashicorp", Name: "subnets", @@ -27,8 +27,8 @@ func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { }, "main registry implied, subdir": { input: "hashicorp/subnets/cidr//examples/foo", - want: ModuleSourceRegistry{ - PackageAddr: ModuleRegistryPackage{ + want: Module{ + Package: ModulePackage{ Host: svchost.Hostname("registry.terraform.io"), Namespace: "hashicorp", Name: "subnets", @@ -39,8 +39,8 @@ func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { }, "custom registry": { input: "example.com/awesomecorp/network/happycloud", - want: ModuleSourceRegistry{ - PackageAddr: ModuleRegistryPackage{ + want: Module{ + Package: ModulePackage{ Host: svchost.Hostname("example.com"), Namespace: "awesomecorp", Name: "network", @@ -51,8 +51,8 @@ func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { }, "custom registry, subdir": { input: "example.com/awesomecorp/network/happycloud//examples/foo", - want: ModuleSourceRegistry{ - PackageAddr: ModuleRegistryPackage{ + want: Module{ + Package: ModulePackage{ Host: svchost.Hostname("example.com"), Namespace: "awesomecorp", Name: "network", @@ -65,7 +65,7 @@ func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - addr, err := ParseRawModuleSourceRegistry(test.input) + addr, err := ParseModuleSource(test.input) if test.wantErr != "" { switch { @@ -89,7 +89,7 @@ func TestParseRawModuleSourceRegistry_Simple(t *testing.T) { } -func TestParseRawModuleSourceRegistry(t *testing.T) { +func TestParseRawModule(t *testing.T) { tests := map[string]struct { input string wantString string @@ -209,7 +209,7 @@ func TestParseRawModuleSourceRegistry(t *testing.T) { for name, test := range tests { t.Run(name, func(t *testing.T) { - addr, err := ParseRawModuleSourceRegistry(test.input) + addr, err := ParseModuleSource(test.input) if test.wantErr != "" { switch { @@ -231,7 +231,7 @@ func TestParseRawModuleSourceRegistry(t *testing.T) { if got, want := addr.ForDisplay(), test.wantForDisplay; got != want { t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) } - if got, want := addr.PackageAddr.ForRegistryProtocol(), test.wantForProtocol; got != want { + if got, want := addr.Package.ForRegistryProtocol(), test.wantForProtocol; got != want { t.Errorf("wrong ForRegistryProtocol() result\ngot: %s\nwant: %s", got, want) } }) diff --git a/provider.go b/provider.go index 4dd0a5b..d76c03c 100644 --- a/provider.go +++ b/provider.go @@ -16,9 +16,9 @@ type Provider struct { Hostname svchost.Hostname } -// DefaultRegistryHost is the hostname used for provider addresses that do +// DefaultProviderRegistryHost is the hostname used for provider addresses that do // not have an explicit hostname. -const DefaultRegistryHost = svchost.Hostname("registry.terraform.io") +const DefaultProviderRegistryHost = svchost.Hostname("registry.terraform.io") // BuiltInProviderHost is the pseudo-hostname used for the "built-in" provider // namespace. Built-in provider addresses must also have their namespace set @@ -34,11 +34,18 @@ const BuiltInProviderHost = svchost.Hostname("terraform.io") // special, even if they haven't encountered the concept formally yet. const BuiltInProviderNamespace = "builtin" +// UnknownProviderNamespace is the special string used to indicate +// unknown namespace, e.g. in "aws". This is equivalent to +// LegacyProviderNamespace for <0.12 style address. This namespace +// would never be produced by Terraform itself explicitly, it is +// only an internal placeholder. +const UnknownProviderNamespace = "?" + // 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. +// DefaultProviderRegistryHost because that host owns the mapping from legacy name to +// FQN. This may be produced by Terraform 0.13. const LegacyProviderNamespace = "-" // String returns an FQN string, indended for use in machine-readable output. @@ -56,7 +63,7 @@ func (pt Provider) ForDisplay() string { panic("called ForDisplay on zero-value addrs.Provider") } - if pt.Hostname == DefaultRegistryHost { + if pt.Hostname == DefaultProviderRegistryHost { return pt.Namespace + "/" + pt.Type } return pt.Hostname.ForDisplay() + "/" + pt.Namespace + "/" + pt.Type @@ -75,10 +82,18 @@ func (pt Provider) ForDisplay() string { // 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") + // Legacy provider addresses must always be created via struct + panic("attempt to create legacy provider address using NewProvider; use Provider{} instead") + } + if namespace == UnknownProviderNamespace { + // Provider addresses with unknown namespace must always + // be created via struct + panic("attempt to create provider address with unknown namespace using NewProvider; use Provider{} instead") + } + if namespace == "" { + // This case is already handled by MustParseProviderPart() below, + // but we catch it early to provide more helpful message. + panic("attempt to create provider address with empty namespace") } return Provider{ @@ -88,63 +103,6 @@ func NewProvider(hostname svchost.Hostname, namespace, typeName string) Provider } } -// 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) - } -} - -// NewDefaultProvider returns the default address of a HashiCorp-maintained, -// Registry-hosted provider. -func NewDefaultProvider(name string) Provider { - return Provider{ - Type: MustParseProviderPart(name), - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - } -} - -// 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, - } -} - -// NewLegacyProvider returns a mock address for a provider. -// This will be removed when ProviderType is fully integrated. -func NewLegacyProvider(name string) Provider { - return Provider{ - // 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. - Type: name, - Namespace: LegacyProviderNamespace, - Hostname: DefaultRegistryHost, - } -} - // 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 @@ -167,6 +125,12 @@ func (pt Provider) IsZero() bool { return pt == Provider{} } +// HasKnownNamespace returns true if the provider namespace is known +// (also if it is legacy namespace) +func (pt Provider) HasKnownNamespace() bool { + return pt.Namespace != UnknownProviderNamespace +} + // 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 @@ -201,25 +165,16 @@ func (pt Provider) IsLegacy() bool { panic("called IsLegacy() on zero-value addrs.Provider") } - return pt.Hostname == DefaultRegistryHost && pt.Namespace == LegacyProviderNamespace + return pt.Hostname == DefaultProviderRegistryHost && 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" -} - // Equals returns true if the receiver and other provider have the same attributes. func (pt Provider) Equals(other Provider) bool { return pt == other } -// ParseRawProviderSourceString parses the source attribute and returns a provider. +// ParseProviderSource parses the source attribute and returns a provider. // This is intended primarily to parse the FQN-like strings returned by // terraform-config-inspect. // @@ -230,7 +185,7 @@ func (pt Provider) Equals(other Provider) bool { // // "name"-only format is parsed as -/name (i.e. legacy namespace) // requiring further identification of the namespace via Registry API -func ParseRawProviderSourceString(str string) (Provider, error) { +func ParseProviderSource(str string) (Provider, error) { var ret Provider parts, err := parseSourceStringParts(str) if err != nil { @@ -239,10 +194,14 @@ func ParseRawProviderSourceString(str string) (Provider, error) { name := parts[len(parts)-1] ret.Type = name - ret.Hostname = DefaultRegistryHost + ret.Hostname = DefaultProviderRegistryHost if len(parts) == 1 { - return NewLegacyProvider(name), nil + return Provider{ + Hostname: DefaultProviderRegistryHost, + Namespace: UnknownProviderNamespace, + Type: name, + }, nil } if len(parts) >= 2 { @@ -278,13 +237,13 @@ func ParseRawProviderSourceString(str string) (Provider, error) { ret.Hostname = hn } - if ret.Namespace == LegacyProviderNamespace && ret.Hostname != DefaultRegistryHost { + if ret.Namespace == LegacyProviderNamespace && ret.Hostname != DefaultProviderRegistryHost { // Legacy provider addresses must always be on the default registry // host, because the default registry host decides what actual FQN // each one maps to. return Provider{}, &ParserError{ Summary: "Invalid provider namespace", - Detail: "The legacy provider namespace \"-\" can be used only with hostname " + DefaultRegistryHost.ForDisplay() + ".", + Detail: "The legacy provider namespace \"-\" can be used only with hostname " + DefaultProviderRegistryHost.ForDisplay() + ".", } } @@ -332,30 +291,6 @@ func ParseRawProviderSourceString(str string) (Provider, error) { return ret, nil } -// ParseAndInferProviderSourceString 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 -// -// "name" format is assumed to be hashicorp/name -func ParseAndInferProviderSourceString(str string) (Provider, error) { - var ret Provider - parts, err := parseSourceStringParts(str) - if err != nil { - return ret, err - } - - if len(parts) == 1 { - return NewDefaultProvider(parts[0]), nil - } - - return ParseRawProviderSourceString(str) -} - func parseSourceStringParts(str string) ([]string, error) { // split the source string into individual components parts := strings.Split(str, "/") @@ -390,16 +325,6 @@ func parseSourceStringParts(str string) ([]string, error) { return parts, nil } -// MustParseRawProviderSourceString is a wrapper around ParseRawProviderSourceString that panics if -// it returns an error. -func MustParseRawProviderSourceString(str string) Provider { - result, err := ParseRawProviderSourceString(str) - if err != nil { - panic(err) - } - return result -} - // 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. @@ -468,15 +393,3 @@ func MustParseProviderPart(given string) string { } return result } - -// 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 -} diff --git a/provider_test.go b/provider_test.go index 39260cf..4f4dbaa 100644 --- a/provider_test.go +++ b/provider_test.go @@ -15,18 +15,18 @@ func TestProviderString(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "hashicorp", }, - NewDefaultProvider("test").String(), + NewProvider(DefaultProviderRegistryHost, "hashicorp", "test").String(), }, { Provider{ Type: "test-beta", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "hashicorp", }, - NewDefaultProvider("test-beta").String(), + NewProvider(DefaultProviderRegistryHost, "hashicorp", "test-beta").String(), }, { Provider{ @@ -39,10 +39,10 @@ func TestProviderString(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "othercorp", }, - DefaultRegistryHost.ForDisplay() + "/othercorp/test", + DefaultProviderRegistryHost.ForDisplay() + "/othercorp/test", }, } @@ -62,7 +62,7 @@ func TestProviderLegacyString(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: LegacyProviderNamespace, }, "test", @@ -93,7 +93,7 @@ func TestProviderDisplay(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "hashicorp", }, "hashicorp/test", @@ -109,7 +109,7 @@ func TestProviderDisplay(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "othercorp", }, "othercorp/test", @@ -132,45 +132,6 @@ func TestProviderDisplay(t *testing.T) { } } -func TestProviderIsDefault(t *testing.T) { - tests := []struct { - Input Provider - Want bool - }{ - { - Provider{ - Type: "test", - Hostname: DefaultRegistryHost, - Namespace: "hashicorp", - }, - true, - }, - { - Provider{ - Type: "test", - Hostname: "registry.terraform.com", - Namespace: "hashicorp", - }, - false, - }, - { - Provider{ - Type: "test", - Hostname: DefaultRegistryHost, - Namespace: "othercorp", - }, - false, - }, - } - - for _, test := range tests { - got := test.Input.IsDefault() - if got != test.Want { - t.Errorf("wrong result for %s\n", test.Input.String()) - } - } -} - func TestProviderIsBuiltIn(t *testing.T) { tests := []struct { Input Provider @@ -203,7 +164,7 @@ func TestProviderIsBuiltIn(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: BuiltInProviderNamespace, }, false, @@ -211,7 +172,7 @@ func TestProviderIsBuiltIn(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "hashicorp", }, false, @@ -227,7 +188,7 @@ func TestProviderIsBuiltIn(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "othercorp", }, false, @@ -250,7 +211,7 @@ func TestProviderIsLegacy(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: LegacyProviderNamespace, }, true, @@ -266,7 +227,7 @@ func TestProviderIsLegacy(t *testing.T) { { Provider{ Type: "test", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, Namespace: "hashicorp", }, false, @@ -281,193 +242,7 @@ func TestProviderIsLegacy(t *testing.T) { } } -func TestParseAndInferProviderSourceString(t *testing.T) { - tests := map[string]struct { - Want Provider - Err bool - }{ - "registry.terraform.io/hashicorp/aws": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "registry.Terraform.io/HashiCorp/AWS": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "terraform.io/builtin/terraform": { - Provider{ - Type: "terraform", - Namespace: BuiltInProviderNamespace, - Hostname: BuiltInProviderHost, - }, - false, - }, - "terraform": { - Provider{ - Type: "terraform", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "hashicorp/aws": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "HashiCorp/AWS": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "aws": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "AWS": { - Provider{ - Type: "aws", - Namespace: "hashicorp", - Hostname: DefaultRegistryHost, - }, - false, - }, - "example.com/foo-bar/baz-boop": { - Provider{ - Type: "baz-boop", - Namespace: "foo-bar", - Hostname: svchost.Hostname("example.com"), - }, - false, - }, - "foo-bar/baz-boop": { - Provider{ - Type: "baz-boop", - Namespace: "foo-bar", - Hostname: DefaultRegistryHost, - }, - false, - }, - "localhost:8080/foo/bar": { - Provider{ - Type: "bar", - Namespace: "foo", - Hostname: svchost.Hostname("localhost:8080"), - }, - false, - }, - "example.com/too/many/parts/here": { - Provider{}, - true, - }, - "/too///many//slashes": { - Provider{}, - true, - }, - "///": { - Provider{}, - true, - }, - "/ / /": { // empty strings - Provider{}, - true, - }, - "badhost!/hashicorp/aws": { - Provider{}, - true, - }, - "example.com/badnamespace!/aws": { - Provider{}, - true, - }, - "example.com/bad--namespace/aws": { - Provider{}, - true, - }, - "example.com/-badnamespace/aws": { - Provider{}, - true, - }, - "example.com/badnamespace-/aws": { - Provider{}, - true, - }, - "example.com/bad.namespace/aws": { - Provider{}, - true, - }, - "example.com/hashicorp/badtype!": { - Provider{}, - true, - }, - "example.com/hashicorp/bad--type": { - Provider{}, - true, - }, - "example.com/hashicorp/-badtype": { - Provider{}, - true, - }, - "example.com/hashicorp/badtype-": { - Provider{}, - true, - }, - "example.com/hashicorp/bad.type": { - Provider{}, - true, - }, - - // We forbid the terraform- prefix both because it's redundant to - // include "terraform" in a Terraform provider name and because we use - // the longer prefix terraform-provider- to hint for users who might be - // accidentally using the git repository name or executable file name - // instead of the provider type. - "example.com/hashicorp/terraform-provider-bad": { - Provider{}, - true, - }, - "example.com/hashicorp/terraform-bad": { - Provider{}, - true, - }, - } - - for name, test := range tests { - got, err := ParseAndInferProviderSourceString(name) - if diff := cmp.Diff(test.Want, got); diff != "" { - t.Errorf("mismatch (%q): %s", name, diff) - } - if err != nil { - if test.Err == false { - t.Errorf("got error: %s, expected success", err) - } - } else { - if test.Err { - t.Errorf("got success, expected error") - } - } - } -} - -func TestParseRawProviderSourceString(t *testing.T) { +func TestParseProviderSource(t *testing.T) { tests := map[string]struct { Want Provider Err bool @@ -476,7 +251,7 @@ func TestParseRawProviderSourceString(t *testing.T) { Provider{ Type: "aws", Namespace: "hashicorp", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -484,7 +259,7 @@ func TestParseRawProviderSourceString(t *testing.T) { Provider{ Type: "aws", Namespace: "hashicorp", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -503,8 +278,8 @@ func TestParseRawProviderSourceString(t *testing.T) { "terraform": { Provider{ Type: "terraform", - Namespace: LegacyProviderNamespace, - Hostname: DefaultRegistryHost, + Namespace: UnknownProviderNamespace, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -512,7 +287,7 @@ func TestParseRawProviderSourceString(t *testing.T) { Provider{ Type: "aws", Namespace: "hashicorp", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -520,23 +295,23 @@ func TestParseRawProviderSourceString(t *testing.T) { Provider{ Type: "aws", Namespace: "hashicorp", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, }, false, }, "aws": { Provider{ Type: "aws", - Namespace: LegacyProviderNamespace, - Hostname: DefaultRegistryHost, + Namespace: UnknownProviderNamespace, + Hostname: DefaultProviderRegistryHost, }, false, }, "AWS": { Provider{ Type: "aws", - Namespace: LegacyProviderNamespace, - Hostname: DefaultRegistryHost, + Namespace: UnknownProviderNamespace, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -552,7 +327,7 @@ func TestParseRawProviderSourceString(t *testing.T) { Provider{ Type: "baz-boop", Namespace: "foo-bar", - Hostname: DefaultRegistryHost, + Hostname: DefaultProviderRegistryHost, }, false, }, @@ -641,7 +416,7 @@ func TestParseRawProviderSourceString(t *testing.T) { } for name, test := range tests { - got, err := ParseRawProviderSourceString(name) + got, err := ParseProviderSource(name) if diff := cmp.Diff(test.Want, got); diff != "" { t.Errorf("mismatch (%q): %s", name, diff) } @@ -743,22 +518,22 @@ func TestProviderEquals(t *testing.T) { Want bool }{ { - NewProvider(DefaultRegistryHost, "foo", "test"), - NewProvider(DefaultRegistryHost, "foo", "test"), + NewProvider(DefaultProviderRegistryHost, "foo", "test"), + NewProvider(DefaultProviderRegistryHost, "foo", "test"), true, }, { - NewProvider(DefaultRegistryHost, "foo", "test"), - NewProvider(DefaultRegistryHost, "bar", "test"), + NewProvider(DefaultProviderRegistryHost, "foo", "test"), + NewProvider(DefaultProviderRegistryHost, "bar", "test"), false, }, { - NewProvider(DefaultRegistryHost, "foo", "test"), - NewProvider(DefaultRegistryHost, "foo", "my-test"), + NewProvider(DefaultProviderRegistryHost, "foo", "test"), + NewProvider(DefaultProviderRegistryHost, "foo", "my-test"), false, }, { - NewProvider(DefaultRegistryHost, "foo", "test"), + NewProvider(DefaultProviderRegistryHost, "foo", "test"), NewProvider("example.com", "foo", "test"), false, },