package registry import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "path" "strings" "time" "github.com/hashicorp/terraform/httpclient" "github.com/hashicorp/terraform/registry/regsrc" "github.com/hashicorp/terraform/registry/response" "github.com/hashicorp/terraform/svchost" "github.com/hashicorp/terraform/svchost/disco" "github.com/hashicorp/terraform/version" ) const ( xTerraformGet = "X-Terraform-Get" xTerraformVersion = "X-Terraform-Version" requestTimeout = 10 * time.Second modulesServiceID = "modules.v1" providersServiceID = "providers.v1" ) var tfVersion = version.String() // Client provides methods to query Terraform Registries. type Client struct { // this is the client to be used for all requests. client *http.Client // services is a required *disco.Disco, which may have services and // credentials pre-loaded. services *disco.Disco } // NewClient returns a new initialized registry client. func NewClient(services *disco.Disco, client *http.Client) *Client { if services == nil { services = disco.New() } if client == nil { client = httpclient.New() client.Timeout = requestTimeout } services.Transport = client.Transport return &Client{ client: client, services: services, } } // Discover queries the host, and returns the url for the registry. func (c *Client) Discover(host svchost.Hostname, serviceID string) (*url.URL, error) { service, err := c.services.DiscoverServiceURL(host, serviceID) if err != nil { return nil, &ServiceUnreachableError{err} } if !strings.HasSuffix(service.Path, "/") { service.Path += "/" } return service, nil } // ModuleVersions queries the registry for a module, and returns the available versions. func (c *Client) ModuleVersions(module *regsrc.Module) (*response.ModuleVersions, error) { host, err := module.SvcHost() if err != nil { return nil, err } service, err := c.Discover(host, modulesServiceID) if err != nil { return nil, err } p, err := url.Parse(path.Join(module.Module(), "versions")) if err != nil { return nil, err } service = service.ResolveReference(p) log.Printf("[DEBUG] fetching module versions from %q", service) req, err := http.NewRequest("GET", service.String(), nil) if err != nil { return nil, err } c.addRequestCreds(host, req) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // OK case http.StatusNotFound: return nil, &errModuleNotFound{addr: module} default: return nil, fmt.Errorf("error looking up module versions: %s", resp.Status) } var versions response.ModuleVersions dec := json.NewDecoder(resp.Body) if err := dec.Decode(&versions); err != nil { return nil, err } for _, mod := range versions.Modules { for _, v := range mod.Versions { log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source) } } return &versions, nil } func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { creds, err := c.services.CredentialsForHost(host) if err != nil { log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) return } if creds != nil { creds.PrepareRequest(req) } } // ModuleLocation find the download location for a specific version module. // This returns a string, because the final location may contain special go-getter syntax. func (c *Client) ModuleLocation(module *regsrc.Module, version string) (string, error) { host, err := module.SvcHost() if err != nil { return "", err } service, err := c.Discover(host, modulesServiceID) if err != nil { return "", err } var p *url.URL if version == "" { p, err = url.Parse(path.Join(module.Module(), "download")) } else { p, err = url.Parse(path.Join(module.Module(), version, "download")) } if err != nil { return "", err } download := service.ResolveReference(p) log.Printf("[DEBUG] looking up module location from %q", download) req, err := http.NewRequest("GET", download.String(), nil) if err != nil { return "", err } c.addRequestCreds(host, req) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) if err != nil { return "", err } defer resp.Body.Close() // there should be no body, but save it for logging body, err := ioutil.ReadAll(resp.Body) if err != nil { return "", fmt.Errorf("error reading response body from registry: %s", err) } switch resp.StatusCode { case http.StatusOK, http.StatusNoContent: // OK case http.StatusNotFound: return "", fmt.Errorf("module %q version %q not found", module, version) default: // anything else is an error: return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body) } // the download location is in the X-Terraform-Get header location := resp.Header.Get(xTerraformGet) if location == "" { return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body) } // If location looks like it's trying to be a relative URL, treat it as // one. // // We don't do this for just _any_ location, since the X-Terraform-Get // header is a go-getter location rather than a URL, and so not all // possible values will parse reasonably as URLs.) // // When used in conjunction with go-getter we normally require this header // to be an absolute URL, but we are more liberal here because third-party // registry implementations may not "know" their own absolute URLs if // e.g. they are running behind a reverse proxy frontend, or such. if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") { locationURL, err := url.Parse(location) if err != nil { return "", fmt.Errorf("invalid relative URL for %q: %s", module, err) } locationURL = download.ResolveReference(locationURL) location = locationURL.String() } return location, nil } // TerraformProviderVersions queries the registry for a provider, and returns the available versions. func (c *Client) TerraformProviderVersions(provider *regsrc.TerraformProvider) (*response.TerraformProviderVersions, error) { host, err := provider.SvcHost() if err != nil { return nil, err } service, err := c.Discover(host, providersServiceID) if err != nil { return nil, err } p, err := url.Parse(path.Join(provider.TerraformProvider(), "versions")) if err != nil { return nil, err } service = service.ResolveReference(p) log.Printf("[DEBUG] fetching provider versions from %q", service) req, err := http.NewRequest("GET", service.String(), nil) if err != nil { return nil, err } c.addRequestCreds(host, req) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() switch resp.StatusCode { case http.StatusOK: // OK case http.StatusNotFound: return nil, &errProviderNotFound{addr: provider} default: return nil, fmt.Errorf("error looking up provider versions: %s", resp.Status) } var versions response.TerraformProviderVersions dec := json.NewDecoder(resp.Body) if err := dec.Decode(&versions); err != nil { return nil, err } return &versions, nil } // TerraformProviderLocation queries the registry for a provider download metadata func (c *Client) TerraformProviderLocation(provider *regsrc.TerraformProvider, version string) (*response.TerraformProviderPlatformLocation, error) { host, err := provider.SvcHost() if err != nil { return nil, err } service, err := c.Discover(host, providersServiceID) if err != nil { return nil, err } p, err := url.Parse(path.Join( provider.TerraformProvider(), version, "download", provider.OS, provider.Arch, )) if err != nil { return nil, err } service = service.ResolveReference(p) log.Printf("[DEBUG] fetching provider location from %q", service) req, err := http.NewRequest("GET", service.String(), nil) if err != nil { return nil, err } c.addRequestCreds(host, req) req.Header.Set(xTerraformVersion, tfVersion) resp, err := c.client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var loc response.TerraformProviderPlatformLocation dec := json.NewDecoder(resp.Body) if err := dec.Decode(&loc); err != nil { return nil, err } switch resp.StatusCode { case http.StatusOK, http.StatusNoContent: // OK case http.StatusNotFound: return nil, fmt.Errorf("provider %q version %q not found", provider.TerraformProvider(), version) default: // anything else is an error: return nil, fmt.Errorf("error getting download location for %q: %s", provider.TerraformProvider(), resp.Status) } return &loc, nil }