diff options
author | Alex Pilon <apilon@hashicorp.com> | 2019-02-22 18:24:37 -0500 |
---|---|---|
committer | Alex Pilon <apilon@hashicorp.com> | 2019-02-22 18:24:37 -0500 |
commit | 15c0b25d011f37e7c20aeca9eaf461f78285b8d9 (patch) | |
tree | 255c250a5c9d4801c74092d33b7337d8c14438ff /vendor/github.com/hashicorp/terraform/svchost/disco/disco.go | |
parent | 07971ca38143c5faf951d152fba370ddcbe26ad5 (diff) | |
download | terraform-provider-statuscake-15c0b25d011f37e7c20aeca9eaf461f78285b8d9.tar.gz terraform-provider-statuscake-15c0b25d011f37e7c20aeca9eaf461f78285b8d9.tar.zst terraform-provider-statuscake-15c0b25d011f37e7c20aeca9eaf461f78285b8d9.zip |
deps: github.com/hashicorp/terraform@sdk-v0.11-with-go-modules
Updated via: go get github.com/hashicorp/terraform@sdk-v0.11-with-go-modules and go mod tidy
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/svchost/disco/disco.go')
-rw-r--r-- | vendor/github.com/hashicorp/terraform/svchost/disco/disco.go | 259 |
1 files changed, 259 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/svchost/disco/disco.go b/vendor/github.com/hashicorp/terraform/svchost/disco/disco.go new file mode 100644 index 0000000..1963cbd --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/disco/disco.go | |||
@@ -0,0 +1,259 @@ | |||
1 | // Package disco handles Terraform's remote service discovery protocol. | ||
2 | // | ||
3 | // This protocol allows mapping from a service hostname, as produced by the | ||
4 | // svchost package, to a set of services supported by that host and the | ||
5 | // endpoint information for each supported service. | ||
6 | package disco | ||
7 | |||
8 | import ( | ||
9 | "encoding/json" | ||
10 | "errors" | ||
11 | "fmt" | ||
12 | "io" | ||
13 | "io/ioutil" | ||
14 | "log" | ||
15 | "mime" | ||
16 | "net/http" | ||
17 | "net/url" | ||
18 | "time" | ||
19 | |||
20 | cleanhttp "github.com/hashicorp/go-cleanhttp" | ||
21 | "github.com/hashicorp/terraform/httpclient" | ||
22 | "github.com/hashicorp/terraform/svchost" | ||
23 | "github.com/hashicorp/terraform/svchost/auth" | ||
24 | ) | ||
25 | |||
26 | const ( | ||
27 | // Fixed path to the discovery manifest. | ||
28 | discoPath = "/.well-known/terraform.json" | ||
29 | |||
30 | // Arbitrary-but-small number to prevent runaway redirect loops. | ||
31 | maxRedirects = 3 | ||
32 | |||
33 | // Arbitrary-but-small time limit to prevent UI "hangs" during discovery. | ||
34 | discoTimeout = 11 * time.Second | ||
35 | |||
36 | // 1MB - to prevent abusive services from using loads of our memory. | ||
37 | maxDiscoDocBytes = 1 * 1024 * 1024 | ||
38 | ) | ||
39 | |||
40 | // httpTransport is overridden during tests, to skip TLS verification. | ||
41 | var httpTransport = cleanhttp.DefaultPooledTransport() | ||
42 | |||
43 | // Disco is the main type in this package, which allows discovery on given | ||
44 | // hostnames and caches the results by hostname to avoid repeated requests | ||
45 | // for the same information. | ||
46 | type Disco struct { | ||
47 | hostCache map[svchost.Hostname]*Host | ||
48 | credsSrc auth.CredentialsSource | ||
49 | |||
50 | // Transport is a custom http.RoundTripper to use. | ||
51 | Transport http.RoundTripper | ||
52 | } | ||
53 | |||
54 | // New returns a new initialized discovery object. | ||
55 | func New() *Disco { | ||
56 | return NewWithCredentialsSource(nil) | ||
57 | } | ||
58 | |||
59 | // NewWithCredentialsSource returns a new discovery object initialized with | ||
60 | // the given credentials source. | ||
61 | func NewWithCredentialsSource(credsSrc auth.CredentialsSource) *Disco { | ||
62 | return &Disco{ | ||
63 | hostCache: make(map[svchost.Hostname]*Host), | ||
64 | credsSrc: credsSrc, | ||
65 | Transport: httpTransport, | ||
66 | } | ||
67 | } | ||
68 | |||
69 | // SetCredentialsSource provides a credentials source that will be used to | ||
70 | // add credentials to outgoing discovery requests, where available. | ||
71 | // | ||
72 | // If this method is never called, no outgoing discovery requests will have | ||
73 | // credentials. | ||
74 | func (d *Disco) SetCredentialsSource(src auth.CredentialsSource) { | ||
75 | d.credsSrc = src | ||
76 | } | ||
77 | |||
78 | // CredentialsForHost returns a non-nil HostCredentials if the embedded source has | ||
79 | // credentials available for the host, and a nil HostCredentials if it does not. | ||
80 | func (d *Disco) CredentialsForHost(hostname svchost.Hostname) (auth.HostCredentials, error) { | ||
81 | if d.credsSrc == nil { | ||
82 | return nil, nil | ||
83 | } | ||
84 | return d.credsSrc.ForHost(hostname) | ||
85 | } | ||
86 | |||
87 | // ForceHostServices provides a pre-defined set of services for a given | ||
88 | // host, which prevents the receiver from attempting network-based discovery | ||
89 | // for the given host. Instead, the given services map will be returned | ||
90 | // verbatim. | ||
91 | // | ||
92 | // When providing "forced" services, any relative URLs are resolved against | ||
93 | // the initial discovery URL that would have been used for network-based | ||
94 | // discovery, yielding the same results as if the given map were published | ||
95 | // at the host's default discovery URL, though using absolute URLs is strongly | ||
96 | // recommended to make the configured behavior more explicit. | ||
97 | func (d *Disco) ForceHostServices(hostname svchost.Hostname, services map[string]interface{}) { | ||
98 | if services == nil { | ||
99 | services = map[string]interface{}{} | ||
100 | } | ||
101 | |||
102 | d.hostCache[hostname] = &Host{ | ||
103 | discoURL: &url.URL{ | ||
104 | Scheme: "https", | ||
105 | Host: string(hostname), | ||
106 | Path: discoPath, | ||
107 | }, | ||
108 | hostname: hostname.ForDisplay(), | ||
109 | services: services, | ||
110 | transport: d.Transport, | ||
111 | } | ||
112 | } | ||
113 | |||
114 | // Discover runs the discovery protocol against the given hostname (which must | ||
115 | // already have been validated and prepared with svchost.ForComparison) and | ||
116 | // returns an object describing the services available at that host. | ||
117 | // | ||
118 | // If a given hostname supports no Terraform services at all, a non-nil but | ||
119 | // empty Host object is returned. When giving feedback to the end user about | ||
120 | // such situations, we say "host <name> does not provide a <service> service", | ||
121 | // regardless of whether that is due to that service specifically being absent | ||
122 | // or due to the host not providing Terraform services at all, since we don't | ||
123 | // wish to expose the detail of whole-host discovery to an end-user. | ||
124 | func (d *Disco) Discover(hostname svchost.Hostname) (*Host, error) { | ||
125 | if host, cached := d.hostCache[hostname]; cached { | ||
126 | return host, nil | ||
127 | } | ||
128 | |||
129 | host, err := d.discover(hostname) | ||
130 | if err != nil { | ||
131 | return nil, err | ||
132 | } | ||
133 | d.hostCache[hostname] = host | ||
134 | |||
135 | return host, nil | ||
136 | } | ||
137 | |||
138 | // DiscoverServiceURL is a convenience wrapper for discovery on a given | ||
139 | // hostname and then looking up a particular service in the result. | ||
140 | func (d *Disco) DiscoverServiceURL(hostname svchost.Hostname, serviceID string) (*url.URL, error) { | ||
141 | host, err := d.Discover(hostname) | ||
142 | if err != nil { | ||
143 | return nil, err | ||
144 | } | ||
145 | return host.ServiceURL(serviceID) | ||
146 | } | ||
147 | |||
148 | // discover implements the actual discovery process, with its result cached | ||
149 | // by the public-facing Discover method. | ||
150 | func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) { | ||
151 | discoURL := &url.URL{ | ||
152 | Scheme: "https", | ||
153 | Host: hostname.String(), | ||
154 | Path: discoPath, | ||
155 | } | ||
156 | |||
157 | client := &http.Client{ | ||
158 | Transport: d.Transport, | ||
159 | Timeout: discoTimeout, | ||
160 | |||
161 | CheckRedirect: func(req *http.Request, via []*http.Request) error { | ||
162 | log.Printf("[DEBUG] Service discovery redirected to %s", req.URL) | ||
163 | if len(via) > maxRedirects { | ||
164 | return errors.New("too many redirects") // this error will never actually be seen | ||
165 | } | ||
166 | return nil | ||
167 | }, | ||
168 | } | ||
169 | |||
170 | req := &http.Request{ | ||
171 | Header: make(http.Header), | ||
172 | Method: "GET", | ||
173 | URL: discoURL, | ||
174 | } | ||
175 | req.Header.Set("Accept", "application/json") | ||
176 | req.Header.Set("User-Agent", httpclient.UserAgentString()) | ||
177 | |||
178 | creds, err := d.CredentialsForHost(hostname) | ||
179 | if err != nil { | ||
180 | log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", hostname, err) | ||
181 | } | ||
182 | if creds != nil { | ||
183 | // Update the request to include credentials. | ||
184 | creds.PrepareRequest(req) | ||
185 | } | ||
186 | |||
187 | log.Printf("[DEBUG] Service discovery for %s at %s", hostname, discoURL) | ||
188 | |||
189 | resp, err := client.Do(req) | ||
190 | if err != nil { | ||
191 | return nil, fmt.Errorf("Failed to request discovery document: %v", err) | ||
192 | } | ||
193 | defer resp.Body.Close() | ||
194 | |||
195 | host := &Host{ | ||
196 | // Use the discovery URL from resp.Request in | ||
197 | // case the client followed any redirects. | ||
198 | discoURL: resp.Request.URL, | ||
199 | hostname: hostname.ForDisplay(), | ||
200 | transport: d.Transport, | ||
201 | } | ||
202 | |||
203 | // Return the host without any services. | ||
204 | if resp.StatusCode == 404 { | ||
205 | return host, nil | ||
206 | } | ||
207 | |||
208 | if resp.StatusCode != 200 { | ||
209 | return nil, fmt.Errorf("Failed to request discovery document: %s", resp.Status) | ||
210 | } | ||
211 | |||
212 | contentType := resp.Header.Get("Content-Type") | ||
213 | mediaType, _, err := mime.ParseMediaType(contentType) | ||
214 | if err != nil { | ||
215 | return nil, fmt.Errorf("Discovery URL has a malformed Content-Type %q", contentType) | ||
216 | } | ||
217 | if mediaType != "application/json" { | ||
218 | return nil, fmt.Errorf("Discovery URL returned an unsupported Content-Type %q", mediaType) | ||
219 | } | ||
220 | |||
221 | // This doesn't catch chunked encoding, because ContentLength is -1 in that case. | ||
222 | if resp.ContentLength > maxDiscoDocBytes { | ||
223 | // Size limit here is not a contractual requirement and so we may | ||
224 | // adjust it over time if we find a different limit is warranted. | ||
225 | return nil, fmt.Errorf( | ||
226 | "Discovery doc response is too large (got %d bytes; limit %d)", | ||
227 | resp.ContentLength, maxDiscoDocBytes, | ||
228 | ) | ||
229 | } | ||
230 | |||
231 | // If the response is using chunked encoding then we can't predict its | ||
232 | // size, but we'll at least prevent reading the entire thing into memory. | ||
233 | lr := io.LimitReader(resp.Body, maxDiscoDocBytes) | ||
234 | |||
235 | servicesBytes, err := ioutil.ReadAll(lr) | ||
236 | if err != nil { | ||
237 | return nil, fmt.Errorf("Error reading discovery document body: %v", err) | ||
238 | } | ||
239 | |||
240 | var services map[string]interface{} | ||
241 | err = json.Unmarshal(servicesBytes, &services) | ||
242 | if err != nil { | ||
243 | return nil, fmt.Errorf("Failed to decode discovery document as a JSON object: %v", err) | ||
244 | } | ||
245 | host.services = services | ||
246 | |||
247 | return host, nil | ||
248 | } | ||
249 | |||
250 | // Forget invalidates any cached record of the given hostname. If the host | ||
251 | // has no cache entry then this is a no-op. | ||
252 | func (d *Disco) Forget(hostname svchost.Hostname) { | ||
253 | delete(d.hostCache, hostname) | ||
254 | } | ||
255 | |||
256 | // ForgetAll is like Forget, but for all of the hostnames that have cache entries. | ||
257 | func (d *Disco) ForgetAll() { | ||
258 | d.hostCache = make(map[svchost.Hostname]*Host) | ||
259 | } | ||