github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/registry/client.go (about) 1 package registry 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "log" 8 "net/http" 9 "net/url" 10 "os" 11 "path" 12 "strconv" 13 "strings" 14 "time" 15 16 "github.com/hashicorp/go-retryablehttp" 17 svchost "github.com/hashicorp/terraform-svchost" 18 "github.com/hashicorp/terraform-svchost/disco" 19 "github.com/hashicorp/terraform/helper/logging" 20 "github.com/hashicorp/terraform/httpclient" 21 "github.com/hashicorp/terraform/registry/regsrc" 22 "github.com/hashicorp/terraform/registry/response" 23 "github.com/hashicorp/terraform/version" 24 ) 25 26 const ( 27 xTerraformGet = "X-Terraform-Get" 28 xTerraformVersion = "X-Terraform-Version" 29 modulesServiceID = "modules.v1" 30 providersServiceID = "providers.v1" 31 32 // registryDiscoveryRetryEnvName is the name of the environment variable that 33 // can be configured to customize number of retries for module and provider 34 // discovery requests with the remote registry. 35 registryDiscoveryRetryEnvName = "TF_REGISTRY_DISCOVERY_RETRY" 36 defaultRetry = 1 37 38 // registryClientTimeoutEnvName is the name of the environment variable that 39 // can be configured to customize the timeout duration (seconds) for module 40 // and provider discovery with the remote registry. 41 registryClientTimeoutEnvName = "TF_REGISTRY_CLIENT_TIMEOUT" 42 43 // defaultRequestTimeout is the default timeout duration for requests to the 44 // remote registry. 45 defaultRequestTimeout = 10 * time.Second 46 ) 47 48 var ( 49 tfVersion = version.String() 50 51 discoveryRetry int 52 requestTimeout time.Duration 53 ) 54 55 func init() { 56 configureDiscoveryRetry() 57 configureRequestTimeout() 58 } 59 60 // Client provides methods to query Terraform Registries. 61 type Client struct { 62 // this is the client to be used for all requests. 63 client *retryablehttp.Client 64 65 // services is a required *disco.Disco, which may have services and 66 // credentials pre-loaded. 67 services *disco.Disco 68 69 // retry is the number of retries the client will attempt for each request 70 // if it runs into a transient failure with the remote registry. 71 retry int 72 } 73 74 // NewClient returns a new initialized registry client. 75 func NewClient(services *disco.Disco, client *http.Client) *Client { 76 if services == nil { 77 services = disco.New() 78 } 79 80 if client == nil { 81 client = httpclient.New() 82 client.Timeout = requestTimeout 83 } 84 retryableClient := retryablehttp.NewClient() 85 retryableClient.HTTPClient = client 86 retryableClient.RetryMax = discoveryRetry 87 retryableClient.RequestLogHook = requestLogHook 88 retryableClient.ErrorHandler = maxRetryErrorHandler 89 90 logOutput, err := logging.LogOutput() 91 if err != nil { 92 log.Printf("[WARN] Failed to set up registry client logger, "+ 93 "continuing without client logging: %s", err) 94 } 95 retryableClient.Logger = log.New(logOutput, "", log.Flags()) 96 97 services.Transport = retryableClient.HTTPClient.Transport 98 99 services.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 100 101 return &Client{ 102 client: retryableClient, 103 services: services, 104 } 105 } 106 107 // Discover queries the host, and returns the url for the registry. 108 func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { 109 service, err := c.services.DiscoverServiceURL(host, serviceID) 110 if err != nil { 111 return nil, &ServiceUnreachableError{err} 112 } 113 if !strings.HasSuffix(service.Path, "/") { 114 service.Path += "/" 115 } 116 return service, nil 117 } 118 119 // ModuleVersions queries the registry for a module, and returns the available versions. 120 func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) { 121 host, err := module.SvcHost() 122 if err != nil { 123 return nil, err 124 } 125 126 service, err := c.Discover(host, modulesServiceID) 127 if err != nil { 128 return nil, err 129 } 130 131 p, err := url.Parse(path.Join(module.Module(), "versions")) 132 if err != nil { 133 return nil, err 134 } 135 136 service = service.ResolveReference(p) 137 138 log.Printf("[DEBUG] fetching module versions from %q", service) 139 140 req, err := retryablehttp.NewRequest("GET", service.String(), nil) 141 if err != nil { 142 return nil, err 143 } 144 145 c.addRequestCreds(host, req.Request) 146 req.Header.Set(xTerraformVersion, tfVersion) 147 148 resp, err := c.client.Do(req) 149 if err != nil { 150 return nil, err 151 } 152 defer resp.Body.Close() 153 154 switch resp.StatusCode { 155 case http.StatusOK: 156 // OK 157 case http.StatusNotFound: 158 return nil, &errModuleNotFound{addr: module} 159 default: 160 return nil, fmt.Errorf("error looking up module versions: %s", resp.Status) 161 } 162 163 var versions response.ModuleVersions 164 165 dec := json.NewDecoder(resp.Body) 166 if err := dec.Decode(&versions); err != nil { 167 return nil, err 168 } 169 170 for _, mod := range versions.Modules { 171 for _, v := range mod.Versions { 172 log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source) 173 } 174 } 175 176 return &versions, nil 177 } 178 179 func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { 180 creds, err := c.services.CredentialsForHost(host) 181 if err != nil { 182 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) 183 return 184 } 185 186 if creds != nil { 187 creds.PrepareRequest(req) 188 } 189 } 190 191 // ModuleLocation find the download location for a specific version module. 192 // This returns a string, because the final location may contain special go-getter syntax. 193 func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) { 194 host, err := module.SvcHost() 195 if err != nil { 196 return "", err 197 } 198 199 service, err := c.Discover(host, modulesServiceID) 200 if err != nil { 201 return "", err 202 } 203 204 var p *url.URL 205 if version == "" { 206 p, err = url.Parse(path.Join(module.Module(), "download")) 207 } else { 208 p, err = url.Parse(path.Join(module.Module(), version, "download")) 209 } 210 if err != nil { 211 return "", err 212 } 213 download := service.ResolveReference(p) 214 215 log.Printf("[DEBUG] looking up module location from %q", download) 216 217 req, err := retryablehttp.NewRequest("GET", download.String(), nil) 218 if err != nil { 219 return "", err 220 } 221 222 c.addRequestCreds(host, req.Request) 223 req.Header.Set(xTerraformVersion, tfVersion) 224 225 resp, err := c.client.Do(req) 226 if err != nil { 227 return "", err 228 } 229 defer resp.Body.Close() 230 231 // there should be no body, but save it for logging 232 body, err := ioutil.ReadAll(resp.Body) 233 if err != nil { 234 return "", fmt.Errorf("error reading response body from registry: %s", err) 235 } 236 237 switch resp.StatusCode { 238 case http.StatusOK, http.StatusNoContent: 239 // OK 240 case http.StatusNotFound: 241 return "", fmt.Errorf("module %q version %q not found", module, version) 242 default: 243 // anything else is an error: 244 return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body) 245 } 246 247 // the download location is in the X-Terraform-Get header 248 location := resp.Header.Get(xTerraformGet) 249 if location == "" { 250 return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body) 251 } 252 253 // If location looks like it's trying to be a relative URL, treat it as 254 // one. 255 // 256 // We don't do this for just _any_ location, since the X-Terraform-Get 257 // header is a go-getter location rather than a URL, and so not all 258 // possible values will parse reasonably as URLs.) 259 // 260 // When used in conjunction with go-getter we normally require this header 261 // to be an absolute URL, but we are more liberal here because third-party 262 // registry implementations may not "know" their own absolute URLs if 263 // e.g. they are running behind a reverse proxy frontend, or such. 264 if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") { 265 locationURL, err := url.Parse(location) 266 if err != nil { 267 return "", fmt.Errorf("invalid relative URL for %q: %s", module, err) 268 } 269 locationURL = download.ResolveReference(locationURL) 270 location = locationURL.String() 271 } 272 273 return location, nil 274 } 275 276 // TerraformProviderVersions queries the registry for a provider, and returns the available versions. 277 func (c *Client) TerraformProviderVersions(provider *regsrc.TerraformProvider) (*response.TerraformProviderVersions, error) { 278 host, err := provider.SvcHost() 279 if err != nil { 280 return nil, err 281 } 282 283 service, err := c.Discover(host, providersServiceID) 284 if err != nil { 285 return nil, err 286 } 287 288 p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions")) 289 if err != nil { 290 return nil, err 291 } 292 293 service = service.ResolveReference(p) 294 295 log.Printf("[DEBUG] fetching provider versions from %q", service) 296 297 req, err := retryablehttp.NewRequest("GET", service.String(), nil) 298 if err != nil { 299 return nil, err 300 } 301 302 c.addRequestCreds(host, req.Request) 303 req.Header.Set(xTerraformVersion, tfVersion) 304 305 resp, err := c.client.Do(req) 306 if err != nil { 307 return nil, err 308 } 309 defer resp.Body.Close() 310 311 switch resp.StatusCode { 312 case http.StatusOK: 313 // OK 314 case http.StatusNotFound: 315 return nil, &errProviderNotFound{addr: provider} 316 default: 317 return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status) 318 } 319 320 var versions response.TerraformProviderVersions 321 322 dec := json.NewDecoder(resp.Body) 323 if err := dec.Decode(&versions); err != nil { 324 return nil, err 325 } 326 327 return &versions, nil 328 } 329 330 // TerraformProviderLocation queries the registry for a provider download metadata 331 func (c *Client) TerraformProviderLocation(provider *regsrc.TerraformProvider, version string) (*response.TerraformProviderPlatformLocation, error) { 332 host, err := provider.SvcHost() 333 if err != nil { 334 return nil, err 335 } 336 337 service, err := c.Discover(host, providersServiceID) 338 if err != nil { 339 return nil, err 340 } 341 342 p, err := url.Parse(path.Join( 343 provider.TerraformProvider(), 344 version, 345 "download", 346 provider.OS, 347 provider.Arch, 348 )) 349 if err != nil { 350 return nil, err 351 } 352 353 service = service.ResolveReference(p) 354 355 log.Printf("[DEBUG] fetching provider location from %q", service) 356 357 req, err := retryablehttp.NewRequest("GET", service.String(), nil) 358 if err != nil { 359 return nil, err 360 } 361 362 c.addRequestCreds(host, req.Request) 363 req.Header.Set(xTerraformVersion, tfVersion) 364 365 resp, err := c.client.Do(req) 366 if err != nil { 367 return nil, err 368 } 369 defer resp.Body.Close() 370 371 var loc response.TerraformProviderPlatformLocation 372 373 dec := json.NewDecoder(resp.Body) 374 if err := dec.Decode(&loc); err != nil { 375 return nil, err 376 } 377 378 switch resp.StatusCode { 379 case http.StatusOK, http.StatusNoContent: 380 // OK 381 case http.StatusNotFound: 382 return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version) 383 default: 384 // anything else is an error: 385 return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status) 386 } 387 388 return &loc, nil 389 } 390 391 // configureDiscoveryRetry configures the number of retries the registry client 392 // will attempt for requests with retryable errors, like 502 status codes 393 func configureDiscoveryRetry() { 394 discoveryRetry = defaultRetry 395 396 if v := os.Getenv(registryDiscoveryRetryEnvName); v != "" { 397 retry, err := strconv.Atoi(v) 398 if err == nil && retry > 0 { 399 discoveryRetry = retry 400 } 401 } 402 } 403 404 func requestLogHook(logger retryablehttp.Logger, req *http.Request, i int) { 405 if i > 0 { 406 logger.Printf("[INFO] Previous request to the remote registry failed, attempting retry.") 407 } 408 } 409 410 func maxRetryErrorHandler(resp *http.Response, err error, numTries int) (*http.Response, error) { 411 // Close the body per library instructions 412 if resp != nil { 413 resp.Body.Close() 414 } 415 416 // Additional error detail: if we have a response, use the status code; 417 // if we have an error, use that; otherwise nothing. We will never have 418 // both response and error. 419 var errMsg string 420 if resp != nil { 421 errMsg = fmt.Sprintf(": %d", resp.StatusCode) 422 } else if err != nil { 423 errMsg = fmt.Sprintf(": %s", err) 424 } 425 426 // This function is always called with numTries=RetryMax+1. If we made any 427 // retry attempts, include that in the error message. 428 if numTries > 1 { 429 return resp, fmt.Errorf("the request failed after %d attempts, please try again later%s", 430 numTries, errMsg) 431 } 432 return resp, fmt.Errorf("the request failed, please try again later%s", errMsg) 433 } 434 435 // configureRequestTimeout configures the registry client request timeout from 436 // environment variables 437 func configureRequestTimeout() { 438 requestTimeout = defaultRequestTimeout 439 440 if v := os.Getenv(registryClientTimeoutEnvName); v != "" { 441 timeout, err := strconv.Atoi(v) 442 if err == nil && timeout > 0 { 443 requestTimeout = time.Duration(timeout) * time.Second 444 } 445 } 446 }