diff options
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/svchost')
10 files changed, 1058 insertions, 0 deletions
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/cache.go b/vendor/github.com/hashicorp/terraform/svchost/auth/cache.go new file mode 100644 index 0000000..4f0d168 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/cache.go | |||
@@ -0,0 +1,45 @@ | |||
1 | package auth | ||
2 | |||
3 | import ( | ||
4 | "github.com/hashicorp/terraform/svchost" | ||
5 | ) | ||
6 | |||
7 | // CachingCredentialsSource creates a new credentials source that wraps another | ||
8 | // and caches its results in memory, on a per-hostname basis. | ||
9 | // | ||
10 | // No means is provided for expiration of cached credentials, so a caching | ||
11 | // credentials source should have a limited lifetime (one Terraform operation, | ||
12 | // for example) to ensure that time-limited credentials don't expire before | ||
13 | // their cache entries do. | ||
14 | func CachingCredentialsSource(source CredentialsSource) CredentialsSource { | ||
15 | return &cachingCredentialsSource{ | ||
16 | source: source, | ||
17 | cache: map[svchost.Hostname]HostCredentials{}, | ||
18 | } | ||
19 | } | ||
20 | |||
21 | type cachingCredentialsSource struct { | ||
22 | source CredentialsSource | ||
23 | cache map[svchost.Hostname]HostCredentials | ||
24 | } | ||
25 | |||
26 | // ForHost passes the given hostname on to the wrapped credentials source and | ||
27 | // caches the result to return for future requests with the same hostname. | ||
28 | // | ||
29 | // Both credentials and non-credentials (nil) responses are cached. | ||
30 | // | ||
31 | // No cache entry is created if the wrapped source returns an error, to allow | ||
32 | // the caller to retry the failing operation. | ||
33 | func (s *cachingCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { | ||
34 | if cache, cached := s.cache[host]; cached { | ||
35 | return cache, nil | ||
36 | } | ||
37 | |||
38 | result, err := s.source.ForHost(host) | ||
39 | if err != nil { | ||
40 | return result, err | ||
41 | } | ||
42 | |||
43 | s.cache[host] = result | ||
44 | return result, nil | ||
45 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/credentials.go b/vendor/github.com/hashicorp/terraform/svchost/auth/credentials.go new file mode 100644 index 0000000..0372c16 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/credentials.go | |||
@@ -0,0 +1,63 @@ | |||
1 | // Package auth contains types and functions to manage authentication | ||
2 | // credentials for service hosts. | ||
3 | package auth | ||
4 | |||
5 | import ( | ||
6 | "net/http" | ||
7 | |||
8 | "github.com/hashicorp/terraform/svchost" | ||
9 | ) | ||
10 | |||
11 | // Credentials is a list of CredentialsSource objects that can be tried in | ||
12 | // turn until one returns credentials for a host, or one returns an error. | ||
13 | // | ||
14 | // A Credentials is itself a CredentialsSource, wrapping its members. | ||
15 | // In principle one CredentialsSource can be nested inside another, though | ||
16 | // there is no good reason to do so. | ||
17 | type Credentials []CredentialsSource | ||
18 | |||
19 | // NoCredentials is an empty CredentialsSource that always returns nil | ||
20 | // when asked for credentials. | ||
21 | var NoCredentials CredentialsSource = Credentials{} | ||
22 | |||
23 | // A CredentialsSource is an object that may be able to provide credentials | ||
24 | // for a given host. | ||
25 | // | ||
26 | // Credentials lookups are not guaranteed to be concurrency-safe. Callers | ||
27 | // using these facilities in concurrent code must use external concurrency | ||
28 | // primitives to prevent race conditions. | ||
29 | type CredentialsSource interface { | ||
30 | // ForHost returns a non-nil HostCredentials if the source has credentials | ||
31 | // available for the host, and a nil HostCredentials if it does not. | ||
32 | // | ||
33 | // If an error is returned, progress through a list of CredentialsSources | ||
34 | // is halted and the error is returned to the user. | ||
35 | ForHost(host svchost.Hostname) (HostCredentials, error) | ||
36 | } | ||
37 | |||
38 | // HostCredentials represents a single set of credentials for a particular | ||
39 | // host. | ||
40 | type HostCredentials interface { | ||
41 | // PrepareRequest modifies the given request in-place to apply the | ||
42 | // receiving credentials. The usual behavior of this method is to | ||
43 | // add some sort of Authorization header to the request. | ||
44 | PrepareRequest(req *http.Request) | ||
45 | |||
46 | // Token returns the authentication token. | ||
47 | Token() string | ||
48 | } | ||
49 | |||
50 | // ForHost iterates over the contained CredentialsSource objects and | ||
51 | // tries to obtain credentials for the given host from each one in turn. | ||
52 | // | ||
53 | // If any source returns either a non-nil HostCredentials or a non-nil error | ||
54 | // then this result is returned. Otherwise, the result is nil, nil. | ||
55 | func (c Credentials) ForHost(host svchost.Hostname) (HostCredentials, error) { | ||
56 | for _, source := range c { | ||
57 | creds, err := source.ForHost(host) | ||
58 | if creds != nil || err != nil { | ||
59 | return creds, err | ||
60 | } | ||
61 | } | ||
62 | return nil, nil | ||
63 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/from_map.go b/vendor/github.com/hashicorp/terraform/svchost/auth/from_map.go new file mode 100644 index 0000000..f91006a --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/from_map.go | |||
@@ -0,0 +1,18 @@ | |||
1 | package auth | ||
2 | |||
3 | // HostCredentialsFromMap converts a map of key-value pairs from a credentials | ||
4 | // definition provided by the user (e.g. in a config file, or via a credentials | ||
5 | // helper) into a HostCredentials object if possible, or returns nil if | ||
6 | // no credentials could be extracted from the map. | ||
7 | // | ||
8 | // This function ignores map keys it is unfamiliar with, to allow for future | ||
9 | // expansion of the credentials map format for new credential types. | ||
10 | func HostCredentialsFromMap(m map[string]interface{}) HostCredentials { | ||
11 | if m == nil { | ||
12 | return nil | ||
13 | } | ||
14 | if token, ok := m["token"].(string); ok { | ||
15 | return HostCredentialsToken(token) | ||
16 | } | ||
17 | return nil | ||
18 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/helper_program.go b/vendor/github.com/hashicorp/terraform/svchost/auth/helper_program.go new file mode 100644 index 0000000..d72ffe3 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/helper_program.go | |||
@@ -0,0 +1,80 @@ | |||
1 | package auth | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "encoding/json" | ||
6 | "fmt" | ||
7 | "os/exec" | ||
8 | "path/filepath" | ||
9 | |||
10 | "github.com/hashicorp/terraform/svchost" | ||
11 | ) | ||
12 | |||
13 | type helperProgramCredentialsSource struct { | ||
14 | executable string | ||
15 | args []string | ||
16 | } | ||
17 | |||
18 | // HelperProgramCredentialsSource returns a CredentialsSource that runs the | ||
19 | // given program with the given arguments in order to obtain credentials. | ||
20 | // | ||
21 | // The given executable path must be an absolute path; it is the caller's | ||
22 | // responsibility to validate and process a relative path or other input | ||
23 | // provided by an end-user. If the given path is not absolute, this | ||
24 | // function will panic. | ||
25 | // | ||
26 | // When credentials are requested, the program will be run in a child process | ||
27 | // with the given arguments along with two additional arguments added to the | ||
28 | // end of the list: the literal string "get", followed by the requested | ||
29 | // hostname in ASCII compatibility form (punycode form). | ||
30 | func HelperProgramCredentialsSource(executable string, args ...string) CredentialsSource { | ||
31 | if !filepath.IsAbs(executable) { | ||
32 | panic("NewCredentialsSourceHelperProgram requires absolute path to executable") | ||
33 | } | ||
34 | |||
35 | fullArgs := make([]string, len(args)+1) | ||
36 | fullArgs[0] = executable | ||
37 | copy(fullArgs[1:], args) | ||
38 | |||
39 | return &helperProgramCredentialsSource{ | ||
40 | executable: executable, | ||
41 | args: fullArgs, | ||
42 | } | ||
43 | } | ||
44 | |||
45 | func (s *helperProgramCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { | ||
46 | args := make([]string, len(s.args), len(s.args)+2) | ||
47 | copy(args, s.args) | ||
48 | args = append(args, "get") | ||
49 | args = append(args, string(host)) | ||
50 | |||
51 | outBuf := bytes.Buffer{} | ||
52 | errBuf := bytes.Buffer{} | ||
53 | |||
54 | cmd := exec.Cmd{ | ||
55 | Path: s.executable, | ||
56 | Args: args, | ||
57 | Stdin: nil, | ||
58 | Stdout: &outBuf, | ||
59 | Stderr: &errBuf, | ||
60 | } | ||
61 | err := cmd.Run() | ||
62 | if _, isExitErr := err.(*exec.ExitError); isExitErr { | ||
63 | errText := errBuf.String() | ||
64 | if errText == "" { | ||
65 | // Shouldn't happen for a well-behaved helper program | ||
66 | return nil, fmt.Errorf("error in %s, but it produced no error message", s.executable) | ||
67 | } | ||
68 | return nil, fmt.Errorf("error in %s: %s", s.executable, errText) | ||
69 | } else if err != nil { | ||
70 | return nil, fmt.Errorf("failed to run %s: %s", s.executable, err) | ||
71 | } | ||
72 | |||
73 | var m map[string]interface{} | ||
74 | err = json.Unmarshal(outBuf.Bytes(), &m) | ||
75 | if err != nil { | ||
76 | return nil, fmt.Errorf("malformed output from %s: %s", s.executable, err) | ||
77 | } | ||
78 | |||
79 | return HostCredentialsFromMap(m), nil | ||
80 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/static.go b/vendor/github.com/hashicorp/terraform/svchost/auth/static.go new file mode 100644 index 0000000..5373fdd --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/static.go | |||
@@ -0,0 +1,28 @@ | |||
1 | package auth | ||
2 | |||
3 | import ( | ||
4 | "github.com/hashicorp/terraform/svchost" | ||
5 | ) | ||
6 | |||
7 | // StaticCredentialsSource is a credentials source that retrieves credentials | ||
8 | // from the provided map. It returns nil if a requested hostname is not | ||
9 | // present in the map. | ||
10 | // | ||
11 | // The caller should not modify the given map after passing it to this function. | ||
12 | func StaticCredentialsSource(creds map[svchost.Hostname]map[string]interface{}) CredentialsSource { | ||
13 | return staticCredentialsSource(creds) | ||
14 | } | ||
15 | |||
16 | type staticCredentialsSource map[svchost.Hostname]map[string]interface{} | ||
17 | |||
18 | func (s staticCredentialsSource) ForHost(host svchost.Hostname) (HostCredentials, error) { | ||
19 | if s == nil { | ||
20 | return nil, nil | ||
21 | } | ||
22 | |||
23 | if m, exists := s[host]; exists { | ||
24 | return HostCredentialsFromMap(m), nil | ||
25 | } | ||
26 | |||
27 | return nil, nil | ||
28 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/auth/token_credentials.go b/vendor/github.com/hashicorp/terraform/svchost/auth/token_credentials.go new file mode 100644 index 0000000..9358bcb --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/auth/token_credentials.go | |||
@@ -0,0 +1,25 @@ | |||
1 | package auth | ||
2 | |||
3 | import ( | ||
4 | "net/http" | ||
5 | ) | ||
6 | |||
7 | // HostCredentialsToken is a HostCredentials implementation that represents a | ||
8 | // single "bearer token", to be sent to the server via an Authorization header | ||
9 | // with the auth type set to "Bearer" | ||
10 | type HostCredentialsToken string | ||
11 | |||
12 | // PrepareRequest alters the given HTTP request by setting its Authorization | ||
13 | // header to the string "Bearer " followed by the encapsulated authentication | ||
14 | // token. | ||
15 | func (tc HostCredentialsToken) PrepareRequest(req *http.Request) { | ||
16 | if req.Header == nil { | ||
17 | req.Header = http.Header{} | ||
18 | } | ||
19 | req.Header.Set("Authorization", "Bearer "+string(tc)) | ||
20 | } | ||
21 | |||
22 | // Token returns the authentication token. | ||
23 | func (tc HostCredentialsToken) Token() string { | ||
24 | return string(tc) | ||
25 | } | ||
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 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/disco/host.go b/vendor/github.com/hashicorp/terraform/svchost/disco/host.go new file mode 100644 index 0000000..ab9514c --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/disco/host.go | |||
@@ -0,0 +1,264 @@ | |||
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 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/label_iter.go b/vendor/github.com/hashicorp/terraform/svchost/label_iter.go new file mode 100644 index 0000000..af8ccba --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/label_iter.go | |||
@@ -0,0 +1,69 @@ | |||
1 | package svchost | ||
2 | |||
3 | import ( | ||
4 | "strings" | ||
5 | ) | ||
6 | |||
7 | // A labelIter allows iterating over domain name labels. | ||
8 | // | ||
9 | // This type is copied from golang.org/x/net/idna, where it is used | ||
10 | // to segment hostnames into their separate labels for analysis. We use | ||
11 | // it for the same purpose here, in ForComparison. | ||
12 | type labelIter struct { | ||
13 | orig string | ||
14 | slice []string | ||
15 | curStart int | ||
16 | curEnd int | ||
17 | i int | ||
18 | } | ||
19 | |||
20 | func (l *labelIter) reset() { | ||
21 | l.curStart = 0 | ||
22 | l.curEnd = 0 | ||
23 | l.i = 0 | ||
24 | } | ||
25 | |||
26 | func (l *labelIter) done() bool { | ||
27 | return l.curStart >= len(l.orig) | ||
28 | } | ||
29 | |||
30 | func (l *labelIter) result() string { | ||
31 | if l.slice != nil { | ||
32 | return strings.Join(l.slice, ".") | ||
33 | } | ||
34 | return l.orig | ||
35 | } | ||
36 | |||
37 | func (l *labelIter) label() string { | ||
38 | if l.slice != nil { | ||
39 | return l.slice[l.i] | ||
40 | } | ||
41 | p := strings.IndexByte(l.orig[l.curStart:], '.') | ||
42 | l.curEnd = l.curStart + p | ||
43 | if p == -1 { | ||
44 | l.curEnd = len(l.orig) | ||
45 | } | ||
46 | return l.orig[l.curStart:l.curEnd] | ||
47 | } | ||
48 | |||
49 | // next sets the value to the next label. It skips the last label if it is empty. | ||
50 | func (l *labelIter) next() { | ||
51 | l.i++ | ||
52 | if l.slice != nil { | ||
53 | if l.i >= len(l.slice) || l.i == len(l.slice)-1 && l.slice[l.i] == "" { | ||
54 | l.curStart = len(l.orig) | ||
55 | } | ||
56 | } else { | ||
57 | l.curStart = l.curEnd + 1 | ||
58 | if l.curStart == len(l.orig)-1 && l.orig[l.curStart] == '.' { | ||
59 | l.curStart = len(l.orig) | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
64 | func (l *labelIter) set(s string) { | ||
65 | if l.slice == nil { | ||
66 | l.slice = strings.Split(l.orig, ".") | ||
67 | } | ||
68 | l.slice[l.i] = s | ||
69 | } | ||
diff --git a/vendor/github.com/hashicorp/terraform/svchost/svchost.go b/vendor/github.com/hashicorp/terraform/svchost/svchost.go new file mode 100644 index 0000000..4eded14 --- /dev/null +++ b/vendor/github.com/hashicorp/terraform/svchost/svchost.go | |||
@@ -0,0 +1,207 @@ | |||
1 | // Package svchost deals with the representations of the so-called "friendly | ||
2 | // hostnames" that we use to represent systems that provide Terraform-native | ||
3 | // remote services, such as module registry, remote operations, etc. | ||
4 | // | ||
5 | // Friendly hostnames are specified such that, as much as possible, they | ||
6 | // are consistent with how web browsers think of hostnames, so that users | ||
7 | // can bring their intuitions about how hostnames behave when they access | ||
8 | // a Terraform Enterprise instance's web UI (or indeed any other website) | ||
9 | // and have this behave in a similar way. | ||
10 | package svchost | ||
11 | |||
12 | import ( | ||
13 | "errors" | ||
14 | "fmt" | ||
15 | "strconv" | ||
16 | "strings" | ||
17 | |||
18 | "golang.org/x/net/idna" | ||
19 | ) | ||
20 | |||
21 | // Hostname is specialized name for string that indicates that the string | ||
22 | // has been converted to (or was already in) the storage and comparison form. | ||
23 | // | ||
24 | // Hostname values are not suitable for display in the user-interface. Use | ||
25 | // the ForDisplay method to obtain a form suitable for display in the UI. | ||
26 | // | ||
27 | // Unlike user-supplied hostnames, strings of type Hostname (assuming they | ||
28 | // were constructed by a function within this package) can be compared for | ||
29 | // equality using the standard Go == operator. | ||
30 | type Hostname string | ||
31 | |||
32 | // acePrefix is the ASCII Compatible Encoding prefix, used to indicate that | ||
33 | // a domain name label is in "punycode" form. | ||
34 | const acePrefix = "xn--" | ||
35 | |||
36 | // displayProfile is a very liberal idna profile that we use to do | ||
37 | // normalization for display without imposing validation rules. | ||
38 | var displayProfile = idna.New( | ||
39 | idna.MapForLookup(), | ||
40 | idna.Transitional(true), | ||
41 | ) | ||
42 | |||
43 | // ForDisplay takes a user-specified hostname and returns a normalized form of | ||
44 | // it suitable for display in the UI. | ||
45 | // | ||
46 | // If the input is so invalid that no normalization can be performed then | ||
47 | // this will return the input, assuming that the caller still wants to | ||
48 | // display _something_. This function is, however, more tolerant than the | ||
49 | // other functions in this package and will make a best effort to prepare | ||
50 | // _any_ given hostname for display. | ||
51 | // | ||
52 | // For validation, use either IsValid (for explicit validation) or | ||
53 | // ForComparison (which implicitly validates, returning an error if invalid). | ||
54 | func ForDisplay(given string) string { | ||
55 | var portPortion string | ||
56 | if colonPos := strings.Index(given, ":"); colonPos != -1 { | ||
57 | given, portPortion = given[:colonPos], given[colonPos:] | ||
58 | } | ||
59 | portPortion, _ = normalizePortPortion(portPortion) | ||
60 | |||
61 | ascii, err := displayProfile.ToASCII(given) | ||
62 | if err != nil { | ||
63 | return given + portPortion | ||
64 | } | ||
65 | display, err := displayProfile.ToUnicode(ascii) | ||
66 | if err != nil { | ||
67 | return given + portPortion | ||
68 | } | ||
69 | return display + portPortion | ||
70 | } | ||
71 | |||
72 | // IsValid returns true if the given user-specified hostname is a valid | ||
73 | // service hostname. | ||
74 | // | ||
75 | // Validity is determined by complying with the RFC 5891 requirements for | ||
76 | // names that are valid for domain lookup (section 5), with the additional | ||
77 | // requirement that user-supplied forms must not _already_ contain | ||
78 | // Punycode segments. | ||
79 | func IsValid(given string) bool { | ||
80 | _, err := ForComparison(given) | ||
81 | return err == nil | ||
82 | } | ||
83 | |||
84 | // ForComparison takes a user-specified hostname and returns a normalized | ||
85 | // form of it suitable for storage and comparison. The result is not suitable | ||
86 | // for display to end-users because it uses Punycode to represent non-ASCII | ||
87 | // characters, and this form is unreadable for non-ASCII-speaking humans. | ||
88 | // | ||
89 | // The result is typed as Hostname -- a specialized name for string -- so that | ||
90 | // other APIs can make it clear within the type system whether they expect a | ||
91 | // user-specified or display-form hostname or a value already normalized for | ||
92 | // comparison. | ||
93 | // | ||
94 | // The returned Hostname is not valid if the returned error is non-nil. | ||
95 | func ForComparison(given string) (Hostname, error) { | ||
96 | var portPortion string | ||
97 | if colonPos := strings.Index(given, ":"); colonPos != -1 { | ||
98 | given, portPortion = given[:colonPos], given[colonPos:] | ||
99 | } | ||
100 | |||
101 | var err error | ||
102 | portPortion, err = normalizePortPortion(portPortion) | ||
103 | if err != nil { | ||
104 | return Hostname(""), err | ||
105 | } | ||
106 | |||
107 | if given == "" { | ||
108 | return Hostname(""), fmt.Errorf("empty string is not a valid hostname") | ||
109 | } | ||
110 | |||
111 | // First we'll apply our additional constraint that Punycode must not | ||
112 | // be given directly by the user. This is not an IDN specification | ||
113 | // requirement, but we prohibit it to force users to use human-readable | ||
114 | // hostname forms within Terraform configuration. | ||
115 | labels := labelIter{orig: given} | ||
116 | for ; !labels.done(); labels.next() { | ||
117 | label := labels.label() | ||
118 | if label == "" { | ||
119 | return Hostname(""), fmt.Errorf( | ||
120 | "hostname contains empty label (two consecutive periods)", | ||
121 | ) | ||
122 | } | ||
123 | if strings.HasPrefix(label, acePrefix) { | ||
124 | return Hostname(""), fmt.Errorf( | ||
125 | "hostname label %q specified in punycode format; service hostnames must be given in unicode", | ||
126 | label, | ||
127 | ) | ||
128 | } | ||
129 | } | ||
130 | |||
131 | result, err := idna.Lookup.ToASCII(given) | ||
132 | if err != nil { | ||
133 | return Hostname(""), err | ||
134 | } | ||
135 | return Hostname(result + portPortion), nil | ||
136 | } | ||
137 | |||
138 | // ForDisplay returns a version of the receiver that is appropriate for display | ||
139 | // in the UI. This includes converting any punycode labels to their | ||
140 | // corresponding Unicode characters. | ||
141 | // | ||
142 | // A round-trip through ForComparison and this ForDisplay method does not | ||
143 | // guarantee the same result as calling this package's top-level ForDisplay | ||
144 | // function, since a round-trip through the Hostname type implies stricter | ||
145 | // handling than we do when doing basic display-only processing. | ||
146 | func (h Hostname) ForDisplay() string { | ||
147 | given := string(h) | ||
148 | var portPortion string | ||
149 | if colonPos := strings.Index(given, ":"); colonPos != -1 { | ||
150 | given, portPortion = given[:colonPos], given[colonPos:] | ||
151 | } | ||
152 | // We don't normalize the port portion here because we assume it's | ||
153 | // already been normalized on the way in. | ||
154 | |||
155 | result, err := idna.Lookup.ToUnicode(given) | ||
156 | if err != nil { | ||
157 | // Should never happen, since type Hostname indicates that a string | ||
158 | // passed through our validation rules. | ||
159 | panic(fmt.Errorf("ForDisplay called on invalid Hostname: %s", err)) | ||
160 | } | ||
161 | return result + portPortion | ||
162 | } | ||
163 | |||
164 | func (h Hostname) String() string { | ||
165 | return string(h) | ||
166 | } | ||
167 | |||
168 | func (h Hostname) GoString() string { | ||
169 | return fmt.Sprintf("svchost.Hostname(%q)", string(h)) | ||
170 | } | ||
171 | |||
172 | // normalizePortPortion attempts to normalize the "port portion" of a hostname, | ||
173 | // which begins with the first colon in the hostname and should be followed | ||
174 | // by a string of decimal digits. | ||
175 | // | ||
176 | // If the port portion is valid, a normalized version of it is returned along | ||
177 | // with a nil error. | ||
178 | // | ||
179 | // If the port portion is invalid, the input string is returned verbatim along | ||
180 | // with a non-nil error. | ||
181 | // | ||
182 | // An empty string is a valid port portion representing the absense of a port. | ||
183 | // If non-empty, the first character must be a colon. | ||
184 | func normalizePortPortion(s string) (string, error) { | ||
185 | if s == "" { | ||
186 | return s, nil | ||
187 | } | ||
188 | |||
189 | if s[0] != ':' { | ||
190 | // should never happen, since caller tends to guarantee the presence | ||
191 | // of a colon due to how it's extracted from the string. | ||
192 | return s, errors.New("port portion is missing its initial colon") | ||
193 | } | ||
194 | |||
195 | numStr := s[1:] | ||
196 | num, err := strconv.Atoi(numStr) | ||
197 | if err != nil { | ||
198 | return s, errors.New("port portion contains non-digit characters") | ||
199 | } | ||
200 | if num == 443 { | ||
201 | return "", nil // ":443" is the default | ||
202 | } | ||
203 | if num > 65535 { | ||
204 | return s, errors.New("port number is greater than 65535") | ||
205 | } | ||
206 | return fmt.Sprintf(":%d", num), nil | ||
207 | } | ||