opentofu/internal/getproviders/multi_source.go

257 lines
8.8 KiB
Go
Raw Normal View History

package getproviders
import (
"context"
"fmt"
"strings"
svchost "github.com/hashicorp/terraform-svchost"
"github.com/hashicorp/terraform/internal/addrs"
)
// MultiSource is a Source that wraps a series of other sources and combines
// their sets of available providers and provider versions.
//
// A MultiSource consists of a sequence of selectors that each specify an
// underlying source to query and a set of matching patterns to decide which
// providers can be retrieved from which sources. If multiple selectors find
// a given provider version then the earliest one in the sequence takes
// priority for deciding the package metadata for the provider.
//
// For underlying sources that make network requests, consider wrapping each
// one in a MemoizeSource so that availability information retrieved in
// AvailableVersions can be reused in PackageMeta.
type MultiSource []MultiSourceSelector
var _ Source = MultiSource(nil)
// AvailableVersions retrieves all of the versions of the given provider
// that are available across all of the underlying selectors, while respecting
// each selector's matching patterns.
func (s MultiSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
if len(s) == 0 { // Easy case: there can be no available versions
return nil, nil, nil
}
// We will return the union of all versions reported by the nested
// sources that have matching patterns that accept the given provider.
vs := make(map[Version]struct{})
var registryError bool
var warnings []string
for _, selector := range s {
if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns
}
thisSourceVersions, warningsResp, err := selector.Source.AvailableVersions(ctx, provider)
switch err.(type) {
case nil:
// okay
case ErrRegistryProviderNotKnown:
registryError = true
continue // ignore, then
case ErrProviderNotFound:
continue // ignore, then
default:
return nil, nil, err
}
for _, v := range thisSourceVersions {
vs[v] = struct{}{}
}
if len(warningsResp) > 0 {
warnings = append(warnings, warningsResp...)
}
}
if len(vs) == 0 {
if registryError {
return nil, nil, ErrRegistryProviderNotKnown{provider}
} else {
return nil, nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
}
}
ret := make(VersionList, 0, len(vs))
for v := range vs {
ret = append(ret, v)
}
ret.Sort()
return ret, warnings, nil
}
// PackageMeta retrieves the package metadata for the requested provider package
// from the first selector that indicates availability of it.
func (s MultiSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
if len(s) == 0 { // Easy case: no providers exist at all
return PackageMeta{}, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
}
for _, selector := range s {
if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns
}
meta, err := selector.Source.PackageMeta(ctx, provider, version, target)
switch err.(type) {
case nil:
return meta, nil
case ErrProviderNotFound, ErrRegistryProviderNotKnown, ErrPlatformNotSupported:
continue // ignore, then
default:
return PackageMeta{}, err
}
}
// If we fall out here then none of the sources have the requested
// package.
return PackageMeta{}, ErrPlatformNotSupported{
Provider: provider,
Version: version,
Platform: target,
}
}
// MultiSourceSelector is an element of the source selection configuration on
// MultiSource. A MultiSource has zero or more of these to configure which
// underlying sources it should consult for a given provider.
type MultiSourceSelector struct {
// Source is the underlying source that this selector applies to.
Source Source
// Include and Exclude are sets of provider matching patterns that
// together define which providers are eligible to be potentially
// installed from the corresponding Source.
Include, Exclude MultiSourceMatchingPatterns
}
// MultiSourceMatchingPatterns is a set of patterns that together define a
// set of providers by matching on the segments of the provider FQNs.
//
// The Provider address values in a MultiSourceMatchingPatterns are special in
// that any of Hostname, Namespace, or Type can be getproviders.Wildcard
// to indicate that any concrete value is permitted for that segment.
type MultiSourceMatchingPatterns []addrs.Provider
// ParseMultiSourceMatchingPatterns parses a slice of strings containing the
// string form of provider matching patterns and, if all the given strings are
// valid, returns the corresponding, normalized, MultiSourceMatchingPatterns
// value.
func ParseMultiSourceMatchingPatterns(strs []string) (MultiSourceMatchingPatterns, error) {
if len(strs) == 0 {
return nil, nil
}
ret := make(MultiSourceMatchingPatterns, len(strs))
for i, str := range strs {
parts := strings.Split(str, "/")
if len(parts) < 2 || len(parts) > 3 {
return nil, fmt.Errorf("invalid provider matching pattern %q: must have either two or three slash-separated segments", str)
}
host := defaultRegistryHost
explicitHost := len(parts) == 3
if explicitHost {
givenHost := parts[0]
if givenHost == "*" {
host = svchost.Hostname(Wildcard)
} else {
normalHost, err := svchost.ForComparison(givenHost)
if err != nil {
return nil, fmt.Errorf("invalid hostname in provider matching pattern %q: %s", str, err)
}
// The remaining code below deals only with the namespace/type portions.
host = normalHost
}
parts = parts[1:]
}
pType, err := normalizeProviderNameOrWildcard(parts[1])
if err != nil {
return nil, fmt.Errorf("invalid provider type %q in provider matching pattern %q: must either be the wildcard * or a provider type name", parts[1], str)
}
namespace, err := normalizeProviderNameOrWildcard(parts[0])
if err != nil {
return nil, fmt.Errorf("invalid registry namespace %q in provider matching pattern %q: must either be the wildcard * or a literal namespace", parts[1], str)
}
ret[i] = addrs.Provider{
Hostname: host,
Namespace: namespace,
Type: pType,
}
if ret[i].Hostname == svchost.Hostname(Wildcard) && !(ret[i].Namespace == Wildcard && ret[i].Type == Wildcard) {
return nil, fmt.Errorf("invalid provider matching pattern %q: hostname can be a wildcard only if both namespace and provider type are also wildcards", str)
}
if ret[i].Namespace == Wildcard && ret[i].Type != Wildcard {
return nil, fmt.Errorf("invalid provider matching pattern %q: namespace can be a wildcard only if the provider type is also a wildcard", str)
}
}
return ret, nil
}
internal/getproviders: LookupLegacyProvider This is a temporary helper so that we can potentially ship the new provider installer without making a breaking change by relying on the old default namespace lookup API on the default registry to find a proper FQN for a legacy provider provider address during installation. If it's given a non-legacy provider address then it just returns the given address verbatim, so any codepath using it will also correctly handle explicit full provider addresses. This also means it will automatically self-disable once we stop using addrs.NewLegacyProvider in the config loader, because there will therefore no longer be any legacy provider addresses in the config to resolve. (They'll be "default" provider addresses instead, assumed to be under registry.terraform.io/hashicorp/* ) It's not decided yet whether we will actually introduce the new provider in a minor release, but even if we don't this API function will likely be useful for a hypothetical automatic upgrade tool to introduce explicit full provider addresses into existing modules that currently rely on the equivalent to this lookup in the current provider installer. This is dead code for now, but my intent is that it would either be called as part of new provider installation to produce an address suitable to pass to Source.AvailableVersions, or it would be called from the aforementioned hypothetical upgrade tool. Whatever happens, these functions can be removed no later than one whole major release after the new provider installer is introduced, when everyone's had the opportunity to update their legacy unqualified addresses.
2020-01-21 18:01:49 -06:00
// CanHandleProvider returns true if and only if the given provider address
// is both included by the selector's include patterns and _not_ excluded
// by its exclude patterns.
//
// The absense of any include patterns is treated the same as a pattern
// that matches all addresses. Exclusions take priority over inclusions.
func (s MultiSourceSelector) CanHandleProvider(addr addrs.Provider) bool {
switch {
case s.Exclude.MatchesProvider(addr):
return false
case len(s.Include) > 0:
return s.Include.MatchesProvider(addr)
default:
return true
}
}
// MatchesProvider tests whether the receiving matching patterns match with
// the given concrete provider address.
func (ps MultiSourceMatchingPatterns) MatchesProvider(addr addrs.Provider) bool {
for _, pattern := range ps {
hostMatch := (pattern.Hostname == svchost.Hostname(Wildcard) || pattern.Hostname == addr.Hostname)
namespaceMatch := (pattern.Namespace == Wildcard || pattern.Namespace == addr.Namespace)
typeMatch := (pattern.Type == Wildcard || pattern.Type == addr.Type)
if hostMatch && namespaceMatch && typeMatch {
return true
}
}
return false
}
// Wildcard is a string value representing a wildcard element in the Include
// and Exclude patterns used with MultiSource. It is not valid to use Wildcard
// anywhere else.
const Wildcard string = "*"
// We'll read the default registry host from over in the addrs package, to
// avoid duplicating it. A "default" provider uses the default registry host
// by definition.
var defaultRegistryHost = addrs.DefaultProviderRegistryHost
func normalizeProviderNameOrWildcard(s string) (string, error) {
if s == Wildcard {
return s, nil
}
return addrs.ParseProviderPart(s)
}
func (s MultiSource) ForDisplay(provider addrs.Provider) string {
return strings.Join(s.sourcesForProvider(provider), "\n")
}
// sourcesForProvider returns a list of source display strings configured for a
// given provider, taking into account any `Exclude` statements.
func (s MultiSource) sourcesForProvider(provider addrs.Provider) []string {
ret := make([]string, 0)
for _, selector := range s {
if !selector.CanHandleProvider(provider) {
continue // doesn't match the given patterns
}
ret = append(ret, selector.Source.ForDisplay(provider))
}
return ret
}