github.com/hartzell/terraform@v0.8.6-0.20180503104400-0cc9e050ecd4/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/httpclient" 15 "github.com/hashicorp/terraform/registry/regsrc" 16 "github.com/hashicorp/terraform/registry/response" 17 "github.com/hashicorp/terraform/svchost" 18 "github.com/hashicorp/terraform/svchost/auth" 19 "github.com/hashicorp/terraform/svchost/disco" 20 "github.com/hashicorp/terraform/version" 21 ) 22 23 const ( 24 xTerraformGet = "X-Terraform-Get" 25 xTerraformVersion = "X-Terraform-Version" 26 requestTimeout = 10 * time.Second 27 serviceID = "modules.v1" 28 ) 29 30 var tfVersion = version.String() 31 32 // Client provides methods to query Terraform Registries. 33 type Client struct { 34 // this is the client to be used for all requests. 35 client *http.Client 36 37 // services is a required *disco.Disco, which may have services and 38 // credentials pre-loaded. 39 services *disco.Disco 40 41 // Creds optionally provides credentials for communicating with service 42 // providers. 43 creds auth.CredentialsSource 44 } 45 46 func NewClient(services *disco.Disco, creds auth.CredentialsSource, client *http.Client) *Client { 47 if services == nil { 48 services = disco.NewDisco() 49 } 50 51 services.SetCredentialsSource(creds) 52 53 if client == nil { 54 client = httpclient.New() 55 client.Timeout = requestTimeout 56 } 57 58 services.Transport = client.Transport 59 60 return &Client{ 61 client: client, 62 services: services, 63 creds: creds, 64 } 65 } 66 67 // Discover qeuries the host, and returns the url for the registry. 68 func (c *Client) Discover(host svchost.Hostname) *url.URL { 69 service := c.services.DiscoverServiceURL(host, serviceID) 70 if service == nil { 71 return nil 72 } 73 if !strings.HasSuffix(service.Path, "/") { 74 service.Path += "/" 75 } 76 return service 77 } 78 79 // Versions queries the registry for a module, and returns the available versions. 80 func (c *Client) Versions(module *regsrc.Module) (*response.ModuleVersions, error) { 81 host, err := module.SvcHost() 82 if err != nil { 83 return nil, err 84 } 85 86 service := c.Discover(host) 87 if service == nil { 88 return nil, fmt.Errorf("host %s does not provide Terraform modules", host) 89 } 90 91 p, err := url.Parse(path.Join(module.Module(), "versions")) 92 if err != nil { 93 return nil, err 94 } 95 96 service = service.ResolveReference(p) 97 98 log.Printf("[DEBUG] fetching module versions from %q", service) 99 100 req, err := http.NewRequest("GET", service.String(), nil) 101 if err != nil { 102 return nil, err 103 } 104 105 c.addRequestCreds(host, req) 106 req.Header.Set(xTerraformVersion, tfVersion) 107 108 resp, err := c.client.Do(req) 109 if err != nil { 110 return nil, err 111 } 112 defer resp.Body.Close() 113 114 switch resp.StatusCode { 115 case http.StatusOK: 116 // OK 117 case http.StatusNotFound: 118 return nil, &errModuleNotFound{addr: module} 119 default: 120 return nil, fmt.Errorf("error looking up module versions: %s", resp.Status) 121 } 122 123 var versions response.ModuleVersions 124 125 dec := json.NewDecoder(resp.Body) 126 if err := dec.Decode(&versions); err != nil { 127 return nil, err 128 } 129 130 for _, mod := range versions.Modules { 131 for _, v := range mod.Versions { 132 log.Printf("[DEBUG] found available version %q for %s", v.Version, mod.Source) 133 } 134 } 135 136 return &versions, nil 137 } 138 139 func (c *Client) addRequestCreds(host svchost.Hostname, req *http.Request) { 140 if c.creds == nil { 141 return 142 } 143 144 creds, err := c.creds.ForHost(host) 145 if err != nil { 146 log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err) 147 return 148 } 149 150 if creds != nil { 151 creds.PrepareRequest(req) 152 } 153 } 154 155 // Location find the download location for a specific version module. 156 // This returns a string, because the final location may contain special go-getter syntax. 157 func (c *Client) Location(module *regsrc.Module, version string) (string, error) { 158 host, err := module.SvcHost() 159 if err != nil { 160 return "", err 161 } 162 163 service := c.Discover(host) 164 if service == nil { 165 return "", fmt.Errorf("host %s does not provide Terraform modules", host.ForDisplay()) 166 } 167 168 var p *url.URL 169 if version == "" { 170 p, err = url.Parse(path.Join(module.Module(), "download")) 171 } else { 172 p, err = url.Parse(path.Join(module.Module(), version, "download")) 173 } 174 if err != nil { 175 return "", err 176 } 177 download := service.ResolveReference(p) 178 179 log.Printf("[DEBUG] looking up module location from %q", download) 180 181 req, err := http.NewRequest("GET", download.String(), nil) 182 if err != nil { 183 return "", err 184 } 185 186 c.addRequestCreds(host, req) 187 req.Header.Set(xTerraformVersion, tfVersion) 188 189 resp, err := c.client.Do(req) 190 if err != nil { 191 return "", err 192 } 193 defer resp.Body.Close() 194 195 // there should be no body, but save it for logging 196 body, err := ioutil.ReadAll(resp.Body) 197 if err != nil { 198 return "", fmt.Errorf("error reading response body from registry: %s", err) 199 } 200 201 switch resp.StatusCode { 202 case http.StatusOK, http.StatusNoContent: 203 // OK 204 case http.StatusNotFound: 205 return "", fmt.Errorf("module %q version %q not found", module, version) 206 default: 207 // anything else is an error: 208 return "", fmt.Errorf("error getting download location for %q: %s resp:%s", module, resp.Status, body) 209 } 210 211 // the download location is in the X-Terraform-Get header 212 location := resp.Header.Get(xTerraformGet) 213 if location == "" { 214 return "", fmt.Errorf("failed to get download URL for %q: %s resp:%s", module, resp.Status, body) 215 } 216 217 // If location looks like it's trying to be a relative URL, treat it as 218 // one. 219 // 220 // We don't do this for just _any_ location, since the X-Terraform-Get 221 // header is a go-getter location rather than a URL, and so not all 222 // possible values will parse reasonably as URLs.) 223 // 224 // When used in conjunction with go-getter we normally require this header 225 // to be an absolute URL, but we are more liberal here because third-party 226 // registry implementations may not "know" their own absolute URLs if 227 // e.g. they are running behind a reverse proxy frontend, or such. 228 if strings.HasPrefix(location, "/") || strings.HasPrefix(location, "./") || strings.HasPrefix(location, "../") { 229 locationURL, err := url.Parse(location) 230 if err != nil { 231 return "", fmt.Errorf("invalid relative URL for %q: %s", module, err) 232 } 233 locationURL = download.ResolveReference(locationURL) 234 location = locationURL.String() 235 } 236 237 return location, nil 238 }