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 }