opentofu/internal/addrs/provider.go

209 lines
8.2 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package addrs
import (
"github.com/hashicorp/hcl/v2"
tfaddr "github.com/hashicorp/terraform-registry-address"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/opentofu/opentofu/internal/tfdiags"
)
// Provider encapsulates a single provider type. In the future this will be
// extended to include additional fields including Namespace and SourceHost
type Provider = tfaddr.Provider
// DefaultProviderRegistryHost is the hostname used for provider addresses that do
// not have an explicit hostname.
const DefaultProviderRegistryHost = tfaddr.DefaultProviderRegistryHost
// 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 = tfaddr.BuiltInProviderHost
// 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 = tfaddr.BuiltInProviderNamespace
// 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 = tfaddr.LegacyProviderNamespace
func IsDefaultProvider(addr Provider) bool {
return addr.Hostname == DefaultProviderRegistryHost && addr.Namespace == "hashicorp"
}
// 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 {
return tfaddr.NewProvider(hostname, namespace, typeName)
}
// 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 OpenTofu 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 OpenTofu 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 tfaddr.Provider{
Type: MustParseProviderPart(name),
Namespace: "hashicorp",
Hostname: DefaultProviderRegistryHost,
}
}
// NewBuiltInProvider returns the address of a "built-in" provider. See
// the docs for Provider.IsBuiltIn for more information.
func NewBuiltInProvider(name string) Provider {
return tfaddr.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: DefaultProviderRegistryHost,
}
}
// ParseProviderSourceString parses a value of the form expected in the "source"
// argument of a required_providers entry and returns the corresponding
// fully-qualified provider address. 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) (tfaddr.Provider, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
ret, err := tfaddr.ParseProviderSource(str)
if pe, ok := err.(*tfaddr.ParserError); ok {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: pe.Summary,
Detail: pe.Detail,
})
return ret, diags
}
if !ret.HasKnownNamespace() {
ret.Namespace = "hashicorp"
}
return ret, nil
}
// 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
}
// 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) {
return tfaddr.ParseProviderPart(given)
}
// 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
}
// 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
}