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