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