Add a method to retrieve version contraints
This commit is contained in:
parent
a4380f7246
commit
268c0f85ce
|
@ -32,7 +32,7 @@ import (
|
||||||
const (
|
const (
|
||||||
defaultHostname = "app.terraform.io"
|
defaultHostname = "app.terraform.io"
|
||||||
defaultParallelism = 10
|
defaultParallelism = 10
|
||||||
serviceID = "tfe.v2"
|
tfeServiceID = "tfe.v2.1"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Remote is an implementation of EnhancedBackend that performs all
|
// Remote is an implementation of EnhancedBackend that performs all
|
||||||
|
@ -141,8 +141,7 @@ func (b *Remote) ConfigSchema() *configschema.Block {
|
||||||
func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
||||||
var diags tfdiags.Diagnostics
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
if val := obj.GetAttr("organization"); !val.IsNull() {
|
if val := obj.GetAttr("organization"); val.IsNull() || val.AsString() == "" {
|
||||||
if val.AsString() == "" {
|
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
"Invalid organization value",
|
"Invalid organization value",
|
||||||
|
@ -150,7 +149,6 @@ func (b *Remote) ValidateConfig(obj cty.Value) tfdiags.Diagnostics {
|
||||||
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
cty.Path{cty.GetAttrStep{Name: "organization"}},
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var name, prefix string
|
var name, prefix string
|
||||||
if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
|
if workspaces := obj.GetAttr("workspaces"); !workspaces.IsNull() {
|
||||||
|
@ -219,9 +217,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
||||||
`If you are sure the hostname is correct, this could also indicate SSL `+
|
"", // no description is needed here, the error is clear
|
||||||
`verification issues. Please use "openssl s_client -connect <HOST>" to `+
|
|
||||||
`identify any certificate or certificate chain issues.`,
|
|
||||||
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
||||||
))
|
))
|
||||||
return diags
|
return diags
|
||||||
|
@ -234,9 +230,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
||||||
`If you are sure the hostname is correct, this could also indicate SSL `+
|
"", // no description is needed here, the error is clear
|
||||||
`verification issues. Please use "openssl s_client -connect <HOST>" to `+
|
|
||||||
`identify any certificate or certificate chain issues.`,
|
|
||||||
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
||||||
))
|
))
|
||||||
return diags
|
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{
|
cfg := &tfe.Config{
|
||||||
Address: service.String(),
|
Address: service.String(),
|
||||||
BasePath: service.Path,
|
BasePath: service.Path,
|
||||||
|
@ -302,7 +309,7 @@ func (b *Remote) discover(hostname string) (*url.URL, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
service, err := b.services.DiscoverServiceURL(host, serviceID)
|
service, err := b.services.DiscoverServiceURL(host, tfeServiceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -473,7 +480,27 @@ func (b *Remote) Operation(ctx context.Context, op *backend.Operation) (*backend
|
||||||
// Retrieve the workspace for this operation.
|
// Retrieve the workspace for this operation.
|
||||||
w, err := b.client.Workspaces.Read(ctx, b.organization, name)
|
w, err := b.client.Workspaces.Read(ctx, b.organization, name)
|
||||||
if err != nil {
|
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.
|
// 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
|
f = b.opApply
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"\n\nThe \"remote\" backend does not support the %q operation.\n"+
|
"\n\nThe \"remote\" backend does not support the %q operation.", op.Type)
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lock
|
// Lock
|
||||||
|
|
|
@ -119,8 +119,8 @@ func TestRemote_config(t *testing.T) {
|
||||||
|
|
||||||
// Configure
|
// Configure
|
||||||
confDiags := b.Configure(tc.config)
|
confDiags := b.Configure(tc.config)
|
||||||
if (confDiags.Err() == nil && tc.confErr != "") ||
|
if (confDiags.Err() != nil || tc.confErr != "") &&
|
||||||
(confDiags.Err() != nil && !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
|
(confDiags.Err() == nil || !strings.Contains(confDiags.Err().Error(), tc.confErr)) {
|
||||||
t.Fatalf("%s: unexpected configure result: %v", name, confDiags.Err())
|
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.
|
// Respond to service discovery calls.
|
||||||
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("/well-known/terraform.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "application/json")
|
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.
|
// 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.
|
// localhost to a local test server.
|
||||||
func testDisco(s *httptest.Server) *disco.Disco {
|
func testDisco(s *httptest.Server) *disco.Disco {
|
||||||
services := map[string]interface{}{
|
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)
|
d := disco.NewWithCredentialsSource(credsSrc)
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||||
|
"github.com/hashicorp/terraform/httpclient"
|
||||||
"github.com/hashicorp/terraform/svchost"
|
"github.com/hashicorp/terraform/svchost"
|
||||||
"github.com/hashicorp/terraform/svchost/auth"
|
"github.com/hashicorp/terraform/svchost/auth"
|
||||||
)
|
)
|
||||||
|
@ -97,6 +98,10 @@ func (d *Disco) ForceHostServices(hostname svchost.Hostname, services map[string
|
||||||
if services == nil {
|
if services == nil {
|
||||||
services = map[string]interface{}{}
|
services = map[string]interface{}{}
|
||||||
}
|
}
|
||||||
|
transport := d.Transport
|
||||||
|
if transport == nil {
|
||||||
|
transport = httpTransport
|
||||||
|
}
|
||||||
d.hostCache[hostname] = &Host{
|
d.hostCache[hostname] = &Host{
|
||||||
discoURL: &url.URL{
|
discoURL: &url.URL{
|
||||||
Scheme: "https",
|
Scheme: "https",
|
||||||
|
@ -105,6 +110,7 @@ func (d *Disco) ForceHostServices(hostname svchost.Hostname, services map[string
|
||||||
},
|
},
|
||||||
hostname: hostname.ForDisplay(),
|
hostname: hostname.ForDisplay(),
|
||||||
services: services,
|
services: services,
|
||||||
|
transport: transport,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,13 +157,13 @@ func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) {
|
||||||
Path: discoPath,
|
Path: discoPath,
|
||||||
}
|
}
|
||||||
|
|
||||||
t := d.Transport
|
transport := d.Transport
|
||||||
if t == nil {
|
if transport == nil {
|
||||||
t = httpTransport
|
transport = httpTransport
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: t,
|
Transport: transport,
|
||||||
Timeout: discoTimeout,
|
Timeout: discoTimeout,
|
||||||
|
|
||||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
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{
|
req := &http.Request{
|
||||||
|
Header: make(http.Header),
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
URL: discoURL,
|
URL: discoURL,
|
||||||
}
|
}
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
req.Header.Set("User-Agent", httpclient.UserAgentString())
|
||||||
|
|
||||||
creds, err := d.CredentialsForHost(hostname)
|
creds, err := d.CredentialsForHost(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -196,6 +205,7 @@ func (d *Disco) discover(hostname svchost.Hostname) (*Host, error) {
|
||||||
// case the client followed any redirects.
|
// case the client followed any redirects.
|
||||||
discoURL: resp.Request.URL,
|
discoURL: resp.Request.URL,
|
||||||
hostname: hostname.ForDisplay(),
|
hostname: hostname.ForDisplay(),
|
||||||
|
transport: transport,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the host without any services.
|
// Return the host without any services.
|
||||||
|
|
|
@ -353,5 +353,5 @@ func testServer(h func(w http.ResponseWriter, r *http.Request)) (portStr string,
|
||||||
server.Close()
|
server.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return portStr, close
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,37 @@
|
||||||
package disco
|
package disco
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-version"
|
||||||
|
"github.com/hashicorp/terraform/httpclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const versionServiceID = "versions.v1"
|
||||||
|
|
||||||
// Host represents a service discovered host.
|
// Host represents a service discovered host.
|
||||||
type Host struct {
|
type Host struct {
|
||||||
discoURL *url.URL
|
discoURL *url.URL
|
||||||
hostname string
|
hostname string
|
||||||
services map[string]interface{}
|
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.
|
// 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)
|
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,
|
// ServiceURL returns the URL associated with the given service identifier,
|
||||||
// which should be of the form "servicename.vN".
|
// which should be of the form "servicename.vN".
|
||||||
//
|
//
|
||||||
// A non-nil result is always an absolute URL with a scheme of either HTTPS
|
// A non-nil result is always an absolute URL with a scheme of either HTTPS
|
||||||
// or HTTP.
|
// or HTTP.
|
||||||
func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
||||||
parts := strings.SplitN(id, ".", 2)
|
svc, ver, err := parseServiceID(id)
|
||||||
if len(parts) != 2 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Invalid service ID format (i.e. service.vN): %s", id)
|
return nil, err
|
||||||
}
|
}
|
||||||
service, version := parts[0], parts[1]
|
|
||||||
|
|
||||||
// No services supported for an empty Host.
|
// No services supported for an empty Host.
|
||||||
if h == nil || h.services == nil {
|
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)
|
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
|
// See if we have a matching service as that would indicate
|
||||||
// the service is supported, but not the requested version.
|
// the service is supported, but not the requested version.
|
||||||
for serviceID := range h.services {
|
for serviceID := range h.services {
|
||||||
if strings.HasPrefix(serviceID, service) {
|
if strings.HasPrefix(serviceID, svc+".") {
|
||||||
return nil, &ErrVersionNotSupported{
|
return nil, &ErrVersionNotSupported{
|
||||||
hostname: h.hostname,
|
hostname: h.hostname,
|
||||||
service: service,
|
service: svc,
|
||||||
version: version,
|
version: ver.Original(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No discovered services match the requested service ID.
|
// No discovered services match the requested service.
|
||||||
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: service}
|
return nil, &ErrServiceNotProvided{hostname: h.hostname, service: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
u, err := url.Parse(urlStr)
|
u, err := url.Parse(urlStr)
|
||||||
|
@ -93,3 +127,132 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
||||||
|
|
||||||
return h.discoURL.ResolveReference(u), nil
|
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
|
package disco
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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
|
||||||
|
}
|
||||||
|
|
|
@ -28,8 +28,12 @@ Terraform Enterprise (version v201809-1 or newer).
|
||||||
Currently the remote backend supports the following Terraform commands:
|
Currently the remote backend supports the following Terraform commands:
|
||||||
|
|
||||||
- `apply`
|
- `apply`
|
||||||
|
- `console`
|
||||||
|
- `destroy` (requires manually setting `CONFIRM_DESTROY=1` on the workspace)
|
||||||
- `fmt`
|
- `fmt`
|
||||||
- `get`
|
- `get`
|
||||||
|
- `graph`
|
||||||
|
- `import`
|
||||||
- `init`
|
- `init`
|
||||||
- `output`
|
- `output`
|
||||||
- `plan`
|
- `plan`
|
||||||
|
|
Loading…
Reference in New Issue