]> git.immae.eu Git - github/fretlink/terraform-provider-statuscake.git/blob - vendor/github.com/hashicorp/terraform/svchost/disco/disco.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 / disco.go
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 }