Repository: warrensbox/terraform-switcher (go)

Issue:
Implement support for multiple Terraform version constraints from the module directory
When parsing Terraform version constraint from the module directory, only the first constraint found is taken into account, whilst Terraform configuration supports multiple `terraform {}` configuration blocks and each may have its own `required_version` parameter.
To improve Terraform versions detection the code needs to be adjusted to gather all the constraints and leverage combined constraint string to detect allowed required Terraform version.

Refs:
* Relevant existing code: https://github.com/warrensbox/terraform-switcher/blob/f268ab5aa580432d9f29fccd8198215afd5d62cd/main.go#L443
* Discussion: https://github.com/warrensbox/terraform-switcher/pull/356#discussion_r1550018910
* Simplified implementation example:
  ```Go
  func installTFProvidedModule(dir string, custBinPath, mirrorURL *string) {
	  fmt.Printf("Reading required version from terraform file\n")
	  module, _ := tfconfig.LoadModule(dir)
      var tfconstraints []string
      for key := range module.RequiredCore {
          tfconstraints = append(tfconstraints, module.RequiredCore[key])
      }
      tfconstraint := strings.Join(tfconstraints, ", ")
	  installFromConstraint(&tfconstraint, custBinPath, mirrorURL)
  }
  ```


Discussion:

Patch:
diff --git a/lib/param_parsing/parameters.go b/lib/param_parsing/parameters.go
--- a/lib/param_parsing/parameters.go
+++ b/lib/param_parsing/parameters.go
@@ -60,7 +60,7 @@ func GetParameters() Params {
 	} else if terraformVersionFileExists(params) {
 		params, err = GetParamsFromTerraformVersion(params)
 	} else if isTerraformModule(params) {
-		params, _ = GetVersionFromVersionsTF(params)
+		params, err = GetVersionFromVersionsTF(params)
 	} else if terraGruntFileExists(params) {
 		params, err = GetVersionFromTerragrunt(params)
 	} else {
diff --git a/lib/param_parsing/versiontf.go b/lib/param_parsing/versiontf.go
--- a/lib/param_parsing/versiontf.go
+++ b/lib/param_parsing/versiontf.go
@@ -1,18 +1,69 @@
 package param_parsing
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+
 	"github.com/hashicorp/terraform-config-inspect/tfconfig"
 	"github.com/warrensbox/terraform-switcher/lib"
 )
 
 func GetVersionFromVersionsTF(params Params) (Params, error) {
-	logger.Infof("Reading version from terraform module at %q", params.ChDirPath)
-	module, err := tfconfig.LoadModule(params.ChDirPath)
+	var tfConstraints []string
+	var exactConstraints []string
+
+	curDir, err := os.Getwd()
+	if err != nil {
+		logger.Fatalf("Could not get current working directory: %v", err)
+	}
+
+	absPath := params.ChDirPath
+	if !filepath.IsAbs(params.ChDirPath) {
+		absPath, err = filepath.Abs(params.ChDirPath)
+		if err != nil {
+			logger.Fatalf("Could not derive absolute path to %q: %v", params.ChDirPath, err)
+		}
+	}
+
+	relPath, err := filepath.Rel(curDir, absPath)
 	if err != nil {
-		logger.Errorf("Could not load terraform module at %q", params.ChDirPath)
-		return params, err.Err()
+		logger.Fatalf("Could not derive relative path to %q: %v", params.ChDirPath, err)
+	}
+
+	logger.Infof("Reading version from Terraform module at %q", relPath)
+	module, _ := tfconfig.LoadModule(params.ChDirPath)
+	if module.Diagnostics.HasErrors() {
+		logger.Fatalf("Could not load Terraform module at %q", params.ChDirPath)
 	}
-	tfConstraint := module.RequiredCore[0]
+
+	requiredVersions := module.RequiredCore
+
+	for key := range requiredVersions {
+		tfConstraint := requiredVersions[key]
+		tfConstraintParts := strings.Fields(tfConstraint)
+
+		if len(tfConstraintParts) > 2 {
+			logger.Fatalf("Invalid version constraint found: %q", tfConstraint)
+		} else if len(tfConstraintParts) == 1 {
+			exactConstraints = append(exactConstraints, tfConstraint)
+			tfConstraint = "= " + tfConstraintParts[0]
+		}
+
+		if tfConstraintParts[0] == "=" {
+			exactConstraints = append(exactConstraints, tfConstraint)
+		}
+
+		tfConstraints = append(tfConstraints, tfConstraint)
+	}
+
+	if len(exactConstraints) > 0 && len(tfConstraints) > 1 {
+		return params, fmt.Errorf("exact constraint (%q) cannot be combined with other conditions", strings.Join(exactConstraints, ", "))
+	}
+
+	tfConstraint := strings.Join(tfConstraints, ", ")
+
 	version, err2 := lib.GetSemver(tfConstraint, params.MirrorURL)
 	if err2 != nil {
 		logger.Errorf("No version found matching %q", tfConstraint)
diff --git a/lib/semver.go b/lib/semver.go
--- a/lib/semver.go
+++ b/lib/semver.go
@@ -46,6 +46,7 @@ func SemVerParser(tfconstraint *string, tflist []string) (string, error) {
 			}
 		}
 	}
+
 	return "", fmt.Errorf("did not find version matching constraint: %s", *tfconstraint)
 }
 


Test patch:
diff --git a/lib/param_parsing/versiontf_test.go b/lib/param_parsing/versiontf_test.go
--- a/lib/param_parsing/versiontf_test.go
+++ b/lib/param_parsing/versiontf_test.go
@@ -4,20 +4,37 @@ import (
 	"fmt"
 	"github.com/hashicorp/go-version"
 	"github.com/warrensbox/terraform-switcher/lib"
+	"strings"
 	"testing"
 )
 
-func TestGetVersionFromVersionsTF(t *testing.T) {
+func TestGetVersionFromVersionsTF_matches_version(t *testing.T) {
 	logger = lib.InitLogger("DEBUG")
 	var params Params
 	params = initParams(params)
 	params.ChDirPath = "../../test-data/integration-tests/test_versiontf"
 	params, _ = GetVersionFromVersionsTF(params)
-	v1, _ := version.NewVersion("1.0.0")
-	v2, _ := version.NewVersion("2.0.0")
+	v1, _ := version.NewVersion("1.0.5")
 	actualVersion, _ := version.NewVersion(params.Version)
-	if !actualVersion.GreaterThanOrEqual(v1) || !actualVersion.LessThan(v2) {
-		t.Error("Determined version is not between 1.0.0 and 2.0.0")
+	if !actualVersion.Equal(v1) {
+		t.Error("Determined version is not 1.0.5")
+	}
+}
+
+func TestGetVersionFromVersionsTF_non_matching_constraints(t *testing.T) {
+	logger = lib.InitLogger("DEBUG")
+	var params Params
+	params = initParams(params)
+	params.ChDirPath = "../../test-data/skip-integration-tests/test_versiontf_non_matching_constraints"
+	params, err := GetVersionFromVersionsTF(params)
+	if err == nil {
+		t.Error("Expected error but got nil")
+	} else {
+		expected := "exact constraint ("
+		expected2 := ") cannot be combined with other conditions"
+		if !strings.Contains(fmt.Sprint(err), expected) || !strings.Contains(fmt.Sprint(err), expected2) {
+			t.Errorf("Expected error string containing %q and %q. Got %q", expected, expected2, err)
+		}
 	}
 }
 
diff --git a/test-data/integration-tests/test_versiontf/version.tf b/test-data/integration-tests/test_versiontf/version.tf
--- a/test-data/integration-tests/test_versiontf/version.tf
+++ b/test-data/integration-tests/test_versiontf/version.tf
@@ -1,8 +1,7 @@
 terraform {
   required_version = "~> 1.0.0"
+}
 
-  required_providers {
-    aws        = ">= 2.52.0"
-    kubernetes = ">= 1.11.1"
-  }
-}
\ No newline at end of file
+terraform {
+  required_version = "<= 1.0.5"
+}
diff --git a/test-data/skip-integration-tests/test_versiontf_non_matching_constraints/version.tf b/test-data/skip-integration-tests/test_versiontf_non_matching_constraints/version.tf
new file mode 100644
--- /dev/null
+++ b/test-data/skip-integration-tests/test_versiontf_non_matching_constraints/version.tf
@@ -0,0 +1,11 @@
+terraform {
+  required_version = "~> 1.0.0"
+}
+
+terraform {
+  required_version = "= 1.0.5"
+}
+
+terraform {
+  required_version = "<= 1.0.4"
+}