Cloud backend: accept version constraints from workspaces
The cloud backend (and remote before it) previously expected a TFC workspace's `terraform-version` attribute to be either the magic string `"latest"` or an explicit semver value. But a workspace might have a version constraint instead (like `~> 1.1.0`), in which case the version check would blow up. This commit checks whether `terraform-version` is a valid version constraint before erroring out, and if so, returns success if the local version meets the constraint. Because it's not practical to deeply introspect the slice of version space defined by a constraint, this check is slightly less robust than the version comparisons below it: - It can give a false OK on open-ended constraints like `>= 1.1.0`. Say you're running 1.3.0, it changed the state format, and the TFE instance admin has not yet added any 1.3.x Terraform versions; your workspace will now break. - It will give a false not-OK when using different minor versions within a range that we know to be compatible, e.g. remote constraint of `~> 0.15.0` and local version of 1.1.0. - This would be totally useless with the pre-0.14 versions of Terraform, where patch releases could change state format... but we're not going back in time to add this feature to them anyway. Still, in the most common likely case (`~> x.y.z`), it'll complain at you (with an error you can choose to override) if you're not using the same minor version, and that seems proportionate, useful, and expected.
This commit is contained in:
parent
0bae48bc01
commit
b43daeaa8d
|
@ -742,10 +742,12 @@ func (b *Cloud) IgnoreVersionConflict() {
|
|||
}
|
||||
|
||||
// VerifyWorkspaceTerraformVersion compares the local Terraform version against
|
||||
// the workspace's configured Terraform version. If they are equal, this means
|
||||
// that there are no compatibility concerns, so it returns no diagnostics.
|
||||
// the workspace's configured Terraform version. If they are compatible, this
|
||||
// means that there are no state compatibility concerns, so it returns no
|
||||
// diagnostics.
|
||||
//
|
||||
// If the versions differ,
|
||||
// If the versions aren't compatible, it returns an error (or, if
|
||||
// b.ignoreVersionConflict is set, a warning).
|
||||
func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Diagnostics {
|
||||
var diags tfdiags.Diagnostics
|
||||
|
||||
|
@ -779,16 +781,58 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di
|
|||
return nil
|
||||
}
|
||||
|
||||
// Even if ignoring version conflicts, it may still be useful to call this
|
||||
// method and warn the user about a mismatch between the local and remote
|
||||
// Terraform versions.
|
||||
severity := tfdiags.Error
|
||||
if b.ignoreVersionConflict {
|
||||
severity = tfdiags.Warning
|
||||
}
|
||||
suggestion := " If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace."
|
||||
if b.ignoreVersionConflict {
|
||||
suggestion = ""
|
||||
}
|
||||
|
||||
remoteVersion, err := version.NewSemver(workspace.TerraformVersion)
|
||||
if err != nil {
|
||||
// If it's not a valid version, it might be a valid version constraint:
|
||||
remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion)
|
||||
if err != nil {
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
tfdiags.Error,
|
||||
"Error looking up workspace",
|
||||
fmt.Sprintf("Invalid Terraform version: %s", err),
|
||||
fmt.Sprintf("Invalid Terraform version or version constraint: %s", err),
|
||||
))
|
||||
return diags
|
||||
}
|
||||
|
||||
// Avoiding tfversion.SemVer because it omits the prerelease prefix, and we
|
||||
// want constraints like `~> 1.2.0-beta1` to be possible.
|
||||
fullTfversion := version.Must(version.NewSemver(tfversion.String()))
|
||||
|
||||
// If it's a constraint, we only ensure that the local version meets it.
|
||||
// This can result in both false positives and false negatives, but in the
|
||||
// most common case (~> x.y.z) it's useful enough.
|
||||
if remoteConstraint.Check(fullTfversion) {
|
||||
return diags
|
||||
}
|
||||
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
severity,
|
||||
"Terraform version mismatch",
|
||||
fmt.Sprintf(
|
||||
"The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).%s",
|
||||
tfversion.String(),
|
||||
b.organization,
|
||||
workspace.Name,
|
||||
workspace.TerraformVersion,
|
||||
suggestion,
|
||||
),
|
||||
))
|
||||
|
||||
return diags
|
||||
}
|
||||
|
||||
v014 := version.Must(version.NewSemver("0.14.0"))
|
||||
if tfversion.SemVer.LessThan(v014) || remoteVersion.LessThan(v014) {
|
||||
// Versions of Terraform prior to 0.14.0 will refuse to load state files
|
||||
|
@ -818,18 +862,6 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di
|
|||
}
|
||||
}
|
||||
|
||||
// Even if ignoring version conflicts, it may still be useful to call this
|
||||
// method and warn the user about a mismatch between the local and remote
|
||||
// Terraform versions.
|
||||
severity := tfdiags.Error
|
||||
if b.ignoreVersionConflict {
|
||||
severity = tfdiags.Warning
|
||||
}
|
||||
|
||||
suggestion := " If you're sure you want to upgrade the state, you can force Terraform to continue using the -ignore-remote-version flag. This may result in an unusable workspace."
|
||||
if b.ignoreVersionConflict {
|
||||
suggestion = ""
|
||||
}
|
||||
diags = diags.Append(tfdiags.Sourceless(
|
||||
severity,
|
||||
"Terraform version mismatch",
|
||||
|
|
Loading…
Reference in New Issue