aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/hashicorp/terraform/svchost
diff options
context:
space:
mode:
Diffstat (limited to 'vendor/github.com/hashicorp/terraform/svchost')
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/cache.go45
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/credentials.go63
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/from_map.go18
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/helper_program.go80
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/static.go28
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/auth/token_credentials.go25
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/disco/disco.go259
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/disco/host.go264
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/label_iter.go69
-rw-r--r--vendor/github.com/hashicorp/terraform/svchost/svchost.go207
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 @@
1package auth
2
3import (
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.
14func CachingCredentialsSource(source CredentialsSource) CredentialsSource {
15 return &cachingCredentialsSource{
16 source: source,
17 cache: map[svchost.Hostname]HostCredentials{},
18 }
19}
20
21type 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.
33func (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.
3package auth
4
5import (
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.
17type Credentials []CredentialsSource
18
19// NoCredentials is an empty CredentialsSource that always returns nil
20// when asked for credentials.
21var 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.
29type 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.
40type 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.
55func (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 @@
1package 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.
10func 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 @@
1package auth
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "os/exec"
8 "path/filepath"
9
10 "github.com/hashicorp/terraform/svchost"
11)
12
13type 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).
30func 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
45func (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 @@
1package auth
2
3import (
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.
12func StaticCredentialsSource(creds map[svchost.Hostname]map[string]interface{}) CredentialsSource {
13 return staticCredentialsSource(creds)
14}
15
16type staticCredentialsSource map[svchost.Hostname]map[string]interface{}
17
18func (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 @@
1package auth
2
3import (
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"
10type 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.
15func (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.
23func (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.
6package disco
7
8import (
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
26const (
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.
41var 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.
46type 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.
55func New() *Disco {
56 return NewWithCredentialsSource(nil)
57}
58
59// NewWithCredentialsSource returns a new discovery object initialized with
60// the given credentials source.
61func 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.
74func (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.
80func (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.
97func (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.
124func (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.
140func (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.
150func (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.
252func (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.
257func (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 @@
1package disco
2
3import (
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
18const versionServiceID = "versions.v1"
19
20// Host represents a service discovered host.
21type 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.
29type 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.
38type ErrServiceNotProvided struct {
39 hostname string
40 service string
41}
42
43// Error returns a customized error message.
44func (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.
52type ErrVersionNotSupported struct {
53 hostname string
54 service string
55 version string
56}
57
58// Error returns a customized error message.
59func (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.
68type ErrNoVersionConstraints struct {
69 disabled bool
70}
71
72// Error returns a customized error message.
73func (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.
85func (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.
151func (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
252func 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 @@
1package svchost
2
3import (
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.
12type labelIter struct {
13 orig string
14 slice []string
15 curStart int
16 curEnd int
17 i int
18}
19
20func (l *labelIter) reset() {
21 l.curStart = 0
22 l.curEnd = 0
23 l.i = 0
24}
25
26func (l *labelIter) done() bool {
27 return l.curStart >= len(l.orig)
28}
29
30func (l *labelIter) result() string {
31 if l.slice != nil {
32 return strings.Join(l.slice, ".")
33 }
34 return l.orig
35}
36
37func (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.
50func (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
64func (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.
10package svchost
11
12import (
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.
30type Hostname string
31
32// acePrefix is the ASCII Compatible Encoding prefix, used to indicate that
33// a domain name label is in "punycode" form.
34const acePrefix = "xn--"
35
36// displayProfile is a very liberal idna profile that we use to do
37// normalization for display without imposing validation rules.
38var 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).
54func 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.
79func 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.
95func 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.
146func (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
164func (h Hostname) String() string {
165 return string(h)
166}
167
168func (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.
184func 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}