github.com/hs0210/hashicorp-terraform@v0.11.12-beta1/svchost/disco/host.go (about) 1 package disco 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "log" 7 "net/http" 8 "net/url" 9 "os" 10 "strconv" 11 "strings" 12 "time" 13 14 "github.com/hashicorp/go-version" 15 "github.com/hashicorp/terraform/httpclient" 16 ) 17 18 const versionServiceID = "versions.v1" 19 20 // Host represents a service discovered host. 21 type Host struct { 22 discoURL *url.URL 23 hostname string 24 services map[string]interface{} 25 transport http.RoundTripper 26 } 27 28 // Constraints represents the version constraints of a service. 29 type Constraints struct { 30 Service string `json:"service"` 31 Product string `json:"product"` 32 Minimum string `json:"minimum"` 33 Maximum string `json:"maximum"` 34 Excluding []string `json:"excluding"` 35 } 36 37 // ErrServiceNotProvided is returned when the service is not provided. 38 type ErrServiceNotProvided struct { 39 hostname string 40 service string 41 } 42 43 // Error returns a customized error message. 44 func (e *ErrServiceNotProvided) Error() string { 45 if e.hostname == "" { 46 return fmt.Sprintf("host does not provide a %s service", e.service) 47 } 48 return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service) 49 } 50 51 // ErrVersionNotSupported is returned when the version is not supported. 52 type ErrVersionNotSupported struct { 53 hostname string 54 service string 55 version string 56 } 57 58 // Error returns a customized error message. 59 func (e *ErrVersionNotSupported) Error() string { 60 if e.hostname == "" { 61 return fmt.Sprintf("host does not support %s version %s", e.service, e.version) 62 } 63 return fmt.Sprintf("host %s does not support %s version %s", e.hostname, e.service, e.version) 64 } 65 66 // ErrNoVersionConstraints is returned when checkpoint was disabled 67 // or the endpoint to query for version constraints was unavailable. 68 type ErrNoVersionConstraints struct { 69 disabled bool 70 } 71 72 // Error returns a customized error message. 73 func (e *ErrNoVersionConstraints) Error() string { 74 if e.disabled { 75 return "checkpoint disabled" 76 } 77 return "unable to contact versions service" 78 } 79 80 // ServiceURL returns the URL associated with the given service identifier, 81 // which should be of the form "servicename.vN". 82 // 83 // A non-nil result is always an absolute URL with a scheme of either HTTPS 84 // or HTTP. 85 func (h *Host) ServiceURL(id string) (*url.URL, error) { 86 svc, ver, err := parseServiceID(id) 87 if err != nil { 88 return nil, err 89 } 90 91 // No services supported for an empty Host. 92 if h == nil || h.services == nil { 93 return nil, &ErrServiceNotProvided{service: svc} 94 } 95 96 urlStr, ok := h.services[id].(string) 97 if !ok { 98 // See if we have a matching service as that would indicate 99 // the service is supported, but not the requested version. 100 for serviceID := range h.services { 101 if strings.HasPrefix(serviceID, svc+".") { 102 return nil, &ErrVersionNotSupported{ 103 hostname: h.hostname, 104 service: svc, 105 version: ver.Original(), 106 } 107 } 108 } 109 110 // No discovered services match the requested service. 111 return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc} 112 } 113 114 u, err := url.Parse(urlStr) 115 if err != nil { 116 return nil, fmt.Errorf("Failed to parse service URL: %v", err) 117 } 118 119 // Make relative URLs absolute using our discovery URL. 120 if !u.IsAbs() { 121 u = h.discoURL.ResolveReference(u) 122 } 123 124 if u.Scheme != "https" && u.Scheme != "http" { 125 return nil, fmt.Errorf("Service URL is using an unsupported scheme: %s", u.Scheme) 126 } 127 if u.User != nil { 128 return nil, fmt.Errorf("Embedded username/password information is not permitted") 129 } 130 131 // Fragment part is irrelevant, since we're not a browser. 132 u.Fragment = "" 133 134 return h.discoURL.ResolveReference(u), nil 135 } 136 137 // VersionConstraints returns the contraints for a given service identifier 138 // (which should be of the form "servicename.vN") and product. 139 // 140 // When an exact (service and version) match is found, the constraints for 141 // that service are returned. 142 // 143 // When the requested version is not provided but the service is, we will 144 // search for all alternative versions. If mutliple alternative versions 145 // are found, the contrains of the latest available version are returned. 146 // 147 // When a service is not provided at all an error will be returned instead. 148 // 149 // When checkpoint is disabled or when a 404 is returned after making the 150 // HTTP call, an ErrNoVersionConstraints error will be returned. 151 func (h *Host) VersionConstraints(id, product string) (*Constraints, error) { 152 svc, _, err := parseServiceID(id) 153 if err != nil { 154 return nil, err 155 } 156 157 // Return early if checkpoint is disabled. 158 if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" { 159 return nil, &ErrNoVersionConstraints{disabled: true} 160 } 161 162 // No services supported for an empty Host. 163 if h == nil || h.services == nil { 164 return nil, &ErrServiceNotProvided{service: svc} 165 } 166 167 // Try to get the service URL for the version service and 168 // return early if the service isn't provided by the host. 169 u, err := h.ServiceURL(versionServiceID) 170 if err != nil { 171 return nil, err 172 } 173 174 // Check if we have an exact (service and version) match. 175 if _, ok := h.services[id].(string); !ok { 176 // If we don't have an exact match, we search for all matching 177 // services and then use the service ID of the latest version. 178 var services []string 179 for serviceID := range h.services { 180 if strings.HasPrefix(serviceID, svc+".") { 181 services = append(services, serviceID) 182 } 183 } 184 185 if len(services) == 0 { 186 // No discovered services match the requested service. 187 return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc} 188 } 189 190 // Set id to the latest service ID we found. 191 var latest *version.Version 192 for _, serviceID := range services { 193 if _, ver, err := parseServiceID(serviceID); err == nil { 194 if latest == nil || latest.LessThan(ver) { 195 id = serviceID 196 latest = ver 197 } 198 } 199 } 200 } 201 202 // Set a default timeout of 1 sec for the versions request (in milliseconds) 203 timeout := 1000 204 if _, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil { 205 timeout, _ = strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")) 206 } 207 208 client := &http.Client{ 209 Transport: h.transport, 210 Timeout: time.Duration(timeout) * time.Millisecond, 211 } 212 213 // Prepare the service URL by setting the service and product. 214 v := u.Query() 215 v.Set("product", product) 216 u.Path += id 217 u.RawQuery = v.Encode() 218 219 // Create a new request. 220 req, err := http.NewRequest("GET", u.String(), nil) 221 if err != nil { 222 return nil, fmt.Errorf("Failed to create version constraints request: %v", err) 223 } 224 req.Header.Set("Accept", "application/json") 225 req.Header.Set("User-Agent", httpclient.UserAgentString()) 226 227 log.Printf("[DEBUG] Retrieve version constraints for service %s and product %s", id, product) 228 229 resp, err := client.Do(req) 230 if err != nil { 231 return nil, fmt.Errorf("Failed to request version constraints: %v", err) 232 } 233 defer resp.Body.Close() 234 235 if resp.StatusCode == 404 { 236 return nil, &ErrNoVersionConstraints{disabled: false} 237 } 238 239 if resp.StatusCode != 200 { 240 return nil, fmt.Errorf("Failed to request version constraints: %s", resp.Status) 241 } 242 243 // Parse the constraints from the response body. 244 result := &Constraints{} 245 if err := json.NewDecoder(resp.Body).Decode(result); err != nil { 246 return nil, fmt.Errorf("Error parsing version constraints: %v", err) 247 } 248 249 return result, nil 250 } 251 252 func parseServiceID(id string) (string, *version.Version, error) { 253 parts := strings.SplitN(id, ".", 2) 254 if len(parts) != 2 { 255 return "", nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id) 256 } 257 258 version, err := version.NewVersion(parts[1]) 259 if err != nil { 260 return "", nil, fmt.Errorf("Invalid service version: %v", err) 261 } 262 263 return parts[0], version, nil 264 }