Merge pull request #19647 from hashicorp/svh/f-versions
core: add a method to the disco package retrieve version constraints
This commit is contained in:
commit
da8e02eb2e
|
@ -32,7 +32,7 @@ import (
|
|||
const (
|
||||
defaultHostname = "app.terraform.io"
|
||||
defaultParallelism = 10
|
||||
serviceID = "tfe.v2"
|
||||
tfeServiceID = "tfe.v2.1"
|
||||
)
|
||||
|
||||
// Remote is an implementation of EnhancedBackend that performs all
|
||||
|
@ -141,15 +141,13 @@ func (b *Remote) ConfigSchema() *configschema.Block {
|
|||
func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
if val := obj.GetAttr("organization"); !val.IsNull() {
|
||||
if val.AsString() == "" {
|
||||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Invalid organization value",
|
||||
`The "organization" attribute value must not be empty.`,
|
||||
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
||||
))
|
||||
}
|
||||
if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" {
|
||||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
"Invalid organization value",
|
||||
`The "organization" attribute value must not be empty.`,
|
||||
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
||||
))
|
||||
}
|
||||
|
||||
var name, prefix string
|
||||
|
@ -219,9 +217,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
||||
`If you are sure the hostname is correct, this could also indicate SSL `+
|
||||
`verification issues. Please use "openssl s_client -connect <HOST>" to `+
|
||||
`identify any certificate or certificate chain issues.`,
|
||||
"", // no description is needed here, the error is clear
|
||||
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
||||
))
|
||||
return diags
|
||||
|
@ -234,9 +230,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
||||
`If you are sure the hostname is correct, this could also indicate SSL `+
|
||||
`verification issues. Please use "openssl s_client -connect <HOST>" to `+
|
||||
`identify any certificate or certificate chain issues.`,
|
||||
"", // no description is needed here, the error is clear
|
||||
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
||||
))
|
||||
return diags
|
||||
|
@ -247,6 +241,19 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
}
|
||||
}
|
||||
|
||||
// Return an error if we still don't have a token at this point.
|
||||
if token == "" {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Required token could not be found",
|
||||
fmt.Sprintf(
|
||||
"Make sure you configured a credentials block for %s in your CLI Config File.",
|
||||
b.hostname,
|
||||
),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
cfg := &tfe.Config{
|
||||
Address: service.String(),
|
||||
BasePath: service.Path,
|
||||
|
@ -302,7 +309,7 @@ func (b *Remote) discover(hostname string) (*url.URL, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service, err := b.services.DiscoverServiceURL(host, serviceID)
|
||||
service, err := b.services.DiscoverServiceURL(host, tfeServiceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -473,7 +480,27 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
|||
// Retrieve the workspace for this operation.
|
||||
w, err := b.client.Workspaces.Read(ctx, b.organization, name)
|
||||
if err != nil {
|
||||
return nil, generalError("Failed to retrieve workspace", err)
|
||||
switch err {
|
||||
case context.Canceled:
|
||||
return nil, err
|
||||
case tfe.ErrResourceNotFound:
|
||||
return nil, fmt.Errorf(
|
||||
"workspace %s not found\n\n"+
|
||||
"The configured \"remote\" backend returns '404 Not Found' errors for resources\n"+
|
||||
"that do not exist, as well as for resources that a user doesn't have access\n"+
|
||||
"to. When the resource does exists, please check the rights for the used token.",
|
||||
name,
|
||||
)
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"%s\n\n"+
|
||||
"The configured \"remote\" backend encountered an unexpected error. Sometimes\n"+
|
||||
"this is caused by network connection problems, in which case you could retr\n"+
|
||||
"the command. If the issue persists please open a support ticket to get help\n"+
|
||||
"resolving the problem.",
|
||||
err,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to use the local backend to run the operation.
|
||||
|
@ -493,9 +520,7 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
|||
f = b.opApply
|
||||
default:
|
||||
return nil, fmt.Errorf(
|
||||
"\n\nThe \"remote\" backend does not support the %q operation.\n"+
|
||||
"Please use the remote backend web UI for running this operation:\n"+
|
||||
"https://%s/app/%s/%s", op.Type, b.hostname, b.organization, op.Workspace)
|
||||
"\n\nThe \"remote\" backend does not support the %q operation.", op.Type)
|
||||
}
|
||||
|
||||
// Lock
|
||||
|
|
|
@ -119,8 +119,8 @@ func TestRemote_config(t *testing.T) {
|
|||
|
||||
// Configure
|
||||
confDiags := b.Configure(tc.config)
|
||||
if (confDiags.Err() == nil && tc.confErr != "") ||
|
||||
(confDiags.Err() != nil && !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
|
||||
if (confDiags.Err() != nil || tc.confErr != "") &&
|
||||
(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
|
||||
t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -181,7 +181,7 @@ func testServer(t *testing.T) *httptest.Server {
|
|||
// Respond to service discovery calls.
|
||||
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"tfe.v2":"/api/v2/"}`)
|
||||
io.WriteString(w, `{"tfe.v2.1":"/api/v2/"}`)
|
||||
})
|
||||
|
||||
// Respond to the initial query to read the hashicorp org entitlements.
|
||||
|
@ -243,7 +243,7 @@ func testServer(t *testing.T) *httptest.Server {
|
|||
// localhost to a local test server.
|
||||
func testDisco(s *httptest.Server) *disco.Disco {
|
||||
services := map[string]interface{}{
|
||||
"tfe.v2": fmt.Sprintf("%s/api/v2/", s.URL),
|
||||
"tfe.v2.1": fmt.Sprintf("%s/api/v2/", s.URL),
|
||||
}
|
||||
d := disco.NewWithCredentialsSource(credsSrc)
|
||||
|
||||
|
|
2
go.mod
2
go.mod
|
@ -68,7 +68,7 @@ require (
|
|||
github.com/hashicorp/go-sockaddr v0.0.0-20180320115054-6d291a969b86 // indirect
|
||||
github.com/hashicorp/go-tfe v0.3.4
|
||||
github.com/hashicorp/go-uuid v1.0.0
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||
github.com/hashicorp/go-version v1.0.0
|
||||
github.com/hashicorp/golang-lru v0.5.0 // indirect
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||
github.com/hashicorp/hcl2 v0.0.0-20181208003705-670926858200
|
||||
|
|
6
go.sum
6
go.sum
|
@ -161,14 +161,12 @@ github.com/hashicorp/go-tfe v0.3.4 h1:A9pKjZMDTSGozXf2wQlWhBI7QoxCoas14Xg/TSiEAV
|
|||
github.com/hashicorp/go-tfe v0.3.4/go.mod h1:Vssg8/lwVz+PyJ/nAK97zYmXxxLe28MCIMhKo+rva1o=
|
||||
github.com/hashicorp/go-uuid v1.0.0 h1:RS8zrF7PhGwyNPOtxSClXXj9HA8feRnJzgnI1RJCSnM=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577 h1:at4+18LrM8myamuV7/vT6x2s1JNXp2k4PsSbt4I02X4=
|
||||
github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/go-version v1.0.0 h1:21MVWPKDphxa7ineQQTrCU5brh7OuVVAzGOCnnCPtE8=
|
||||
github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
|
||||
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
|
||||
github.com/hashicorp/hcl2 v0.0.0-20181206005933-df9794be1f23 h1:RcXTRSKSKCJYXxI7yaOwAH1lYfZIzxhQhW2bFC8hABE=
|
||||
github.com/hashicorp/hcl2 v0.0.0-20181206005933-df9794be1f23/go.mod h1:ShfpTh661oAaxo7VcNxg0zcZW6jvMa7Moy2oFx7e5dE=
|
||||
github.com/hashicorp/hcl2 v0.0.0-20181208003705-670926858200 h1:F/nGtDwtQsuw7ZHmiLpHsPWNljDC24kiSHSGUnou9sw=
|
||||
github.com/hashicorp/hcl2 v0.0.0-20181208003705-670926858200/go.mod h1:ShfpTh661oAaxo7VcNxg0zcZW6jvMa7Moy2oFx7e5dE=
|
||||
github.com/hashicorp/hil v0.0.0-20170627220502-fa9f258a9250 h1:fooK5IvDL/KIsi4LxF/JH68nVdrBSiGNPhS2JAQjtjo=
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"time"
|
||||
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/terraform/httpclient"
|
||||
"github.com/hashicorp/terraform/svchost"
|
||||
"github.com/hashicorp/terraform/svchost/auth"
|
||||
)
|
||||
|
@ -97,14 +98,19 @@ func (d *Disco) ForceHostServices(hostname svchost.Hostname, services map[string
|
|||
if services == nil {
|
||||
services = map[string]interface{}{}
|
||||
}
|
||||
transport := d.Transport
|
||||
if transport == nil {
|
||||
transport = httpTransport
|
||||
}
|
||||
d.hostCache[hostname] = &Host{
|
||||
discoURL: &url.URL{
|
||||
Scheme: "https",
|
||||
Host: string(hostname),
|
||||
Path: discoPath,
|
||||
},
|
||||
hostname: hostname.ForDisplay(),
|
||||
services: services,
|
||||
hostname: hostname.ForDisplay(),
|
||||
services: services,
|
||||
transport: transport,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,13 +157,13 @@ func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) {
|
|||
Path: discoPath,
|
||||
}
|
||||
|
||||
t := d.Transport
|
||||
if t == nil {
|
||||
t = httpTransport
|
||||
transport := d.Transport
|
||||
if transport == nil {
|
||||
transport = httpTransport
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: t,
|
||||
Transport: transport,
|
||||
Timeout: discoTimeout,
|
||||
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
|
@ -170,9 +176,12 @@ func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) {
|
|||
}
|
||||
|
||||
req := &http.Request{
|
||||
Header: make(http.Header),
|
||||
Method: "GET",
|
||||
URL: discoURL,
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", httpclient.UserAgentString())
|
||||
|
||||
creds, err := d.CredentialsForHost(hostname)
|
||||
if err != nil {
|
||||
|
@ -194,8 +203,9 @@ func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) {
|
|||
host := &Host{
|
||||
// Use the discovery URL from resp.Request in
|
||||
// case the client followed any redirects.
|
||||
discoURL: resp.Request.URL,
|
||||
hostname: hostname.ForDisplay(),
|
||||
discoURL: resp.Request.URL,
|
||||
hostname: hostname.ForDisplay(),
|
||||
transport: transport,
|
||||
}
|
||||
|
||||
// Return the host without any services.
|
||||
|
|
|
@ -353,5 +353,5 @@ func testServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string,
|
|||
server.Close()
|
||||
}
|
||||
|
||||
return
|
||||
return portStr, close
|
||||
}
|
||||
|
|
|
@ -1,16 +1,37 @@
|
|||
package disco
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform/httpclient"
|
||||
)
|
||||
|
||||
const versionServiceID = "versions.v1"
|
||||
|
||||
// Host represents a service discovered host.
|
||||
type Host struct {
|
||||
discoURL *url.URL
|
||||
hostname string
|
||||
services map[string]interface{}
|
||||
discoURL *url.URL
|
||||
hostname string
|
||||
services map[string]interface{}
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
// Constraints represents the version constraints of a service.
|
||||
type Constraints struct {
|
||||
Service string `json:"service"`
|
||||
Product string `json:"product"`
|
||||
Minimum string `json:"minimum"`
|
||||
Excluding []string `json:"excluding"`
|
||||
Maximum string `json:"maximum"`
|
||||
}
|
||||
|
||||
// ErrServiceNotProvided is returned when the service is not provided.
|
||||
|
@ -36,21 +57,34 @@ func (e *ErrVersionNotSupported) Error() string {
|
|||
return fmt.Sprintf("host %s does not support %s version %s", e.hostname, e.service, e.version)
|
||||
}
|
||||
|
||||
// ErrNoVersionConstraints is returned when checkpoint was disabled
|
||||
// or the endpoint to query for version constraints was unavailable.
|
||||
type ErrNoVersionConstraints struct {
|
||||
disabled bool
|
||||
}
|
||||
|
||||
// Error returns a customized error message.
|
||||
func (e *ErrNoVersionConstraints) Error() string {
|
||||
if e.disabled {
|
||||
return "checkpoint disabled"
|
||||
}
|
||||
return "unable to contact versions service"
|
||||
}
|
||||
|
||||
// ServiceURL returns the URL associated with the given service identifier,
|
||||
// which should be of the form "servicename.vN".
|
||||
//
|
||||
// A non-nil result is always an absolute URL with a scheme of either HTTPS
|
||||
// or HTTP.
|
||||
func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
||||
parts := strings.SplitN(id, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id)
|
||||
svc, ver, err := parseServiceID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service, version := parts[0], parts[1]
|
||||
|
||||
// No services supported for an empty Host.
|
||||
if h == nil || h.services == nil {
|
||||
return nil, &ErrServiceNotProvided{hostname: "<unknown>", service: service}
|
||||
return nil, &ErrServiceNotProvided{hostname: "<unknown>", service: svc}
|
||||
}
|
||||
|
||||
urlStr, ok := h.services[id].(string)
|
||||
|
@ -58,17 +92,17 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
|||
// See if we have a matching service as that would indicate
|
||||
// the service is supported, but not the requested version.
|
||||
for serviceID := range h.services {
|
||||
if strings.HasPrefix(serviceID, service) {
|
||||
if strings.HasPrefix(serviceID, svc+".") {
|
||||
return nil, &ErrVersionNotSupported{
|
||||
hostname: h.hostname,
|
||||
service: service,
|
||||
version: version,
|
||||
service: svc,
|
||||
version: ver.Original(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// No discovered services match the requested service ID.
|
||||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: service}
|
||||
// No discovered services match the requested service.
|
||||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
|
||||
}
|
||||
|
||||
u, err := url.Parse(urlStr)
|
||||
|
@ -93,3 +127,132 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
|||
|
||||
return h.discoURL.ResolveReference(u), nil
|
||||
}
|
||||
|
||||
// VersionConstraints returns the contraints for a given service identifier
|
||||
// (which should be of the form "servicename.vN") and product.
|
||||
//
|
||||
// When an exact (service and version) match is found, the constraints for
|
||||
// that service are returned.
|
||||
//
|
||||
// When the requested version is not provided but the service is, we will
|
||||
// search for all alternative versions. If mutliple alternative versions
|
||||
// are found, the contrains of the latest available version are returned.
|
||||
//
|
||||
// When a service is not provided at all an error will be returned instead.
|
||||
//
|
||||
// When checkpoint is disabled or when a 404 is returned after making the
|
||||
// HTTP call, an ErrNoVersionConstraints error will be returned.
|
||||
func (h *Host) VersionConstraints(id, product string) (*Constraints, error) {
|
||||
svc, _, err := parseServiceID(id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Return early if checkpoint is disabled.
|
||||
if disabled := os.Getenv("CHECKPOINT_DISABLE"); disabled != "" {
|
||||
return nil, &ErrNoVersionConstraints{disabled: true}
|
||||
}
|
||||
|
||||
// No services supported for an empty Host.
|
||||
if h == nil || h.services == nil {
|
||||
return nil, &ErrServiceNotProvided{hostname: "<unknown>", service: svc}
|
||||
}
|
||||
|
||||
// Try to get the service URL for the version service and
|
||||
// return early if the service isn't provided by the host.
|
||||
u, err := h.ServiceURL(versionServiceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check if we have an exact (service and version) match.
|
||||
if _, ok := h.services[id].(string); !ok {
|
||||
// If we don't have an exact match, we search for all matching
|
||||
// services and then use the service ID of the latest version.
|
||||
var services []string
|
||||
for serviceID := range h.services {
|
||||
if strings.HasPrefix(serviceID, svc+".") {
|
||||
services = append(services, serviceID)
|
||||
}
|
||||
}
|
||||
|
||||
if len(services) == 0 {
|
||||
// No discovered services match the requested service.
|
||||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
|
||||
}
|
||||
|
||||
// Set id to the latest service ID we found.
|
||||
var latest *version.Version
|
||||
for _, serviceID := range services {
|
||||
if _, ver, err := parseServiceID(serviceID); err == nil {
|
||||
if latest == nil || latest.LessThan(ver) {
|
||||
id = serviceID
|
||||
latest = ver
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set a default timeout of 1 sec for the versions request (in milliseconds)
|
||||
timeout := 1000
|
||||
if _, err := strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT")); err == nil {
|
||||
timeout, _ = strconv.Atoi(os.Getenv("CHECKPOINT_TIMEOUT"))
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: h.transport,
|
||||
Timeout: time.Duration(timeout) * time.Millisecond,
|
||||
}
|
||||
|
||||
// Prepare the service URL by setting the service and product.
|
||||
v := u.Query()
|
||||
v.Set("product", product)
|
||||
u.Path += id
|
||||
u.RawQuery = v.Encode()
|
||||
|
||||
// Create a new request.
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create version constraints request: %v", err)
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
req.Header.Set("User-Agent", httpclient.UserAgentString())
|
||||
|
||||
log.Printf("[DEBUG] Retrieve version constraints for service %s and product %s", id, product)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to request version constraints: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, &ErrNoVersionConstraints{disabled: false}
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Failed to request version constraints: %s", resp.Status)
|
||||
}
|
||||
|
||||
// Parse the constraints from the response body.
|
||||
result := &Constraints{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||
return nil, fmt.Errorf("Error parsing version constraints: %v", err)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func parseServiceID(id string) (string, *version.Version, error) {
|
||||
parts := strings.SplitN(id, ".", 2)
|
||||
if len(parts) != 2 {
|
||||
return "", nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id)
|
||||
}
|
||||
|
||||
version, err := version.NewVersion(parts[1])
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("Invalid service version: %v", err)
|
||||
}
|
||||
|
||||
return parts[0], version, nil
|
||||
}
|
||||
|
|
|
@ -1,7 +1,14 @@
|
|||
package disco
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
@ -61,3 +68,223 @@ func TestHostServiceURL(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionConstrains(t *testing.T) {
|
||||
baseURL, _ := url.Parse("https://example.com/disco/foo.json")
|
||||
|
||||
t.Run("exact service version is provided", func(t *testing.T) {
|
||||
portStr, close := testVersionsServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := []byte(`
|
||||
{
|
||||
"service": "%s",
|
||||
"product": "%s",
|
||||
"minimum": "0.11.8",
|
||||
"maximum": "0.12.0"
|
||||
}`)
|
||||
// Add the requested service and product to the response.
|
||||
service := path.Base(r.URL.Path)
|
||||
product := r.URL.Query().Get("product")
|
||||
resp = []byte(fmt.Sprintf(string(resp), service, product))
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Header().Add("Content-Length", strconv.Itoa(len(resp)))
|
||||
w.Write(resp)
|
||||
})
|
||||
defer close()
|
||||
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v1": "/api/v1/",
|
||||
"thingy.v2": "/api/v2/",
|
||||
"versions.v1": "https://localhost" + portStr + "/v1/versions/",
|
||||
},
|
||||
}
|
||||
|
||||
expected := &Constraints{
|
||||
Service: "thingy.v1",
|
||||
Product: "terraform",
|
||||
Minimum: "0.11.8",
|
||||
Maximum: "0.12.0",
|
||||
}
|
||||
|
||||
actual, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected version constraints error: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("expected %#v, got: %#v", expected, actual)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("service provided with different versions", func(t *testing.T) {
|
||||
portStr, close := testVersionsServer(func(w http.ResponseWriter, r *http.Request) {
|
||||
resp := []byte(`
|
||||
{
|
||||
"service": "%s",
|
||||
"product": "%s",
|
||||
"minimum": "0.11.8",
|
||||
"maximum": "0.12.0"
|
||||
}`)
|
||||
// Add the requested service and product to the response.
|
||||
service := path.Base(r.URL.Path)
|
||||
product := r.URL.Query().Get("product")
|
||||
resp = []byte(fmt.Sprintf(string(resp), service, product))
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
w.Header().Add("Content-Length", strconv.Itoa(len(resp)))
|
||||
w.Write(resp)
|
||||
})
|
||||
defer close()
|
||||
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v2": "/api/v2/",
|
||||
"thingy.v3": "/api/v3/",
|
||||
"versions.v1": "https://localhost" + portStr + "/v1/versions/",
|
||||
},
|
||||
}
|
||||
|
||||
expected := &Constraints{
|
||||
Service: "thingy.v3",
|
||||
Product: "terraform",
|
||||
Minimum: "0.11.8",
|
||||
Maximum: "0.12.0",
|
||||
}
|
||||
|
||||
actual, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected version constraints error: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("expected %#v, got: %#v", expected, actual)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("service not provided", func(t *testing.T) {
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"versions.v1": "https://localhost/v1/versions/",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if _, ok := err.(*ErrServiceNotProvided); !ok {
|
||||
t.Fatalf("expected service not provided error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("versions service returns a 404", func(t *testing.T) {
|
||||
portStr, close := testVersionsServer(nil)
|
||||
defer close()
|
||||
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v1": "/api/v1/",
|
||||
"versions.v1": "https://localhost" + portStr + "/v1/non-existent/",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if _, ok := err.(*ErrNoVersionConstraints); !ok {
|
||||
t.Fatalf("expected service not provided error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("checkpoint is disabled", func(t *testing.T) {
|
||||
if err := os.Setenv("CHECKPOINT_DISABLE", "1"); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
defer os.Unsetenv("CHECKPOINT_DISABLE")
|
||||
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v1": "/api/v1/",
|
||||
"versions.v1": "https://localhost/v1/versions/",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if _, ok := err.(*ErrNoVersionConstraints); !ok {
|
||||
t.Fatalf("expected service not provided error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("versions service not discovered", func(t *testing.T) {
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v1": "/api/v1/",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if _, ok := err.(*ErrServiceNotProvided); !ok {
|
||||
t.Fatalf("expected service not provided error, got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("versions service version not discovered", func(t *testing.T) {
|
||||
host := Host{
|
||||
discoURL: baseURL,
|
||||
hostname: "test-server",
|
||||
transport: httpTransport,
|
||||
services: map[string]interface{}{
|
||||
"thingy.v1": "/api/v1/",
|
||||
"versions.v2": "https://localhost/v2/versions/",
|
||||
},
|
||||
}
|
||||
|
||||
_, err := host.VersionConstraints("thingy.v1", "terraform")
|
||||
if _, ok := err.(*ErrVersionNotSupported); !ok {
|
||||
t.Fatalf("expected service not provided error, got: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func testVersionsServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string, close func()) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(
|
||||
func(w http.ResponseWriter, r *http.Request) {
|
||||
// Test server always returns 404 if the URL isn't what we expect
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/versions/") {
|
||||
w.WriteHeader(404)
|
||||
w.Write([]byte("not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// If the URL is correct then the given hander decides the response
|
||||
h(w, r)
|
||||
},
|
||||
))
|
||||
|
||||
serverURL, _ := url.Parse(server.URL)
|
||||
|
||||
portStr = serverURL.Port()
|
||||
if portStr != "" {
|
||||
portStr = ":" + portStr
|
||||
}
|
||||
|
||||
close = func() {
|
||||
server.Close()
|
||||
}
|
||||
|
||||
return portStr, close
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ go:
|
|||
- 1.3
|
||||
- 1.4
|
||||
- 1.9
|
||||
- "1.10"
|
||||
|
||||
script:
|
||||
- go test
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
module github.com/hashicorp/go-version
|
|
@ -15,7 +15,7 @@ var versionRegexp *regexp.Regexp
|
|||
// The raw regular expression string used for testing the validity
|
||||
// of a version.
|
||||
const VersionRegexpRaw string = `v?([0-9]+(\.[0-9]+)*?)` +
|
||||
`(-?([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
|
||||
`(-([0-9]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)|(-?([A-Za-z\-~]+[0-9A-Za-z\-~]*(\.[0-9A-Za-z\-~]+)*)))?` +
|
||||
`(\+([0-9A-Za-z\-~]+(\.[0-9A-Za-z\-~]+)*))?` +
|
||||
`?`
|
||||
|
||||
|
@ -25,6 +25,7 @@ type Version struct {
|
|||
pre string
|
||||
segments []int64
|
||||
si int
|
||||
original string
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -59,11 +60,17 @@ func NewVersion(v string) (*Version, error) {
|
|||
segments = append(segments, 0)
|
||||
}
|
||||
|
||||
pre := matches[7]
|
||||
if pre == "" {
|
||||
pre = matches[4]
|
||||
}
|
||||
|
||||
return &Version{
|
||||
metadata: matches[7],
|
||||
pre: matches[4],
|
||||
metadata: matches[10],
|
||||
pre: pre,
|
||||
segments: segments,
|
||||
si: si,
|
||||
original: v,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -301,11 +308,19 @@ func (v *Version) Segments() []int {
|
|||
// for a version "1.2.3-beta", segments will return a slice of
|
||||
// 1, 2, 3.
|
||||
func (v *Version) Segments64() []int64 {
|
||||
return v.segments
|
||||
result := make([]int64, len(v.segments))
|
||||
copy(result, v.segments)
|
||||
return result
|
||||
}
|
||||
|
||||
// String returns the full version string included pre-release
|
||||
// and metadata information.
|
||||
//
|
||||
// This value is rebuilt according to the parsed segments and other
|
||||
// information. Therefore, ambiguities in the version string such as
|
||||
// prefixed zeroes (1.04.0 => 1.4.0), `v` prefix (v1.0.0 => 1.0.0), and
|
||||
// missing parts (1.0 => 1.0.0) will be made into a canonicalized form
|
||||
// as shown in the parenthesized examples.
|
||||
func (v *Version) String() string {
|
||||
var buf bytes.Buffer
|
||||
fmtParts := make([]string, len(v.segments))
|
||||
|
@ -324,3 +339,9 @@ func (v *Version) String() string {
|
|||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Original returns the original parsed version as-is, including any
|
||||
// potential whitespace, `v` prefix, etc.
|
||||
func (v *Version) Original() string {
|
||||
return v.original
|
||||
}
|
||||
|
|
|
@ -331,7 +331,7 @@ github.com/hashicorp/go-slug
|
|||
github.com/hashicorp/go-tfe
|
||||
# github.com/hashicorp/go-uuid v1.0.0
|
||||
github.com/hashicorp/go-uuid
|
||||
# github.com/hashicorp/go-version v0.0.0-20180322230233-23480c066577
|
||||
# github.com/hashicorp/go-version v1.0.0
|
||||
github.com/hashicorp/go-version
|
||||
# github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f
|
||||
github.com/hashicorp/hcl
|
||||
|
|
|
@ -28,8 +28,12 @@ Terraform Enterprise (version v201809-1 or newer).
|
|||
Currently the remote backend supports the following Terraform commands:
|
||||
|
||||
- `apply`
|
||||
- `console`
|
||||
- `destroy` (requires manually setting `CONFIRM_DESTROY=1` on the workspace)
|
||||
- `fmt`
|
||||
- `get`
|
||||
- `graph`
|
||||
- `import`
|
||||
- `init`
|
||||
- `output`
|
||||
- `plan`
|
||||
|
|
Loading…
Reference in New Issue