]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/svchost/disco/host.go
deps: github.com/hashicorp/terraform@sdk-v0.11-with-go-modules
[github/fretlink/terraform-provider-statuscake.git] / vendor / github.com / hashicorp / terraform / svchost / disco / host.go
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 v, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil {
205 timeout = v
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 }