Skip to content

Commit b43daea

Browse files
nfagerlundchrisarcand
authored andcommitted
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.
1 parent 0bae48b commit b43daea

1 file changed

Lines changed: 50 additions & 18 deletions

File tree

‎internal/cloud/backend.go‎

Lines changed: 50 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -742,10 +742,12 @@ func (b *Cloud) IgnoreVersionConflict() {
742742
}
743743

744744
// VerifyWorkspaceTerraformVersion compares the local Terraform version against
745-
// the workspace's configured Terraform version. If they are equal, this means
746-
// that there are no compatibility concerns, so it returns no diagnostics.
745+
// the workspace's configured Terraform version. If they are compatible, this
746+
// means that there are no state compatibility concerns, so it returns no
747+
// diagnostics.
747748
//
748-
// If the versions differ,
749+
// If the versions aren't compatible, it returns an error (or, if
750+
// b.ignoreVersionConflict is set, a warning).
749751
func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Diagnostics {
750752
var diags tfdiags.Diagnostics
751753

@@ -779,13 +781,55 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di
779781
return nil
780782
}
781783

784+
// Even if ignoring version conflicts, it may still be useful to call this
785+
// method and warn the user about a mismatch between the local and remote
786+
// Terraform versions.
787+
severity := tfdiags.Error
788+
if b.ignoreVersionConflict {
789+
severity = tfdiags.Warning
790+
}
791+
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."
792+
if b.ignoreVersionConflict {
793+
suggestion = ""
794+
}
795+
782796
remoteVersion, err := version.NewSemver(workspace.TerraformVersion)
783797
if err != nil {
798+
// If it's not a valid version, it might be a valid version constraint:
799+
remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion)
800+
if err != nil {
801+
diags = diags.Append(tfdiags.Sourceless(
802+
tfdiags.Error,
803+
"Error looking up workspace",
804+
fmt.Sprintf("Invalid Terraform version or version constraint: %s", err),
805+
))
806+
return diags
807+
}
808+
809+
// Avoiding tfversion.SemVer because it omits the prerelease prefix, and we
810+
// want constraints like `~> 1.2.0-beta1` to be possible.
811+
fullTfversion := version.Must(version.NewSemver(tfversion.String()))
812+
813+
// If it's a constraint, we only ensure that the local version meets it.
814+
// This can result in both false positives and false negatives, but in the
815+
// most common case (~> x.y.z) it's useful enough.
816+
if remoteConstraint.Check(fullTfversion) {
817+
return diags
818+
}
819+
784820
diags = diags.Append(tfdiags.Sourceless(
785-
tfdiags.Error,
786-
"Error looking up workspace",
787-
fmt.Sprintf("Invalid Terraform version: %s", err),
821+
severity,
822+
"Terraform version mismatch",
823+
fmt.Sprintf(
824+
"The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).%s",
825+
tfversion.String(),
826+
b.organization,
827+
workspace.Name,
828+
workspace.TerraformVersion,
829+
suggestion,
830+
),
788831
))
832+
789833
return diags
790834
}
791835

@@ -818,18 +862,6 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di
818862
}
819863
}
820864

821-
// Even if ignoring version conflicts, it may still be useful to call this
822-
// method and warn the user about a mismatch between the local and remote
823-
// Terraform versions.
824-
severity := tfdiags.Error
825-
if b.ignoreVersionConflict {
826-
severity = tfdiags.Warning
827-
}
828-
829-
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."
830-
if b.ignoreVersionConflict {
831-
suggestion = ""
832-
}
833865
diags = diags.Append(tfdiags.Sourceless(
834866
severity,
835867
"Terraform version mismatch",

0 commit comments

Comments
 (0)