github.com/graywolf-at-work-2/terraform-vendor@v1.4.5/internal/getproviders/didyoumean.go (about) 1 package getproviders 2 3 import ( 4 "context" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 "path" 11 12 "github.com/hashicorp/go-retryablehttp" 13 svchost "github.com/hashicorp/terraform-svchost" 14 "github.com/hashicorp/terraform/internal/addrs" 15 ) 16 17 // MissingProviderSuggestion takes a provider address that failed installation 18 // due to the remote registry reporting that it didn't exist, and attempts 19 // to find another provider that the user might have meant to select. 20 // 21 // If the result is equal to the given address then that indicates that there 22 // is no suggested alternative to offer, either because the function 23 // successfully determined there is no recorded alternative or because the 24 // lookup failed somehow. We don't consider a failure to find a suggestion 25 // as an installation failure, because the caller should already be reporting 26 // that the provider didn't exist anyway and this is only extra context for 27 // that error message. 28 // 29 // The result of this is a best effort, so any UI presenting it should be 30 // careful to give it only as a possibility and not necessarily a suitable 31 // replacement for the given provider. 32 // 33 // In practice today this function only knows how to suggest alternatives for 34 // "default" providers, which is to say ones that are in the hashicorp 35 // namespace in the Terraform registry. It will always return no result for 36 // any other provider. That might change in future if we introduce other ways 37 // to discover provider suggestions. 38 // 39 // If the given context is cancelled then this function might not return a 40 // renaming suggestion even if one would've been available for a completed 41 // request. 42 func MissingProviderSuggestion(ctx context.Context, addr addrs.Provider, source Source, reqs Requirements) addrs.Provider { 43 if !addrs.IsDefaultProvider(addr) { 44 return addr 45 } 46 47 // Before possibly looking up legacy naming, see if the user has another provider 48 // named in their requirements that is of the same type, and offer that 49 // as a suggestion 50 for req := range reqs { 51 if req != addr && req.Type == addr.Type { 52 return req 53 } 54 } 55 56 // Our strategy here, for a default provider, is to use the default 57 // registry's special API for looking up "legacy" providers and try looking 58 // for a legacy provider whose type name matches the type of the given 59 // provider. This should then find a suitable answer for any provider 60 // that was originally auto-installable in v0.12 and earlier but moved 61 // into a non-default namespace as part of introducing the hierarchical 62 // provider namespace. 63 // 64 // To achieve that, we need to find the direct registry client in 65 // particular from the given source, because that is the only Source 66 // implementation that can actually handle a legacy provider lookup. 67 regSource := findLegacyProviderLookupSource(addr.Hostname, source) 68 if regSource == nil { 69 // If there's no direct registry source in the installation config 70 // then we can't provide a renaming suggestion. 71 return addr 72 } 73 74 defaultNS, redirectNS, err := regSource.lookupLegacyProviderNamespace(ctx, addr.Hostname, addr.Type) 75 if err != nil { 76 return addr 77 } 78 79 switch { 80 case redirectNS != "": 81 return addrs.Provider{ 82 Hostname: addr.Hostname, 83 Namespace: redirectNS, 84 Type: addr.Type, 85 } 86 default: 87 return addrs.Provider{ 88 Hostname: addr.Hostname, 89 Namespace: defaultNS, 90 Type: addr.Type, 91 } 92 } 93 } 94 95 // findLegacyProviderLookupSource tries to find a *RegistrySource that can talk 96 // to the given registry host in the given Source. It might be given directly, 97 // or it might be given indirectly via a MultiSource where the selector 98 // includes a wildcard for registry.terraform.io. 99 // 100 // Returns nil if the given source does not have any configured way to talk 101 // directly to the given host. 102 // 103 // If the given source contains multiple sources that can talk to the given 104 // host directly, the first one in the sequence takes preference. In practice 105 // it's pointless to have two direct installation sources that match the same 106 // hostname anyway, so this shouldn't arise in normal use. 107 func findLegacyProviderLookupSource(host svchost.Hostname, source Source) *RegistrySource { 108 switch source := source.(type) { 109 110 case *RegistrySource: 111 // Easy case: the source is a registry source directly, and so we'll 112 // just use it. 113 return source 114 115 case *MemoizeSource: 116 // Also easy: the source is a memoize wrapper, so defer to its 117 // underlying source. 118 return findLegacyProviderLookupSource(host, source.underlying) 119 120 case MultiSource: 121 // Trickier case: if it's a multisource then we need to scan over 122 // its selectors until we find one that is a *RegistrySource _and_ 123 // that is configured to accept arbitrary providers from the 124 // given hostname. 125 126 // For our matching purposes we'll use an address that would not be 127 // valid as a real provider FQN and thus can only match a selector 128 // that has no filters at all or a selector that wildcards everything 129 // except the hostname, like "registry.terraform.io/*/*" 130 matchAddr := addrs.Provider{ 131 Hostname: host, 132 // Other fields are intentionally left empty, to make this invalid 133 // as a specific provider address. 134 } 135 136 for _, selector := range source { 137 // If this source has suitable matching patterns to install from 138 // the given hostname then we'll recursively search inside it 139 // for *RegistrySource objects. 140 if selector.CanHandleProvider(matchAddr) { 141 ret := findLegacyProviderLookupSource(host, selector.Source) 142 if ret != nil { 143 return ret 144 } 145 } 146 } 147 148 // If we get here then there were no selectors that are both configured 149 // to handle modules from the given hostname and that are registry 150 // sources, so we fail. 151 return nil 152 153 default: 154 // This source cannot be and cannot contain a *RegistrySource, so 155 // we fail. 156 return nil 157 } 158 } 159 160 // lookupLegacyProviderNamespace is a special method available only on 161 // RegistrySource which can deal with legacy provider addresses that contain 162 // only a type and leave the namespace implied. 163 // 164 // It asks the registry at the given hostname to provide a default namespace 165 // for the given provider type, which can be combined with the given hostname 166 // and type name to produce a fully-qualified provider address. 167 // 168 // Not all unqualified type names can be resolved to a default namespace. If 169 // the request fails, this method returns an error describing the failure. 170 // 171 // This method exists only to allow compatibility with unqualified names 172 // in older configurations. New configurations should be written so as not to 173 // depend on it, and this fallback mechanism will likely be removed altogether 174 // in a future Terraform version. 175 func (s *RegistrySource) lookupLegacyProviderNamespace(ctx context.Context, hostname svchost.Hostname, typeName string) (string, string, error) { 176 client, err := s.registryClient(hostname) 177 if err != nil { 178 return "", "", err 179 } 180 return client.legacyProviderDefaultNamespace(ctx, typeName) 181 } 182 183 // legacyProviderDefaultNamespace returns the raw address strings produced by 184 // the registry when asked about the given unqualified provider type name. 185 // The returned namespace string is taken verbatim from the registry's response. 186 // 187 // This method exists only to allow compatibility with unqualified names 188 // in older configurations. New configurations should be written so as not to 189 // depend on it. 190 func (c *registryClient) legacyProviderDefaultNamespace(ctx context.Context, typeName string) (string, string, error) { 191 endpointPath, err := url.Parse(path.Join("-", typeName, "versions")) 192 if err != nil { 193 // Should never happen because we're constructing this from 194 // already-validated components. 195 return "", "", err 196 } 197 endpointURL := c.baseURL.ResolveReference(endpointPath) 198 199 req, err := retryablehttp.NewRequest("GET", endpointURL.String(), nil) 200 if err != nil { 201 return "", "", err 202 } 203 req = req.WithContext(ctx) 204 c.addHeadersToRequest(req.Request) 205 206 // This is just to give us something to return in error messages. It's 207 // not a proper provider address. 208 placeholderProviderAddr := addrs.NewLegacyProvider(typeName) 209 210 resp, err := c.httpClient.Do(req) 211 if err != nil { 212 return "", "", c.errQueryFailed(placeholderProviderAddr, err) 213 } 214 defer resp.Body.Close() 215 216 switch resp.StatusCode { 217 case http.StatusOK: 218 // Great! 219 case http.StatusNotFound: 220 return "", "", ErrProviderNotFound{ 221 Provider: placeholderProviderAddr, 222 } 223 case http.StatusUnauthorized, http.StatusForbidden: 224 return "", "", c.errUnauthorized(placeholderProviderAddr.Hostname) 225 default: 226 return "", "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status)) 227 } 228 229 type ResponseBody struct { 230 Id string `json:"id"` 231 MovedTo string `json:"moved_to"` 232 } 233 var body ResponseBody 234 235 dec := json.NewDecoder(resp.Body) 236 if err := dec.Decode(&body); err != nil { 237 return "", "", c.errQueryFailed(placeholderProviderAddr, err) 238 } 239 240 provider, diags := addrs.ParseProviderSourceString(body.Id) 241 if diags.HasErrors() { 242 return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err()) 243 } 244 245 if provider.Type != typeName { 246 return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", provider.Type, typeName) 247 } 248 249 var movedTo addrs.Provider 250 if body.MovedTo != "" { 251 movedTo, diags = addrs.ParseProviderSourceString(body.MovedTo) 252 if diags.HasErrors() { 253 return "", "", fmt.Errorf("Error parsing provider ID from Registry: %s", diags.Err()) 254 } 255 256 if movedTo.Type != typeName { 257 return "", "", fmt.Errorf("Registry returned provider with type %q, expected %q", movedTo.Type, typeName) 258 } 259 } 260 261 return provider.Namespace, movedTo.Namespace, nil 262 }