github.com/hashicorp/terraform-plugin-sdk@v1.17.2/internal/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 "path" 11 "strings" 12 "time" 13 14 "github.com/hashicorp/terraform-plugin-sdk/httpclient" 15 internalhttpclient "github.com/hashicorp/terraform-plugin-sdk/internal/httpclient" 16 "github.com/hashicorp/terraform-plugin-sdk/internal/registry/regsrc" 17 "github.com/hashicorp/terraform-plugin-sdk/internal/registry/response" 18 "github.com/hashicorp/terraform-plugin-sdk/internal/version" 19 "github.com/hashicorp/terraform-svchost" 20 "github.com/hashicorp/terraform-svchost/disco" 21 ) 22 23 const ( 24 xTerraformGet = "X-Terraform-Get" 25 xTerraformVersion = "X-Terraform-Version" 26 requestTimeout = 10 * time.Second 27 modulesServiceID = "modules.v1" 28 providersServiceID = "providers.v1" 29 ) 30 31 var tfVersion = version.String() 32 33 // Client provides methods to query Terraform Registries. 34 type Client struct { 35 // this is the client to be used for all requests. 36 client *http.Client 37 38 // services is a required *disco.Disco, which may have services and 39 // credentials pre-loaded. 40 services *disco.Disco 41 } 42 43 // NewClient returns a new initialized registry client. 44 func NewClient(services *disco.Disco, client *http.Client) *Client { 45 if services == nil { 46 services = disco.New() 47 } 48 49 if client == nil { 50 client = internalhttpclient.New() 51 client.Timeout = requestTimeout 52 } 53 54 services.Transport = client.Transport 55 56 services.SetUserAgent(httpclient.TerraformUserAgent(version.String())) 57 58 return &Client{ 59 client: client, 60 services: services, 61 } 62 } 63 64 // Discover queries the host, and returns the url for the registry. 65 func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { 66 service, err := c.services.DiscoverServiceURL(host, serviceID) 67 if err != nil { 68 return nil, &ServiceUnreachableError{err} 69 } 70 if !strings.HasSuffix(service.Path, "/") { 71 service.Path += "/" 72 } 73 return service, nil 74 } 75 76 // ModuleVersions queries the registry for a module, and returns the available versions. 77 func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) { 78 host, err := module.SvcHost() 79 if err != nil { 80 return nil, err 81 } 82 83 service, err := c.Discover(host, modulesServiceID) 84 if err != nil { 85 return nil, err 86 } 87 88 p, err := url.Parse(path.Join(module.Module(), "versions")) 89 if err != nil { 90 return nil, err 91 } 92 93 service = service.ResolveReference(p) 94 95 log.Printf("[DEBUG] fetching module versions from %q", service) 96 97 req, err := http.NewRequest("GET", service.String(), nil) 98 if err != nil { 99 return nil, err 100 } 101 102 c.addRequestCreds(host, req) 103 req.Header.Set(xTerraformVersion, tfVersion) 104 105 resp, err := c.client.Do(req) 106 if err != nil { 107 return nil, err 108 } 109 defer resp.Body.Close() 110 111 switch resp.StatusCode { 112 case http.StatusOK: 113 // OK 114 case http.StatusNotFound: 115 return nil, &errModuleNotFound{addr: module} 116 default: 117 return nil, fmt.Errorf("error looking up module versions: %s", resp.Status) 118 } 119 120 var versions response.ModuleVersions 121 122 dec := json.NewDecoder(resp.Body) 123 if err := dec.Decode(&versions); err != nil { 124 return nil, err 125 } 126 127 for _, mod := range versions.Modules { 128 for _, v := range mod.Versions { 129 log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source) 130 } 131 } 132 133 return &versions, nil 134 } 135 136 func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { 137 creds, err := c.services.CredentialsForHost(host) 138 if err != nil { 139 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) 140 return 141 } 142 143 if creds != nil { 144 creds.PrepareRequest(req) 145 } 146 } 147 148 // ModuleLocation find the download location for a specific version module. 149 // This returns a string, because the final location may contain special go-getter syntax. 150 func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) { 151 host, err := module.SvcHost() 152 if err != nil { 153 return "", err 154 } 155 156 service, err := c.Discover(host, modulesServiceID) 157 if err != nil { 158 return "", err 159 } 160 161 var p *url.URL 162 if version == "" { 163 p, err = url.Parse(path.Join(module.Module(), "download")) 164 } else { 165 p, err = url.Parse(path.Join(module.Module(), version, "download")) 166 } 167 if err != nil { 168 return "", err 169 } 170 download := service.ResolveReference(p) 171 172 log.Printf("[DEBUG] looking up module location from %q", download) 173 174 req, err := http.NewRequest("GET", download.String(), nil) 175 if err != nil { 176 return "", err 177 } 178 179 c.addRequestCreds(host, req) 180 req.Header.Set(xTerraformVersion, tfVersion) 181 182 resp, err := c.client.Do(req) 183 if err != nil { 184 return "", err 185 } 186 defer resp.Body.Close() 187 188 // there should be no body, but save it for logging 189 body, err := ioutil.ReadAll(resp.Body) 190 if err != nil { 191 return "", fmt.Errorf("error reading response body from registry: %s", err) 192 } 193 194 switch resp.StatusCode { 195 case http.StatusOK, http.StatusNoContent: 196 // OK 197 case http.StatusNotFound: 198 return "", fmt.Errorf("module %q version %q not found", module, version) 199 default: 200 // anything else is an error: 201 return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body) 202 } 203 204 // the download location is in the X-Terraform-Get header 205 location := resp.Header.Get(xTerraformGet) 206 if location == "" { 207 return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body) 208 } 209 210 // If location looks like it's trying to be a relative URL, treat it as 211 // one. 212 // 213 // We don't do this for just _any_ location, since the X-Terraform-Get 214 // header is a go-getter location rather than a URL, and so not all 215 // possible values will parse reasonably as URLs.) 216 // 217 // When used in conjunction with go-getter we normally require this header 218 // to be an absolute URL, but we are more liberal here because third-party 219 // registry implementations may not "know" their own absolute URLs if 220 // e.g. they are running behind a reverse proxy frontend, or such. 221 if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") { 222 locationURL, err := url.Parse(location) 223 if err != nil { 224 return "", fmt.Errorf("invalid relative URL for %q: %s", module, err) 225 } 226 locationURL = download.ResolveReference(locationURL) 227 location = locationURL.String() 228 } 229 230 return location, nil 231 } 232 233 // TerraformProviderVersions queries the registry for a provider, and returns the available versions. 234 func (c *Client) TerraformProviderVersions(provider *regsrc.TerraformProvider) (*response.TerraformProviderVersions, error) { 235 host, err := provider.SvcHost() 236 if err != nil { 237 return nil, err 238 } 239 240 service, err := c.Discover(host, providersServiceID) 241 if err != nil { 242 return nil, err 243 } 244 245 p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions")) 246 if err != nil { 247 return nil, err 248 } 249 250 service = service.ResolveReference(p) 251 252 log.Printf("[DEBUG] fetching provider versions from %q", service) 253 254 req, err := http.NewRequest("GET", service.String(), nil) 255 if err != nil { 256 return nil, err 257 } 258 259 c.addRequestCreds(host, req) 260 req.Header.Set(xTerraformVersion, tfVersion) 261 262 resp, err := c.client.Do(req) 263 if err != nil { 264 return nil, err 265 } 266 defer resp.Body.Close() 267 268 switch resp.StatusCode { 269 case http.StatusOK: 270 // OK 271 case http.StatusNotFound: 272 return nil, &errProviderNotFound{addr: provider} 273 default: 274 return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status) 275 } 276 277 var versions response.TerraformProviderVersions 278 279 dec := json.NewDecoder(resp.Body) 280 if err := dec.Decode(&versions); err != nil { 281 return nil, err 282 } 283 284 return &versions, nil 285 } 286 287 // TerraformProviderLocation queries the registry for a provider download metadata 288 func (c *Client) TerraformProviderLocation(provider *regsrc.TerraformProvider, version string) (*response.TerraformProviderPlatformLocation, error) { 289 host, err := provider.SvcHost() 290 if err != nil { 291 return nil, err 292 } 293 294 service, err := c.Discover(host, providersServiceID) 295 if err != nil { 296 return nil, err 297 } 298 299 p, err := url.Parse(path.Join( 300 provider.TerraformProvider(), 301 version, 302 "download", 303 provider.OS, 304 provider.Arch, 305 )) 306 if err != nil { 307 return nil, err 308 } 309 310 service = service.ResolveReference(p) 311 312 log.Printf("[DEBUG] fetching provider location from %q", service) 313 314 req, err := http.NewRequest("GET", service.String(), nil) 315 if err != nil { 316 return nil, err 317 } 318 319 c.addRequestCreds(host, req) 320 req.Header.Set(xTerraformVersion, tfVersion) 321 322 resp, err := c.client.Do(req) 323 if err != nil { 324 return nil, err 325 } 326 defer resp.Body.Close() 327 328 var loc response.TerraformProviderPlatformLocation 329 330 dec := json.NewDecoder(resp.Body) 331 if err := dec.Decode(&loc); err != nil { 332 return nil, err 333 } 334 335 switch resp.StatusCode { 336 case http.StatusOK, http.StatusNoContent: 337 // OK 338 case http.StatusNotFound: 339 return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version) 340 default: 341 // anything else is an error: 342 return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status) 343 } 344 345 return &loc, nil 346 }