github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/internal/getproviders/registry_client.go (about) 1 package getproviders 2 3 import ( 4 "encoding/hex" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "net/http" 9 "net/url" 10 "path" 11 "time" 12 13 "github.com/apparentlymart/go-versions/versions" 14 svchost "github.com/hashicorp/terraform-svchost" 15 svcauth "github.com/hashicorp/terraform-svchost/auth" 16 17 "github.com/hashicorp/terraform/addrs" 18 "github.com/hashicorp/terraform/httpclient" 19 "github.com/hashicorp/terraform/version" 20 ) 21 22 const terraformVersionHeader = "X-Terraform-Version" 23 24 // registryClient is a client for the provider registry protocol that is 25 // specialized only for the needs of this package. It's not intended as a 26 // general registry API client. 27 type registryClient struct { 28 baseURL *url.URL 29 creds svcauth.HostCredentials 30 31 httpClient *http.Client 32 } 33 34 func newRegistryClient(baseURL *url.URL, creds svcauth.HostCredentials) *registryClient { 35 httpClient := httpclient.New() 36 httpClient.Timeout = 10 * time.Second 37 38 return ®istryClient{ 39 baseURL: baseURL, 40 creds: creds, 41 httpClient: httpClient, 42 } 43 } 44 45 // ProviderVersions returns the raw version strings produced by the registry 46 // for the given provider. 47 // 48 // The returned error will be ErrProviderNotKnown if the registry responds 49 // with 404 Not Found to indicate that the namespace or provider type are 50 // not known, ErrUnauthorized if the registry responds with 401 or 403 status 51 // codes, or ErrQueryFailed for any other protocol or operational problem. 52 func (c *registryClient) ProviderVersions(addr addrs.Provider) ([]string, error) { 53 endpointPath, err := url.Parse(path.Join(addr.Namespace, addr.Type, "versions")) 54 if err != nil { 55 // Should never happen because we're constructing this from 56 // already-validated components. 57 return nil, err 58 } 59 endpointURL := c.baseURL.ResolveReference(endpointPath) 60 61 req, err := http.NewRequest("GET", endpointURL.String(), nil) 62 if err != nil { 63 return nil, err 64 } 65 c.addHeadersToRequest(req) 66 67 resp, err := c.httpClient.Do(req) 68 if err != nil { 69 return nil, c.errQueryFailed(addr, err) 70 } 71 defer resp.Body.Close() 72 73 switch resp.StatusCode { 74 case http.StatusOK: 75 // Great! 76 case http.StatusNotFound: 77 return nil, ErrProviderNotKnown{ 78 Provider: addr, 79 } 80 case http.StatusUnauthorized, http.StatusForbidden: 81 return nil, c.errUnauthorized(addr.Hostname) 82 default: 83 return nil, c.errQueryFailed(addr, errors.New(resp.Status)) 84 } 85 86 // We ignore everything except the version numbers here because our goal 87 // is to find out which versions are available _at all_. Which ones are 88 // compatible with the current Terraform becomes relevant only once we've 89 // selected one, at which point we'll return an error if the selected one 90 // is incompatible. 91 // 92 // We intentionally produce an error on incompatibility, rather than 93 // silently ignoring an incompatible version, in order to give the user 94 // explicit feedback about why their selection wasn't valid and allow them 95 // to decide whether to fix that by changing the selection or by some other 96 // action such as upgrading Terraform, using a different OS to run 97 // Terraform, etc. Changes that affect compatibility are considered 98 // breaking changes from a provider API standpoint, so provider teams 99 // should change compatibility only in new major versions. 100 type ResponseBody struct { 101 Versions []struct { 102 Version string `json:"version"` 103 } `json:"versions"` 104 } 105 var body ResponseBody 106 107 dec := json.NewDecoder(resp.Body) 108 if err := dec.Decode(&body); err != nil { 109 return nil, c.errQueryFailed(addr, err) 110 } 111 112 if len(body.Versions) == 0 { 113 return nil, nil 114 } 115 116 ret := make([]string, len(body.Versions)) 117 for i, v := range body.Versions { 118 ret[i] = v.Version 119 } 120 return ret, nil 121 } 122 123 // PackageMeta returns metadata about a distribution package for a 124 // provider. 125 // 126 // The returned error will be ErrPlatformNotSupported if the registry responds 127 // with 404 Not Found, under the assumption that the caller previously checked 128 // that the provider and version are valid. It will return ErrUnauthorized if 129 // the registry responds with 401 or 403 status codes, or ErrQueryFailed for 130 // any other protocol or operational problem. 131 func (c *registryClient) PackageMeta(provider addrs.Provider, version Version, target Platform) (PackageMeta, error) { 132 endpointPath, err := url.Parse(path.Join( 133 provider.Namespace, 134 provider.Type, 135 version.String(), 136 "download", 137 target.OS, 138 target.Arch, 139 )) 140 if err != nil { 141 // Should never happen because we're constructing this from 142 // already-validated components. 143 return PackageMeta{}, err 144 } 145 endpointURL := c.baseURL.ResolveReference(endpointPath) 146 147 req, err := http.NewRequest("GET", endpointURL.String(), nil) 148 if err != nil { 149 return PackageMeta{}, err 150 } 151 c.addHeadersToRequest(req) 152 153 resp, err := c.httpClient.Do(req) 154 if err != nil { 155 return PackageMeta{}, c.errQueryFailed(provider, err) 156 } 157 defer resp.Body.Close() 158 159 switch resp.StatusCode { 160 case http.StatusOK: 161 // Great! 162 case http.StatusNotFound: 163 return PackageMeta{}, ErrPlatformNotSupported{ 164 Provider: provider, 165 Version: version, 166 Platform: target, 167 } 168 case http.StatusUnauthorized, http.StatusForbidden: 169 return PackageMeta{}, c.errUnauthorized(provider.Hostname) 170 default: 171 return PackageMeta{}, c.errQueryFailed(provider, errors.New(resp.Status)) 172 } 173 174 type ResponseBody struct { 175 Protocols []string `json:"protocols"` 176 OS string `json:"os"` 177 Arch string `json:"arch"` 178 Filename string `json:"filename"` 179 DownloadURL string `json:"download_url"` 180 SHA256Sum string `json:"shasum"` 181 182 // TODO: Other metadata for signature checking 183 } 184 var body ResponseBody 185 186 dec := json.NewDecoder(resp.Body) 187 if err := dec.Decode(&body); err != nil { 188 return PackageMeta{}, c.errQueryFailed(provider, err) 189 } 190 191 var protoVersions VersionList 192 for _, versionStr := range body.Protocols { 193 v, err := versions.ParseVersion(versionStr) 194 if err != nil { 195 return PackageMeta{}, c.errQueryFailed( 196 provider, 197 fmt.Errorf("registry response includes invalid version string %q: %s", versionStr, err), 198 ) 199 } 200 protoVersions = append(protoVersions, v) 201 } 202 protoVersions.Sort() 203 204 downloadURL, err := url.Parse(body.DownloadURL) 205 if err != nil { 206 return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: %s", err) 207 } 208 downloadURL = resp.Request.URL.ResolveReference(downloadURL) 209 if downloadURL.Scheme != "http" && downloadURL.Scheme != "https" { 210 return PackageMeta{}, fmt.Errorf("registry response includes invalid download URL: must use http or https scheme") 211 } 212 213 ret := PackageMeta{ 214 ProtocolVersions: protoVersions, 215 TargetPlatform: Platform{ 216 OS: body.OS, 217 Arch: body.Arch, 218 }, 219 Filename: body.Filename, 220 Location: PackageHTTPURL(downloadURL.String()), 221 // SHA256Sum is populated below 222 } 223 224 if len(body.SHA256Sum) != len(ret.SHA256Sum)*2 { 225 return PackageMeta{}, c.errQueryFailed( 226 provider, 227 fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err), 228 ) 229 } 230 _, err = hex.Decode(ret.SHA256Sum[:], []byte(body.SHA256Sum)) 231 if err != nil { 232 return PackageMeta{}, c.errQueryFailed( 233 provider, 234 fmt.Errorf("registry response includes invalid SHA256 hash %q: %s", body.SHA256Sum, err), 235 ) 236 } 237 238 return ret, nil 239 } 240 241 // LegacyProviderCanonicalAddress returns the raw address strings produced by 242 // the registry when asked about the given unqualified provider type name. 243 // The returned namespace string is taken verbatim from the registry's response. 244 // 245 // This method exists only to allow compatibility with unqualified names 246 // in older configurations. New configurations should be written so as not to 247 // depend on it. 248 func (c *registryClient) LegacyProviderDefaultNamespace(typeName string) (string, error) { 249 endpointPath, err := url.Parse(path.Join("-", typeName)) 250 if err != nil { 251 // Should never happen because we're constructing this from 252 // already-validated components. 253 return "", err 254 } 255 endpointURL := c.baseURL.ResolveReference(endpointPath) 256 257 req, err := http.NewRequest("GET", endpointURL.String(), nil) 258 if err != nil { 259 return "", err 260 } 261 c.addHeadersToRequest(req) 262 263 // This is just to give us something to return in error messages. It's 264 // not a proper provider address. 265 placeholderProviderAddr := addrs.NewLegacyProvider(typeName) 266 267 resp, err := c.httpClient.Do(req) 268 if err != nil { 269 return "", c.errQueryFailed(placeholderProviderAddr, err) 270 } 271 defer resp.Body.Close() 272 273 switch resp.StatusCode { 274 case http.StatusOK: 275 // Great! 276 case http.StatusNotFound: 277 return "", ErrProviderNotKnown{ 278 Provider: placeholderProviderAddr, 279 } 280 case http.StatusUnauthorized, http.StatusForbidden: 281 return "", c.errUnauthorized(placeholderProviderAddr.Hostname) 282 default: 283 return "", c.errQueryFailed(placeholderProviderAddr, errors.New(resp.Status)) 284 } 285 286 type ResponseBody struct { 287 Namespace string 288 } 289 var body ResponseBody 290 291 dec := json.NewDecoder(resp.Body) 292 if err := dec.Decode(&body); err != nil { 293 return "", c.errQueryFailed(placeholderProviderAddr, err) 294 } 295 296 return body.Namespace, nil 297 } 298 299 func (c *registryClient) addHeadersToRequest(req *http.Request) { 300 if c.creds != nil { 301 c.creds.PrepareRequest(req) 302 } 303 req.Header.Set(terraformVersionHeader, version.String()) 304 } 305 306 func (c *registryClient) errQueryFailed(provider addrs.Provider, err error) error { 307 return ErrQueryFailed{ 308 Provider: provider, 309 Wrapped: err, 310 } 311 } 312 313 func (c *registryClient) errUnauthorized(hostname svchost.Hostname) error { 314 return ErrUnauthorized{ 315 Hostname: hostname, 316 HaveCredentials: c.creds != nil, 317 } 318 }