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