Skip to content

Commit baa72ce

Browse files
nfagerlundchrisarcand
authored andcommitted
Simplify logic flow: everything is a constraint
Explicit version strings are actually also version constraints! And the special comparisons we were doing to allow a range of compatible versions can also be expressed as version constraints. Bonus: also simplify the way we handle version check errors, by composing the messages inline and only extracting the repetitive parts into a function.
1 parent 83538fd commit baa72ce

3 files changed

Lines changed: 58 additions & 85 deletions

File tree

‎internal/cloud/backend.go‎

Lines changed: 50 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -781,66 +781,66 @@ func (b *Cloud) VerifyWorkspaceTerraformVersion(workspaceName string) tfdiags.Di
781781
return nil
782782
}
783783

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-
remoteVersion, err := version.NewSemver(workspace.TerraformVersion)
784+
remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion)
788785
if err != nil {
789-
log.Printf("[DEBUG] Invalid Terraform version (%s); will try to parse as version constraint", workspace.TerraformVersion)
786+
message := fmt.Sprintf(
787+
"The remote workspace specified an invalid Terraform version or constraint (%s), "+
788+
"and it isn't possible to determine whether the local Terraform version (%s) is compatible.",
789+
workspace.TerraformVersion,
790+
tfversion.String(),
791+
)
792+
diags = diags.Append(incompatibleWorkspaceTerraformVersion(message, b.ignoreVersionConflict))
793+
return diags
790794
}
791-
if remoteVersion == nil {
792-
// If it's not a valid version, it might be a valid version constraint:
793-
remoteConstraint, err := version.NewConstraint(workspace.TerraformVersion)
794-
if err != nil {
795-
diags = diags.Append(terraformInvalidVersionOrConstraint(b.ignoreVersionConflict, workspace.TerraformVersion))
796-
return diags
797-
}
798795

799-
// Avoiding tfversion.SemVer because it omits the prerelease prefix, and we
800-
// want constraints like `~> 1.2.0-beta1` to be possible.
801-
fullTfversion := version.Must(version.NewSemver(tfversion.String()))
796+
// If the workspace has a literal Terraform version, see if we can use a
797+
// looser version constraint.
798+
remoteVersion, _ := version.NewSemver(workspace.TerraformVersion)
799+
if remoteVersion != nil {
800+
v014 := version.Must(version.NewSemver("0.14.0"))
801+
v120 := version.Must(version.NewSemver("1.2.0"))
802802

803-
// If it's a constraint, we only ensure that the local version meets it.
804-
// This can result in both false positives and false negatives, but in the
805-
// most common case (~> x.y.z) it's useful enough.
806-
if remoteConstraint.Check(fullTfversion) {
807-
return diags
803+
// Versions from 0.14 through the early 1.x series should be compatible
804+
// (though we don't know about 1.2 yet).
805+
if remoteVersion.GreaterThanOrEqual(v014) && remoteVersion.LessThan(v120) {
806+
early1xCompatible, err := version.NewConstraint(fmt.Sprintf(">= 0.14.0, < %s", v120.String()))
807+
if err != nil {
808+
panic(err)
809+
}
810+
remoteConstraint = early1xCompatible
808811
}
809812

810-
diags = diags.Append(terraformMismatchDiagnostic(b.ignoreVersionConflict, b.organization, workspace, tfversion.String()))
811-
return diags
812-
}
813-
814-
v014 := version.Must(version.NewSemver("0.14.0"))
815-
if tfversion.SemVer.LessThan(v014) || remoteVersion.LessThan(v014) {
816-
// Versions of Terraform prior to 0.14.0 will refuse to load state files
817-
// written by a newer version of Terraform, even if it is only a patch
818-
// level difference. As a result we require an exact match.
819-
if tfversion.SemVer.Equal(remoteVersion) {
820-
return diags
813+
// Any future new state format will require at least a minor version
814+
// increment, so x.y.* will always be compatible with each other.
815+
if remoteVersion.GreaterThanOrEqual(v120) {
816+
rwvs := remoteVersion.Segments64()
817+
if len(rwvs) >= 3 {
818+
// ~> x.y.0
819+
minorVersionCompatible, err := version.NewConstraint(fmt.Sprintf("~> %d.%d.0", rwvs[0], rwvs[1]))
820+
if err != nil {
821+
panic(err)
822+
}
823+
remoteConstraint = minorVersionCompatible
824+
}
821825
}
822826
}
823-
if tfversion.SemVer.GreaterThanOrEqual(v014) && remoteVersion.GreaterThanOrEqual(v014) {
824-
// Versions of Terraform after 0.14.0 should be compatible with each
825-
// other. At the time this code was written, the only constraints we
826-
// are aware of are:
827-
//
828-
// - 0.14.0 is guaranteed to be compatible with versions up to but not
829-
// including 1.2.0
830-
v120 := version.Must(version.NewSemver("1.2.0"))
831-
if tfversion.SemVer.LessThan(v120) && remoteVersion.LessThan(v120) {
832-
return diags
833-
}
834-
// - Any new Terraform state version will require at least minor patch
835-
// increment, so x.y.* will always be compatible with each other
836-
tfvs := tfversion.SemVer.Segments64()
837-
rwvs := remoteVersion.Segments64()
838-
if len(tfvs) == 3 && len(rwvs) == 3 && tfvs[0] == rwvs[0] && tfvs[1] == rwvs[1] {
839-
return diags
840-
}
827+
828+
// Re-parsing tfversion.String because tfversion.SemVer omits the prerelease
829+
// prefix, and we want to allow constraints like `~> 1.2.0-beta1`.
830+
fullTfversion := version.Must(version.NewSemver(tfversion.String()))
831+
832+
if remoteConstraint.Check(fullTfversion) {
833+
return diags
841834
}
842835

843-
diags = diags.Append(terraformMismatchDiagnostic(b.ignoreVersionConflict, b.organization, workspace, tfversion.String()))
836+
message := fmt.Sprintf(
837+
"The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).",
838+
tfversion.String(),
839+
b.organization,
840+
workspace.Name,
841+
workspace.TerraformVersion,
842+
)
843+
diags = diags.Append(incompatibleWorkspaceTerraformVersion(message, b.ignoreVersionConflict))
844844
return diags
845845
}
846846

‎internal/cloud/backend_test.go‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion(t *testing.T) {
656656
if len(diags) != 1 {
657657
t.Fatal("expected diag, but none returned")
658658
}
659-
if got := diags.Err().Error(); !strings.Contains(got, "Terraform version mismatch") {
659+
if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version") {
660660
t.Fatalf("unexpected error: %s", got)
661661
}
662662
} else {
@@ -705,7 +705,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion_workspaceErrors(t *testing.T) {
705705
if len(diags) != 1 {
706706
t.Fatal("expected diag, but none returned")
707707
}
708-
if got := diags.Err().Error(); !strings.Contains(got, "Terraform version error: The remote workspace specified") {
708+
if got := diags.Err().Error(); !strings.Contains(got, "Incompatible Terraform version: The remote workspace specified") {
709709
t.Fatalf("unexpected error: %s", got)
710710
}
711711
}
@@ -757,7 +757,7 @@ func TestCloud_VerifyWorkspaceTerraformVersion_ignoreFlagSet(t *testing.T) {
757757
if got, want := diags[0].Severity(), tfdiags.Warning; got != want {
758758
t.Errorf("wrong severity: got %#v, want %#v", got, want)
759759
}
760-
if got, want := diags[0].Description().Summary, "Terraform version mismatch"; got != want {
760+
if got, want := diags[0].Description().Summary, "Incompatible Terraform version"; got != want {
761761
t.Errorf("wrong summary: got %s, want %s", got, want)
762762
}
763763
wantDetail := "The local Terraform version (0.14.0) does not meet the version requirements for remote workspace hashicorp/app-prod (0.13.5)."

‎internal/cloud/errors.go‎

Lines changed: 5 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"fmt"
55
"strings"
66

7-
tfe "github.com/hashicorp/go-tfe"
87
"github.com/hashicorp/terraform/internal/tfdiags"
98
"github.com/zclconf/go-cty/cty"
109
)
@@ -32,41 +31,15 @@ var (
3231
)
3332
)
3433

35-
func terraformMismatchDiagnostic(ignoreVersionConflict bool, organization string, workspace *tfe.Workspace, tfversion string) tfdiags.Diagnostic {
36-
severity := tfdiags.Error
37-
if ignoreVersionConflict {
38-
severity = tfdiags.Warning
39-
}
40-
41-
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."
42-
if ignoreVersionConflict {
43-
suggestion = ""
44-
}
34+
const ignoreRemoteVersionHelp = "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."
4535

46-
description := fmt.Sprintf(
47-
"The local Terraform version (%s) does not meet the version requirements for remote workspace %s/%s (%s).\n\n%s",
48-
tfversion,
49-
organization,
50-
workspace.Name,
51-
workspace.TerraformVersion,
52-
suggestion,
53-
)
54-
description = strings.TrimSpace(description)
55-
return tfdiags.Sourceless(severity, "Terraform version mismatch", description)
56-
}
57-
58-
func terraformInvalidVersionOrConstraint(ignoreVersionConflict bool, tfversion string) tfdiags.Diagnostic {
36+
func incompatibleWorkspaceTerraformVersion(message string, ignoreVersionConflict bool) tfdiags.Diagnostic {
5937
severity := tfdiags.Error
38+
suggestion := ignoreRemoteVersionHelp
6039
if ignoreVersionConflict {
6140
severity = tfdiags.Warning
62-
}
63-
64-
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."
65-
if ignoreVersionConflict {
6641
suggestion = ""
6742
}
68-
69-
description := fmt.Sprintf("The remote workspace specified an invalid Terraform version or version constraint: %s\n\n%s", tfversion, suggestion)
70-
description = strings.TrimSpace(description)
71-
return tfdiags.Sourceless(severity, "Terraform version error", description)
43+
description := strings.TrimSpace(fmt.Sprintf("%s\n\n%s", message, suggestion))
44+
return tfdiags.Sourceless(severity, "Incompatible Terraform version", description)
7245
}

0 commit comments

Comments
 (0)