github.com/opentofu/opentofu@v1.7.1/internal/getproviders/multi_source.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package getproviders
     7  
     8  import (
     9  	"context"
    10  	"fmt"
    11  	"strings"
    12  
    13  	svchost "github.com/hashicorp/terraform-svchost"
    14  
    15  	"github.com/opentofu/opentofu/internal/addrs"
    16  )
    17  
    18  // MultiSource is a Source that wraps a series of other sources and combines
    19  // their sets of available providers and provider versions.
    20  //
    21  // A MultiSource consists of a sequence of selectors that each specify an
    22  // underlying source to query and a set of matching patterns to decide which
    23  // providers can be retrieved from which sources. If multiple selectors find
    24  // a given provider version then the earliest one in the sequence takes
    25  // priority for deciding the package metadata for the provider.
    26  //
    27  // For underlying sources that make network requests, consider wrapping each
    28  // one in a MemoizeSource so that availability information retrieved in
    29  // AvailableVersions can be reused in PackageMeta.
    30  type MultiSource []MultiSourceSelector
    31  
    32  var _ Source = MultiSource(nil)
    33  
    34  // AvailableVersions retrieves all of the versions of the given provider
    35  // that are available across all of the underlying selectors, while respecting
    36  // each selector's matching patterns.
    37  func (s MultiSource) AvailableVersions(ctx context.Context, provider addrs.Provider) (VersionList, Warnings, error) {
    38  	if len(s) == 0 { // Easy case: there can be no available versions
    39  		return nil, nil, nil
    40  	}
    41  
    42  	// We will return the union of all versions reported by the nested
    43  	// sources that have matching patterns that accept the given provider.
    44  	vs := make(map[Version]struct{})
    45  	var registryError bool
    46  	var warnings []string
    47  	for _, selector := range s {
    48  		if !selector.CanHandleProvider(provider) {
    49  			continue // doesn't match the given patterns
    50  		}
    51  		thisSourceVersions, warningsResp, err := selector.Source.AvailableVersions(ctx, provider)
    52  		switch err.(type) {
    53  		case nil:
    54  		// okay
    55  		case ErrRegistryProviderNotKnown:
    56  			registryError = true
    57  			continue // ignore, then
    58  		case ErrProviderNotFound:
    59  			continue // ignore, then
    60  		default:
    61  			return nil, nil, err
    62  		}
    63  		for _, v := range thisSourceVersions {
    64  			vs[v] = struct{}{}
    65  		}
    66  		if len(warningsResp) > 0 {
    67  			warnings = append(warnings, warningsResp...)
    68  		}
    69  	}
    70  
    71  	if len(vs) == 0 {
    72  		if registryError {
    73  			return nil, nil, ErrRegistryProviderNotKnown{provider}
    74  		} else {
    75  			return nil, nil, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
    76  		}
    77  	}
    78  	ret := make(VersionList, 0, len(vs))
    79  	for v := range vs {
    80  		ret = append(ret, v)
    81  	}
    82  	ret.Sort()
    83  
    84  	return ret, warnings, nil
    85  }
    86  
    87  // PackageMeta retrieves the package metadata for the requested provider package
    88  // from the first selector that indicates availability of it.
    89  func (s MultiSource) PackageMeta(ctx context.Context, provider addrs.Provider, version Version, target Platform) (PackageMeta, error) {
    90  	if len(s) == 0 { // Easy case: no providers exist at all
    91  		return PackageMeta{}, ErrProviderNotFound{provider, s.sourcesForProvider(provider)}
    92  	}
    93  
    94  	for _, selector := range s {
    95  		if !selector.CanHandleProvider(provider) {
    96  			continue // doesn't match the given patterns
    97  		}
    98  		meta, err := selector.Source.PackageMeta(ctx, provider, version, target)
    99  		switch err.(type) {
   100  		case nil:
   101  			return meta, nil
   102  		case ErrProviderNotFound, ErrRegistryProviderNotKnown, ErrPlatformNotSupported:
   103  			continue // ignore, then
   104  		default:
   105  			return PackageMeta{}, err
   106  		}
   107  	}
   108  
   109  	// If we fall out here then none of the sources have the requested
   110  	// package.
   111  	return PackageMeta{}, ErrPlatformNotSupported{
   112  		Provider: provider,
   113  		Version:  version,
   114  		Platform: target,
   115  	}
   116  }
   117  
   118  // MultiSourceSelector is an element of the source selection configuration on
   119  // MultiSource. A MultiSource has zero or more of these to configure which
   120  // underlying sources it should consult for a given provider.
   121  type MultiSourceSelector struct {
   122  	// Source is the underlying source that this selector applies to.
   123  	Source Source
   124  
   125  	// Include and Exclude are sets of provider matching patterns that
   126  	// together define which providers are eligible to be potentially
   127  	// installed from the corresponding Source.
   128  	Include, Exclude MultiSourceMatchingPatterns
   129  }
   130  
   131  // MultiSourceMatchingPatterns is a set of patterns that together define a
   132  // set of providers by matching on the segments of the provider FQNs.
   133  //
   134  // The Provider address values in a MultiSourceMatchingPatterns are special in
   135  // that any of Hostname, Namespace, or Type can be getproviders.Wildcard
   136  // to indicate that any concrete value is permitted for that segment.
   137  type MultiSourceMatchingPatterns []addrs.Provider
   138  
   139  // ParseMultiSourceMatchingPatterns parses a slice of strings containing the
   140  // string form of provider matching patterns and, if all the given strings are
   141  // valid, returns the corresponding, normalized, MultiSourceMatchingPatterns
   142  // value.
   143  func ParseMultiSourceMatchingPatterns(strs []string) (MultiSourceMatchingPatterns, error) {
   144  	if len(strs) == 0 {
   145  		return nil, nil
   146  	}
   147  
   148  	ret := make(MultiSourceMatchingPatterns, len(strs))
   149  	for i, str := range strs {
   150  		parts := strings.Split(str, "/")
   151  		if len(parts) < 2 || len(parts) > 3 {
   152  			return nil, fmt.Errorf("invalid provider matching pattern %q: must have either two or three slash-separated segments", str)
   153  		}
   154  		host := defaultRegistryHost
   155  		explicitHost := len(parts) == 3
   156  		if explicitHost {
   157  			givenHost := parts[0]
   158  			if givenHost == "*" {
   159  				host = svchost.Hostname(Wildcard)
   160  			} else {
   161  				normalHost, err := svchost.ForComparison(givenHost)
   162  				if err != nil {
   163  					return nil, fmt.Errorf("invalid hostname in provider matching pattern %q: %w", str, err)
   164  				}
   165  
   166  				// The remaining code below deals only with the namespace/type portions.
   167  				host = normalHost
   168  			}
   169  
   170  			parts = parts[1:]
   171  		}
   172  
   173  		pType, err := normalizeProviderNameOrWildcard(parts[1])
   174  		if err != nil {
   175  			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)
   176  		}
   177  		namespace, err := normalizeProviderNameOrWildcard(parts[0])
   178  		if err != nil {
   179  			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)
   180  		}
   181  
   182  		ret[i] = addrs.Provider{
   183  			Hostname:  host,
   184  			Namespace: namespace,
   185  			Type:      pType,
   186  		}
   187  
   188  		if ret[i].Hostname == svchost.Hostname(Wildcard) && !(ret[i].Namespace == Wildcard && ret[i].Type == Wildcard) {
   189  			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)
   190  		}
   191  		if ret[i].Namespace == Wildcard && ret[i].Type != Wildcard {
   192  			return nil, fmt.Errorf("invalid provider matching pattern %q: namespace can be a wildcard only if the provider type is also a wildcard", str)
   193  		}
   194  	}
   195  	return ret, nil
   196  }
   197  
   198  // CanHandleProvider returns true if and only if the given provider address
   199  // is both included by the selector's include patterns and _not_ excluded
   200  // by its exclude patterns.
   201  //
   202  // The absense of any include patterns is treated the same as a pattern
   203  // that matches all addresses. Exclusions take priority over inclusions.
   204  func (s MultiSourceSelector) CanHandleProvider(addr addrs.Provider) bool {
   205  	switch {
   206  	case s.Exclude.MatchesProvider(addr):
   207  		return false
   208  	case len(s.Include) > 0:
   209  		return s.Include.MatchesProvider(addr)
   210  	default:
   211  		return true
   212  	}
   213  }
   214  
   215  // MatchesProvider tests whether the receiving matching patterns match with
   216  // the given concrete provider address.
   217  func (ps MultiSourceMatchingPatterns) MatchesProvider(addr addrs.Provider) bool {
   218  	for _, pattern := range ps {
   219  		hostMatch := (pattern.Hostname == svchost.Hostname(Wildcard) || pattern.Hostname == addr.Hostname)
   220  		namespaceMatch := (pattern.Namespace == Wildcard || pattern.Namespace == addr.Namespace)
   221  		typeMatch := (pattern.Type == Wildcard || pattern.Type == addr.Type)
   222  		if hostMatch && namespaceMatch && typeMatch {
   223  			return true
   224  		}
   225  	}
   226  	return false
   227  }
   228  
   229  // Wildcard is a string value representing a wildcard element in the Include
   230  // and Exclude patterns used with MultiSource. It is not valid to use Wildcard
   231  // anywhere else.
   232  const Wildcard string = "*"
   233  
   234  // We'll read the default registry host from over in the addrs package, to
   235  // avoid duplicating it. A "default" provider uses the default registry host
   236  // by definition.
   237  var defaultRegistryHost = addrs.DefaultProviderRegistryHost
   238  
   239  func normalizeProviderNameOrWildcard(s string) (string, error) {
   240  	if s == Wildcard {
   241  		return s, nil
   242  	}
   243  	return addrs.ParseProviderPart(s)
   244  }
   245  
   246  func (s MultiSource) ForDisplay(provider addrs.Provider) string {
   247  	return strings.Join(s.sourcesForProvider(provider), "\n")
   248  }
   249  
   250  // sourcesForProvider returns a list of source display strings configured for a
   251  // given provider, taking into account any `Exclude` statements.
   252  func (s MultiSource) sourcesForProvider(provider addrs.Provider) []string {
   253  	ret := make([]string, 0)
   254  	for _, selector := range s {
   255  		if !selector.CanHandleProvider(provider) {
   256  			continue // doesn't match the given patterns
   257  		}
   258  		ret = append(ret, selector.Source.ForDisplay(provider))
   259  	}
   260  	return ret
   261  }