backend/remote: return detailed incompatibility info
This commit is contained in:
parent
28f2a76ffd
commit
8f04e93739
|
@ -12,6 +12,7 @@ import (
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
tfe "github.com/hashicorp/go-tfe"
|
tfe "github.com/hashicorp/go-tfe"
|
||||||
|
version "github.com/hashicorp/go-version"
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
"github.com/hashicorp/terraform/configs/configschema"
|
"github.com/hashicorp/terraform/configs/configschema"
|
||||||
"github.com/hashicorp/terraform/helper/schema"
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
@ -21,7 +22,7 @@ import (
|
||||||
"github.com/hashicorp/terraform/svchost/disco"
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/hashicorp/terraform/tfdiags"
|
"github.com/hashicorp/terraform/tfdiags"
|
||||||
"github.com/hashicorp/terraform/version"
|
tfversion "github.com/hashicorp/terraform/version"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
@ -211,8 +212,30 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Discover the service URL for this host to confirm that it provides
|
// Discover the service URL for this host to confirm that it provides
|
||||||
// a remote backend API and to discover the required base path.
|
// a remote backend API and to get the version constraints.
|
||||||
service, err := b.discover(b.hostname)
|
service, constraints, err := b.discover()
|
||||||
|
if err != nil {
|
||||||
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
|
tfdiags.Error,
|
||||||
|
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
||||||
|
"", // no description is needed here, the error is clear
|
||||||
|
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check any retrieved constraints to make sure we are compatible.
|
||||||
|
if constraints == nil {
|
||||||
|
diags = diags.Append(b.checkConstraints(constraints))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if we have any discovery of version constraints errors.
|
||||||
|
if diags.HasErrors() {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the token for this host as configured in the credentials
|
||||||
|
// section of the CLI Config File.
|
||||||
|
token, err := b.token()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
diags = diags.Append(tfdiags.AttributeValue(
|
||||||
tfdiags.Error,
|
tfdiags.Error,
|
||||||
|
@ -223,18 +246,8 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve the token for this host as configured in the credentials
|
// Get the token from the config if no token was configured for this
|
||||||
// section of the CLI Config File.
|
// host in credentials section of the CLI Config File.
|
||||||
token, err := b.token(b.hostname)
|
|
||||||
if err != nil {
|
|
||||||
diags = diags.Append(tfdiags.AttributeValue(
|
|
||||||
tfdiags.Error,
|
|
||||||
strings.ToUpper(err.Error()[:1])+err.Error()[1:],
|
|
||||||
"", // no description is needed here, the error is clear
|
|
||||||
cty.Path{cty.GetAttrStep{Name: "hostname"}},
|
|
||||||
))
|
|
||||||
return diags
|
|
||||||
}
|
|
||||||
if token == "" {
|
if token == "" {
|
||||||
if val := obj.GetAttr("token"); !val.IsNull() {
|
if val := obj.GetAttr("token"); !val.IsNull() {
|
||||||
token = val.AsString()
|
token = val.AsString()
|
||||||
|
@ -262,7 +275,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the version header to the current version.
|
// Set the version header to the current version.
|
||||||
cfg.Headers.Set(version.Header, version.Version)
|
cfg.Headers.Set(tfversion.Header, tfversion.Version)
|
||||||
|
|
||||||
// Create the remote backend API client.
|
// Create the remote backend API client.
|
||||||
b.client, err = tfe.NewClient(cfg)
|
b.client, err = tfe.NewClient(cfg)
|
||||||
|
@ -303,30 +316,146 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
||||||
return diags
|
return diags
|
||||||
}
|
}
|
||||||
|
|
||||||
// discover the remote backend API service URL and token.
|
// discover the remote backend API service URL and version constraints.
|
||||||
func (b *Remote) discover(hostname string) (*url.URL, error) {
|
func (b *Remote) discover() (*url.URL, *disco.Constraints, error) {
|
||||||
host, err := svchost.ForComparison(hostname)
|
hostname, err := svchost.ForComparison(b.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
service, err := b.services.DiscoverServiceURL(host, tfeServiceID)
|
|
||||||
|
host, err := b.services.Discover(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
return service, nil
|
|
||||||
|
service, err := host.ServiceURL(tfeServiceID)
|
||||||
|
// Return the error, unless its a disco.ErrVersionNotSupported error.
|
||||||
|
if _, ok := err.(*disco.ErrVersionNotSupported); !ok && err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return early if we are a development build.
|
||||||
|
if tfversion.Prerelease == "dev" {
|
||||||
|
return service, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We purposefully ignore the error and return the previous error, as
|
||||||
|
// checking for version constraints is considered optional.
|
||||||
|
constraints, _ := host.VersionConstraints(tfeServiceID, "terraform")
|
||||||
|
|
||||||
|
return service, constraints, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConstraints checks service version constrains against our own
|
||||||
|
// version and returns rich and informational diagnostics in case any
|
||||||
|
// incompatibilities are detected.
|
||||||
|
func (b *Remote) checkConstraints(c *disco.Constraints) tfdiags.Diagnostics {
|
||||||
|
var diags tfdiags.Diagnostics
|
||||||
|
|
||||||
|
if c == nil || c.Minimum == "" || c.Maximum == "" {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a parsable constraints string.
|
||||||
|
excluding := ""
|
||||||
|
if len(c.Excluding) > 0 {
|
||||||
|
excluding = fmt.Sprintf(", != %s", strings.Join(c.Excluding, ", != "))
|
||||||
|
}
|
||||||
|
constStr := fmt.Sprintf(">= %s%s, <= %s", c.Minimum, excluding, c.Maximum)
|
||||||
|
|
||||||
|
// Create the constraints to check against.
|
||||||
|
constraints, err := version.NewConstraint(constStr)
|
||||||
|
if err != nil {
|
||||||
|
return diags.Append(checkConstraintsWarning(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the version to check.
|
||||||
|
v, err := version.NewVersion(tfversion.String())
|
||||||
|
if err != nil {
|
||||||
|
return diags.Append(checkConstraintsWarning(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return if we satisfy all constraints.
|
||||||
|
if constraints.Check(v) {
|
||||||
|
return diags
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find out what action (upgrade/downgrade) we should advice.
|
||||||
|
minimum, err := version.NewVersion(c.Minimum)
|
||||||
|
if err != nil {
|
||||||
|
return diags.Append(checkConstraintsWarning(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
maximum, err := version.NewVersion(c.Maximum)
|
||||||
|
if err != nil {
|
||||||
|
return diags.Append(checkConstraintsWarning(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
var action, toVersion string
|
||||||
|
var excludes []*version.Version
|
||||||
|
switch {
|
||||||
|
case minimum.GreaterThan(v):
|
||||||
|
action = "upgrade"
|
||||||
|
toVersion = ">= " + minimum.String()
|
||||||
|
case maximum.LessThan(v):
|
||||||
|
action = "downgrade"
|
||||||
|
toVersion = "<= " + maximum.String()
|
||||||
|
case len(c.Excluding) > 0:
|
||||||
|
for _, exclude := range c.Excluding {
|
||||||
|
v, err := version.NewVersion(exclude)
|
||||||
|
if err != nil {
|
||||||
|
return diags.Append(checkConstraintsWarning(err))
|
||||||
|
}
|
||||||
|
excludes = append(excludes, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort all the excludes.
|
||||||
|
sort.Sort(version.Collection(excludes))
|
||||||
|
|
||||||
|
// Get the latest excluded version.
|
||||||
|
action = "upgrade"
|
||||||
|
toVersion = "> " + excludes[len(excludes)-1].String()
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(excludes) == 1:
|
||||||
|
excluding = fmt.Sprintf(", excluding version %s", excludes[0].String())
|
||||||
|
case len(excludes) > 1:
|
||||||
|
var vs []string
|
||||||
|
for _, v := range excludes {
|
||||||
|
vs = append(vs, v.String())
|
||||||
|
}
|
||||||
|
excluding = fmt.Sprintf(", excluding versions %s", strings.Join(vs, ", "))
|
||||||
|
default:
|
||||||
|
excluding = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := fmt.Sprintf("Incompatible Terraform version v%s", v.String())
|
||||||
|
details := fmt.Sprintf(
|
||||||
|
"The configured Terraform Enterprise backend is compatible with Terraform "+
|
||||||
|
"versions >= %s, < %s%s.", c.Minimum, c.Maximum, excluding,
|
||||||
|
)
|
||||||
|
|
||||||
|
if action != "" && toVersion != "" {
|
||||||
|
summary = fmt.Sprintf("Please %s Terraform to %s", action, toVersion)
|
||||||
|
details += fmt.Sprintf(" Please %s to a supported version and try again.", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the customized and informational error message.
|
||||||
|
return diags.Append(tfdiags.Sourceless(tfdiags.Error, summary, details))
|
||||||
}
|
}
|
||||||
|
|
||||||
// token returns the token for this host as configured in the credentials
|
// token returns the token for this host as configured in the credentials
|
||||||
// section of the CLI Config File. If no token was configured, an empty
|
// section of the CLI Config File. If no token was configured, an empty
|
||||||
// string will be returned instead.
|
// string will be returned instead.
|
||||||
func (b *Remote) token(hostname string) (string, error) {
|
func (b *Remote) token() (string, error) {
|
||||||
host, err := svchost.ForComparison(hostname)
|
hostname, err := svchost.ForComparison(b.hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
creds, err := b.services.CredentialsForHost(host)
|
creds, err := b.services.CredentialsForHost(hostname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", host, err)
|
log.Printf("[WARN] Failed to get credentials for %s: %s (ignoring)", b.hostname, err)
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
if creds != nil {
|
if creds != nil {
|
||||||
|
@ -444,8 +573,8 @@ func (b *Remote) StateMgr(name string) (state.State, error) {
|
||||||
|
|
||||||
// We only set the Terraform Version for the new workspace if this is
|
// We only set the Terraform Version for the new workspace if this is
|
||||||
// a release candidate or a final release.
|
// a release candidate or a final release.
|
||||||
if version.Prerelease == "" || strings.HasPrefix(version.Prerelease, "rc") {
|
if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") {
|
||||||
options.TerraformVersion = tfe.String(version.String())
|
options.TerraformVersion = tfe.String(tfversion.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
|
workspace, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
|
||||||
|
@ -709,6 +838,15 @@ func generalError(msg string, err error) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func checkConstraintsWarning(err error) tfdiags.Diagnostic {
|
||||||
|
return tfdiags.Sourceless(
|
||||||
|
tfdiags.Warning,
|
||||||
|
fmt.Sprintf("Failed to check version constraints: %v", err),
|
||||||
|
"Checking version constraints is considered optional, but this is an"+
|
||||||
|
"unexpected error which should be reported.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const operationCanceled = `
|
const operationCanceled = `
|
||||||
[reset][red]The remote operation was successfully cancelled.[reset]
|
[reset][red]The remote operation was successfully cancelled.[reset]
|
||||||
`
|
`
|
||||||
|
|
|
@ -6,6 +6,8 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/backend"
|
"github.com/hashicorp/terraform/backend"
|
||||||
|
"github.com/hashicorp/terraform/svchost/disco"
|
||||||
|
"github.com/hashicorp/terraform/version"
|
||||||
"github.com/zclconf/go-cty/cty"
|
"github.com/zclconf/go-cty/cty"
|
||||||
|
|
||||||
backendLocal "github.com/hashicorp/terraform/backend/local"
|
backendLocal "github.com/hashicorp/terraform/backend/local"
|
||||||
|
@ -241,3 +243,95 @@ func TestRemote_addAndRemoveWorkspacesNoDefault(t *testing.T) {
|
||||||
t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states)
|
t.Fatalf("expected %#+v, got %#+v", expectedWorkspaces, states)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemote_checkConstraints(t *testing.T) {
|
||||||
|
b := testBackendDefault(t)
|
||||||
|
|
||||||
|
cases := map[string]struct {
|
||||||
|
constraints *disco.Constraints
|
||||||
|
prerelease string
|
||||||
|
version string
|
||||||
|
result string
|
||||||
|
}{
|
||||||
|
"compatible version": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.11.1",
|
||||||
|
result: "",
|
||||||
|
},
|
||||||
|
"version too old": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.10.1",
|
||||||
|
result: "upgrade Terraform to >= 0.11.0",
|
||||||
|
},
|
||||||
|
"version too new": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.12.0",
|
||||||
|
result: "downgrade Terraform to <= 0.11.11",
|
||||||
|
},
|
||||||
|
"version excluded - ordered": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Excluding: []string{"0.11.7", "0.11.8"},
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.11.7",
|
||||||
|
result: "upgrade Terraform to > 0.11.8",
|
||||||
|
},
|
||||||
|
"version excluded - unordered": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Excluding: []string{"0.11.8", "0.11.6"},
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.11.6",
|
||||||
|
result: "upgrade Terraform to > 0.11.8",
|
||||||
|
},
|
||||||
|
"list versions": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.10.1",
|
||||||
|
result: "versions >= 0.11.0, < 0.11.11.",
|
||||||
|
},
|
||||||
|
"list exclusion": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Excluding: []string{"0.11.6"},
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.11.6",
|
||||||
|
result: "excluding version 0.11.6.",
|
||||||
|
},
|
||||||
|
"list exclusions": {
|
||||||
|
constraints: &disco.Constraints{
|
||||||
|
Minimum: "0.11.0",
|
||||||
|
Excluding: []string{"0.11.8", "0.11.6"},
|
||||||
|
Maximum: "0.11.11",
|
||||||
|
},
|
||||||
|
version: "0.11.6",
|
||||||
|
result: "excluding versions 0.11.6, 0.11.8.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range cases {
|
||||||
|
version.Prerelease = tc.prerelease
|
||||||
|
version.Version = tc.version
|
||||||
|
|
||||||
|
// Check the constraints.
|
||||||
|
diags := b.checkConstraints(tc.constraints)
|
||||||
|
if (diags.Err() != nil || tc.result != "") &&
|
||||||
|
(diags.Err() == nil || !strings.Contains(diags.Err().Error(), tc.result)) {
|
||||||
|
t.Fatalf("%s: unexpected constraints result: %v", name, diags.Err())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -42,6 +42,9 @@ type ErrServiceNotProvided struct {
|
||||||
|
|
||||||
// Error returns a customized error message.
|
// Error returns a customized error message.
|
||||||
func (e *ErrServiceNotProvided) Error() string {
|
func (e *ErrServiceNotProvided) Error() string {
|
||||||
|
if e.hostname == "" {
|
||||||
|
return fmt.Sprintf("host does not provide a %s service", e.service)
|
||||||
|
}
|
||||||
return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service)
|
return fmt.Sprintf("host %s does not provide a %s service", e.hostname, e.service)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -54,6 +57,9 @@ type ErrVersionNotSupported struct {
|
||||||
|
|
||||||
// Error returns a customized error message.
|
// Error returns a customized error message.
|
||||||
func (e *ErrVersionNotSupported) Error() string {
|
func (e *ErrVersionNotSupported) Error() string {
|
||||||
|
if e.hostname == "" {
|
||||||
|
return fmt.Sprintf("host does not support %s version %s", e.service, e.version)
|
||||||
|
}
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,7 +90,7 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
||||||
|
|
||||||
// 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: svc}
|
return nil, &ErrServiceNotProvided{service: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
urlStr, ok := h.services[id].(string)
|
urlStr, ok := h.services[id].(string)
|
||||||
|
@ -155,7 +161,7 @@ func (h *Host) VersionConstraints(id, product string) (*Constraints, error) {
|
||||||
|
|
||||||
// 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: svc}
|
return nil, &ErrServiceNotProvided{service: svc}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to get the service URL for the version service and
|
// Try to get the service URL for the version service and
|
||||||
|
|
Loading…
Reference in New Issue