Merge pull request #19659 from hashicorp/svh/f-check-constraints
backend/remote: return detailed incompatibility info
This commit is contained in:
commit
2ef8315885
|
@ -12,6 +12,7 @@ import (
|
|||
"sync"
|
||||
|
||||
tfe "github.com/hashicorp/go-tfe"
|
||||
version "github.com/hashicorp/go-version"
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/configs/configschema"
|
||||
"github.com/hashicorp/terraform/helper/schema"
|
||||
|
@ -21,7 +22,7 @@ import (
|
|||
"github.com/hashicorp/terraform/svchost/disco"
|
||||
"github.com/hashicorp/terraform/terraform"
|
||||
"github.com/hashicorp/terraform/tfdiags"
|
||||
"github.com/hashicorp/terraform/version"
|
||||
tfversion "github.com/hashicorp/terraform/version"
|
||||
"github.com/mitchellh/cli"
|
||||
"github.com/mitchellh/colorstring"
|
||||
"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
|
||||
// a remote backend API and to discover the required base path.
|
||||
service, err := b.discover(b.hostname)
|
||||
// a remote backend API and to get the version constraints.
|
||||
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 {
|
||||
diags = diags.Append(tfdiags.AttributeValue(
|
||||
tfdiags.Error,
|
||||
|
@ -223,18 +246,8 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
// Retrieve the token for this host as configured in the 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
|
||||
}
|
||||
// Get the token from the config if no token was configured for this
|
||||
// host in credentials section of the CLI Config File.
|
||||
if token == "" {
|
||||
if val := obj.GetAttr("token"); !val.IsNull() {
|
||||
token = val.AsString()
|
||||
|
@ -262,7 +275,7 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
}
|
||||
|
||||
// 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.
|
||||
b.client, err = tfe.NewClient(cfg)
|
||||
|
@ -303,30 +316,146 @@ func (b *Remote) Configure(obj cty.Value) tfdiags.Diagnostics {
|
|||
return diags
|
||||
}
|
||||
|
||||
// discover the remote backend API service URL and token.
|
||||
func (b *Remote) discover(hostname string) (*url.URL, error) {
|
||||
host, err := svchost.ForComparison(hostname)
|
||||
// discover the remote backend API service URL and version constraints.
|
||||
func (b *Remote) discover() (*url.URL, *disco.Constraints, error) {
|
||||
hostname, err := svchost.ForComparison(b.hostname)
|
||||
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 {
|
||||
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
|
||||
// section of the CLI Config File. If no token was configured, an empty
|
||||
// string will be returned instead.
|
||||
func (b *Remote) token(hostname string) (string, error) {
|
||||
host, err := svchost.ForComparison(hostname)
|
||||
func (b *Remote) token() (string, error) {
|
||||
hostname, err := svchost.ForComparison(b.hostname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
creds, err := b.services.CredentialsForHost(host)
|
||||
creds, err := b.services.CredentialsForHost(hostname)
|
||||
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
|
||||
}
|
||||
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
|
||||
// a release candidate or a final release.
|
||||
if version.Prerelease == "" || strings.HasPrefix(version.Prerelease, "rc") {
|
||||
options.TerraformVersion = tfe.String(version.String())
|
||||
if tfversion.Prerelease == "" || strings.HasPrefix(tfversion.Prerelease, "rc") {
|
||||
options.TerraformVersion = tfe.String(tfversion.String())
|
||||
}
|
||||
|
||||
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 = `
|
||||
[reset][red]The remote operation was successfully cancelled.[reset]
|
||||
`
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/hashicorp/terraform/backend"
|
||||
"github.com/hashicorp/terraform/svchost/disco"
|
||||
"github.com/hashicorp/terraform/version"
|
||||
"github.com/zclconf/go-cty/cty"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -54,6 +57,9 @@ type ErrVersionNotSupported struct {
|
|||
|
||||
// Error returns a customized error message.
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -84,7 +90,7 @@ func (h *Host) ServiceURL(id string) (*url.URL, error) {
|
|||
|
||||
// No services supported for an empty Host.
|
||||
if h == nil || h.services == nil {
|
||||
return nil, &ErrServiceNotProvided{hostname: "<unknown>", service: svc}
|
||||
return nil, &ErrServiceNotProvided{service: svc}
|
||||
}
|
||||
|
||||
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.
|
||||
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
|
||||
|
|
Loading…
Reference in New Issue