Skip to content

Commit

Permalink
Merge pull request #19660 from hashicorp/svh/f-backports
Browse files Browse the repository at this point in the history
backport: discovery and version constraints changes
  • Loading branch information
Sander van Harmelen authored Dec 14, 2018
2 parents ec66613 + 18c3ca8 commit bdd272d
Show file tree
Hide file tree
Showing 15 changed files with 1,001 additions and 194 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ script:
branches:
only:
- master
- v0.11
notifications:
irc:
channels:
Expand Down
192 changes: 167 additions & 25 deletions backend/remote/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import (
"sync"

tfe "github.com/hashicorp/go-tfe"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/backend"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/svchost"
"github.com/hashicorp/terraform/svchost/disco"
"github.com/hashicorp/terraform/terraform"
"github.com/hashicorp/terraform/version"
tfversion "github.com/hashicorp/terraform/version"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
)
Expand All @@ -28,7 +29,7 @@ const (
defaultHostname = "app.terraform.io"
defaultModuleDepth = -1
defaultParallelism = 10
serviceID = "tfe.v2"
tfeServiceID = "tfe.v2"
)

// Remote is an implementation of EnhancedBackend that performs all
Expand Down Expand Up @@ -152,22 +153,43 @@ func (b *Remote) configure(ctx context.Context) error {
}

// 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)
if err != nil {
return err
// a remote backend API and to get the version constraints.
service, constraints, discoErr := b.discover()
if _, ok := discoErr.(*disco.ErrVersionNotSupported); !ok && discoErr != nil {
return discoErr
}

// Check any retrieved constraints to make sure we are compatible.
if constraints == nil {
if err := b.checkConstraints(constraints); err != nil {
return err
}
}

// When checking version constraints silently failed, we return the
// more generic error we received during the service discovery.
if discoErr != nil {
return discoErr
}

// Retrieve the token for this host as configured in the credentials
// section of the CLI Config File.
token, err := b.token(b.hostname)
token, err := b.token()
if err != nil {
return err
}

// Get the token from the config if no token was configured for this
// host in credentials section of the CLI Config File.
if token == "" {
token = d.Get("token").(string)
}

// Return an error if we still don't have a token at this point.
if token == "" {
return fmt.Errorf("required token could not be found")
}

cfg := &tfe.Config{
Address: service.String(),
BasePath: service.Path,
Expand All @@ -176,7 +198,7 @@ func (b *Remote) configure(ctx context.Context) error {
}

// 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)
Expand All @@ -187,30 +209,143 @@ func (b *Remote) configure(ctx context.Context) error {
return nil
}

// 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
}

host, err := b.services.Discover(hostname)
if err != nil {
return nil, nil, err
}

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) error {
if c == nil || c.Minimum == "" || c.Maximum == "" {
return nil
}

// 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 checkConstraintsWarning(err)
}
service := b.services.DiscoverServiceURL(host, serviceID)
if service == nil {
return nil, fmt.Errorf("host %s does not provide a remote backend API", host)

// Create the version to check.
v, err := version.NewVersion(tfversion.String())
if err != nil {
return checkConstraintsWarning(err)
}

// Return if we satisfy all constraints.
if constraints.Check(v) {
return nil
}
return service, nil

// Find out what action (upgrade/downgrade) we should advice.
minimum, err := version.NewVersion(c.Minimum)
if err != nil {
return checkConstraintsWarning(err)
}

maximum, err := version.NewVersion(c.Maximum)
if err != nil {
return 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 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\n"+
"versions >= %s, < %s%s.", c.Minimum, c.Maximum, excluding,
)

if action != "" && toVersion != "" {
summary = fmt.Sprintf("Please %s Terraform to %s", action, toVersion)
}

// Return the customized and informational error message.
return fmt.Errorf("%s\n\n%s", 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 {
Expand Down Expand Up @@ -273,8 +408,8 @@ func (b *Remote) State(workspace 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())
}

_, err = b.client.Workspaces.Create(context.Background(), b.organization, options)
Expand Down Expand Up @@ -409,9 +544,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
Expand Down Expand Up @@ -540,6 +673,15 @@ func generalError(msg string, err error) error {
}
}

func checkConstraintsWarning(err error) error {
return fmt.Errorf(
"Failed to check version constraints: %v\n\n"+
"Checking version constraints is considered optional, but this is an\n"+
"unexpected error which should be reported.",
err,
)
}

const generalErr = `
%s: %v
Expand Down
Loading

0 comments on commit bdd272d

Please sign in to comment.