257 lines
8.8 KiB
Go
257 lines
8.8 KiB
Go
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
|
|
}
|
|
|
|
// 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.DefaultRegistryHost
|
|
|
|
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
|
|
}
|