mirror of
https://github.com/opentofu/opentofu.git
synced 2024-12-30 10:47:14 -06:00
0b734a2803
In earlier commits we started to make the installation codepath context-aware so that it could be canceled in the event of a SIGINT, but we didn't complete wiring that through the API of the getproviders package. Here we make the getproviders.Source interface methods, along with some other functions that can make network requests, take a context.Context argument and act appropriately if that context is cancelled. The main providercache.Installer.EnsureProviderVersions method now also has some context-awareness so that it can abort its work early if its context reports any sort of error. That avoids waiting for the process to wind through all of the remaining iterations of the various loops, logging each request failure separately, and instead returns just a single aggregate "canceled" error. We can then set things up in the "terraform init" and "terraform providers mirror" commands so that the context will be cancelled if we get an interrupt signal, allowing provider installation to abort early while still atomically completing any local-side effects that may have started.
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/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
|
|
}
|