Repository: runatlantis/atlantis (language: go)

Issue:
Specified terraform version (using required_version) not used when required_providers is specified
I have a project that I just upgraded to terraform 0.13, and that on it's own worked fine. I then attempted to use the [new syntax](https://www.terraform.io/docs/configuration/terraform.html#specifying-provider-requirements) for specifying a provider version, and that seems to cause atlantis to be unable to parse the `required_version` specified in the terraform. For example, with this configuration plans are successful:
```hcl
terraform {
  required_version = "0.13.0"

  backend "s3" {
    region         = "us-west-2"
    bucket         = "<redacted>"
    key            = "<redacted>"
    dynamodb_table = "<redacted>"
  }
}
```

But with this configuration:
```hcl
terraform {
  required_version = "0.13.0"

  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 3"
    }
  }

  backend "s3" {
    region         = "us-west-2"
    bucket         = "<redacted>"
    key            = "<redacted>"
    dynamodb_table = "<redacted>"
  }
}
```

The plan fails with 
```

Warning: Provider source not supported in Terraform v0.12

  on main.tf line 5, in terraform:
   5:     aws = {
   6:       source = "hashicorp/aws"
   7:       version = "~> 3"
   8:     }

A source was declared for provider aws. Terraform v0.12 does not support the
provider source attribute. It will be ignored.


Error: Unsupported Terraform Core version

This configuration does not support Terraform version 0.12.27. To proceed,
either choose another supported Terraform version or update the root module's
version constraint. Version constraints are normally set for good reason, so
updating the constraint may lead to other errors or unexpected behavior.
```

It seems specifying the provider info breaks `required_version = "0.13.0"`.


Discussion / hints:
Seems to be related to the "source" argument in the provider block
I'm seeing this error message in the logs:

```
2020/08/17 03:42:43+0000 [EROR] project_command_builder.go:450 [censored]#79: Trying to detect required version: Unsuitable value type: Unsuitable value: string required
```
PRs welcome! Can someone dig into this?
Ok, so I did a bit more digging and I think I got to the root of it. Here's what I found:

The error we're seeing in the logs seems to originate here: 
https://github.com/runatlantis/atlantis/blob/1d59cf6b2d405e994d422188871dcf2613e28da4/server/events/project_command_builder.go#L476

```
2020/08/20 02:48:37+0000 [EROR] project_command_builder.go:478 joerx/tf-sandbox#1: Trying to detect required version: Unsuitable value type: Unsuitable value: string required
```

Looks like `tfconfig` is complaining about something in our Terraform config. Adding some additional output to find out what exactly it's complaining about:

```
2020/08/20 02:48:37+0000 [DBUG] project_command_builder.go:480 joerx/tf-sandbox#1: Unsuitable value: string required in /home/atlantis/.atlantis/repos/joerx/tf-sandbox/1/default/custom-provider-poc/backend.tf:5
```

Which corresponds to the required provider block in my test config (this PR: https://github.com/joerx/tf-sandbox/pull/1):

```
terraform {
  required_version = "0.13.0"

  required_providers {
    cheesecake = {
      source  = "joerx/cheesecake"
      version = "0.2.3"
    }
  }
}
```

Apparently the `tfconfig` library expects a string where now is a provider config object. Seems the corresponding dependency is a few revisions behind. I'll try to bump it and see if that helps.

Is there a workaround, since https://github.com/runatlantis/atlantis/pull/1171 hasn't been released yet?
Hmm... I tried using the top-of-trunk atlantis, but it didn't work for me either.  It seems to be ignoring the terraform required_version specification, and is not overriding the default terraform version 0.12.29 set via environment variables.  By adding an atlantis.yaml via a repo config, I was able to set the right version 0.13.4, and things worked.  But the docs seem to say that the specified required_version in the terraform config should be sufficient.
https://www.runatlantis.io/docs/terraform-versions.html#via-terraform-config

How are you supposed to do a transition from one major version of terraform to another?  We have our atlantis instance defaulting to 0.12.29 so that all the old terraform 12 stuff will still work, by default. But when trying to move to 0.13 in a specific repo, it seems to not be obeying the directives in the HCL files.
Does the terraform required_version specification _only_ work if specifying an exact version, not a version range, even tho that is valid there?
Looked at the source code and answered my own question.  It does not, for reasons that are unclear to me.  Filed https://github.com/runatlantis/atlantis/issues/1217. 
For what it's worth, I wrote a small wrapper around Terraform that will automatically download the correct TF version based on a semver constraint. It's here: https://github.com/joerx/tfx - we're using it in a [custom workflow](https://www.runatlantis.io/docs/custom-workflows.html) for Atlantis and it works reasonably well so far.
> will automatically download the correct TF version based on a semver constraint.

You're scraping the HTML with a regex, right?


Yeah. haven’t found a better way to do it yet 

Sent from my iPhone

> On 12 Oct 2020, at 12:28 PM, Richard Lee <notifications@github.com> wrote:
> 
> ﻿
> will automatically download the correct TF version based on a semver constraint.
> 
> You're scraping the HTML with a regex, right?
> 
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub, or unsubscribe.

There is a similar project called [terraform-switcher](https://github.com/warrensbox/terraform-switcher) that uses the official [Terraform releases list](https://releases.hashicorp.com/terraform/) and selects the correct release based on the `required_version` constraint
> Looked at the source code and answered my own question. It does not, for reasons that are unclear to me. Filed #1217.

@llamahunter if you have a moment can you please make note of where in the source you found the constraint to make it easier for someone to pick up? 
I wanted to report that this still doesnt work in the latest helm chart.

```
terraform {
  required_version = "0.13.5"
  backend "s3" {
    bucket         = "terraform-tfstate-us-east-1"
    encrypt        = "true"
    key            = "ops/restricted" # Change as needed
    dynamodb_table = "terraform_locks_us-east-1"
    region         = "us-east-1"
  }
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "3.19.0"
      region  = "us-east-1"
    }
  }
}
```

Results in 

```
Error: Unsupported Terraform Core version

  on providers.tf line 2, in terraform:
   2:   required_version = "0.13.5"

This configuration does not support Terraform version 0.13.3. To proceed,
either choose another supported Terraform version or update this version
constraint. Version constraints are normally set for good reason, so updating
the constraint may lead to other errors or unexpected behavior.
```

We are specifying defaultTFVersion: 0.13.3 chart, and then attempting to upgrade section by section to 0.13.5, then we would bump up the default version.
Same here @jseiser looks as blocker to proceed with custom workflows with terraform > `0.12.27` in our case is `0.13.5`.  
https://github.com/runatlantis/atlantis/issues/1217 is in place to fix this, but it hasn't been merged yet.

Fix patch:
diff --git a/go.mod b/go.mod
index e617ece712..9615d561b5 100644
--- a/go.mod
+++ b/go.mod
@@ -3,6 +3,7 @@ module github.com/runatlantis/atlantis
 go 1.19
 
 require (
+	github.com/Masterminds/semver v1.5.0
 	github.com/Masterminds/sprig/v3 v3.2.2
 	github.com/alicebob/miniredis/v2 v2.23.1
 	github.com/bradleyfalzon/ghinstallation/v2 v2.1.0
@@ -38,10 +39,10 @@ require (
 	github.com/stretchr/testify v1.8.1
 	github.com/urfave/cli v1.22.10
 	github.com/urfave/negroni v1.0.0
+	github.com/warrensbox/terraform-switcher v0.1.1-0.20221027055942-201c8e92e997
 	github.com/xanzy/go-gitlab v0.74.0
 	go.etcd.io/bbolt v1.3.6
 	go.uber.org/zap v1.23.0
-	golang.org/x/sync v0.1.0
 	golang.org/x/term v0.2.0
 	golang.org/x/text v0.4.0
 	gopkg.in/go-playground/validator.v9 v9.31.0
diff --git a/go.sum b/go.sum
index 3d74894c9f..7a19275d0e 100644
--- a/go.sum
+++ b/go.sum
@@ -51,6 +51,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
 github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
 github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
+github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
+github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
 github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
 github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
 github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8=
@@ -473,6 +475,8 @@ github.com/urfave/cli v1.22.10 h1:p8Fspmz3iTctJstry1PYS3HVdllxnEzTEsgIgtxTrCk=
 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
 github.com/urfave/negroni v1.0.0 h1:kIimOitoypq34K7TG7DUaJ9kq/N4Ofuwi1sjz0KipXc=
 github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4=
+github.com/warrensbox/terraform-switcher v0.1.1-0.20221027055942-201c8e92e997 h1:be5WC0FHdhimAhe2G3DPhduX117RM8qdTMYCMHDt4DM=
+github.com/warrensbox/terraform-switcher v0.1.1-0.20221027055942-201c8e92e997/go.mod h1:saryXNaL624mlulV138FP+HhVw7IpvETUXLS3nTvH1g=
 github.com/xanzy/go-gitlab v0.74.0 h1:Ha1cokbjn0PXy6B19t3W324dwM4AOT52fuHr7nERPrc=
 github.com/xanzy/go-gitlab v0.74.0/go.mod h1:d/a0vswScO7Agg1CZNz15Ic6SSvBG9vfw8egL99t4kA=
 github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -616,8 +620,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
-golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
diff --git a/server/events/project_command_context_builder.go b/server/events/project_command_context_builder.go
index 2238138593..956d7f539f 100644
--- a/server/events/project_command_context_builder.go
+++ b/server/events/project_command_context_builder.go
@@ -2,17 +2,22 @@ package events
 
 import (
 	"path/filepath"
-	"regexp"
+	"sort"
 
+	"github.com/Masterminds/semver"
 	"github.com/google/uuid"
 	"github.com/hashicorp/go-version"
+
 	"github.com/hashicorp/terraform-config-inspect/tfconfig"
 	"github.com/runatlantis/atlantis/server/core/config/valid"
 	"github.com/runatlantis/atlantis/server/events/command"
 	"github.com/runatlantis/atlantis/server/events/models"
 	"github.com/uber-go/tally"
+	lib "github.com/warrensbox/terraform-switcher/lib"
 )
 
+var mirrorURL = "https://releases.hashicorp.com/terraform"
+
 func NewProjectCommandContextBuilder(policyCheckEnabled bool, commentBuilder CommentBuilder, scope tally.Scope) ProjectCommandContextBuilder {
 	projectCommandContextBuilder := &DefaultProjectCommandContextBuilder{
 		CommentBuilder: commentBuilder,
@@ -287,7 +292,6 @@ func getTfVersion(ctx *command.Context, absProjDir string) *version.Version {
 	module, diags := tfconfig.LoadModule(absProjDir)
 	if diags.HasErrors() {
 		ctx.Log.Err("trying to detect required version: %s", diags.Error())
-		return nil
 	}
 
 	if len(module.RequiredCore) != 1 {
@@ -295,21 +299,36 @@ func getTfVersion(ctx *command.Context, absProjDir string) *version.Version {
 		return nil
 	}
 	requiredVersionSetting := module.RequiredCore[0]
+	ctx.Log.Debug("found required_version setting of %q", requiredVersionSetting)
 
-	// We allow `= x.y.z`, `=x.y.z` or `x.y.z` where `x`, `y` and `z` are integers.
-	re := regexp.MustCompile(`^=?\s*([^\s]+)\s*$`)
-	matched := re.FindStringSubmatch(requiredVersionSetting)
-	if len(matched) == 0 {
-		ctx.Log.Debug("did not specify exact version in terraform configuration, found %q", requiredVersionSetting)
-		return nil
+	tflist, _ := lib.GetTFList(mirrorURL, true)
+	constrains, _ := semver.NewConstraint(requiredVersionSetting)
+	versions := make([]*semver.Version, len(tflist))
+
+	for i, tfvals := range tflist {
+		version, err := semver.NewVersion(tfvals) //NewVersion parses a given version and returns an instance of Version or an error if unable to parse the version.
+		if err == nil {
+			versions[i] = version
+		}
 	}
-	ctx.Log.Debug("found required_version setting of %q", requiredVersionSetting)
-	version, err := version.NewVersion(matched[1])
-	if err != nil {
-		ctx.Log.Debug(err.Error())
+
+	if len(versions) == 0 {
+		ctx.Log.Debug("did not specify exact valid version in terraform configuration, found %q", requiredVersionSetting)
 		return nil
 	}
 
-	ctx.Log.Info("detected module requires version: %q", version.String())
-	return version
+	sort.Sort(sort.Reverse(semver.Collection(versions)))
+
+	for _, element := range versions {
+		if constrains.Check(element) { // Validate a version against a constraint
+			tfversionStr := element.String()
+			if lib.ValidVersionFormat(tfversionStr) { //check if version format is correct
+				tfversion, _ := version.NewVersion(tfversionStr)
+				ctx.Log.Info("detected module requires version: %s", tfversionStr)
+				return tfversion
+			}
+		}
+	}
+	ctx.Log.Debug("could not match any valid terraform version with %q", requiredVersionSetting)
+	return nil
 }


Test patch:
diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go
index b3322f435b..d3f96b1e7c 100644
--- a/server/events/command_runner_test.go
+++ b/server/events/command_runner_test.go
@@ -32,7 +32,6 @@ import (
 	lockingmocks "github.com/runatlantis/atlantis/server/core/locking/mocks"
 	"github.com/runatlantis/atlantis/server/events"
 	"github.com/runatlantis/atlantis/server/events/mocks"
-	eventmocks "github.com/runatlantis/atlantis/server/events/mocks"
 	"github.com/runatlantis/atlantis/server/events/mocks/matchers"
 	"github.com/runatlantis/atlantis/server/events/models"
 	"github.com/runatlantis/atlantis/server/events/models/fixtures"
@@ -87,7 +86,7 @@ func setup(t *testing.T) *vcsmocks.MockClient {
 	Ok(t, err)
 
 	drainer = &events.Drainer{}
-	deleteLockCommand = eventmocks.NewMockDeleteLockCommand()
+	deleteLockCommand = mocks.NewMockDeleteLockCommand()
 	applyLockChecker = lockingmocks.NewMockApplyLockChecker()
 	lockingLocker = lockingmocks.NewMockLocker()
 
diff --git a/server/events/project_command_builder_test.go b/server/events/project_command_builder_test.go
index fd0709c256..12bb03aad5 100644
--- a/server/events/project_command_builder_test.go
+++ b/server/events/project_command_builder_test.go
@@ -861,6 +861,7 @@ func TestDefaultProjectCommandBuilder_TerraformVersion(t *testing.T) {
 	// If terraform configuration is used, result should be `0.12.8`.
 	// If project configuration is used, result should be `0.12.6`.
 	// If default is to be used, result should be `nil`.
+
 	baseVersionConfig := `
 terraform {
   required_version = "%s0.12.8"
@@ -875,7 +876,29 @@ projects:
 `
 
 	exactSymbols := []string{"", "="}
-	nonExactSymbols := []string{">", ">=", "<", "<=", "~="}
+	// Depending on when the tests are run, the > and >= matching versions will have to be increased.
+	// It's probably not worth testing the terraform-switcher version here so we only test <, <=, and ~>.
+	// One way to test this in the future is to mock tfswitcher.GetTFList() to return the highest
+	// version of 1.3.5.
+	// nonExactSymbols := []string{">", ">=", "<", "<=", "~>"}
+	nonExactSymbols := []string{"<", "<=", "~>"}
+	nonExactVersions := map[string]map[string][]int{
+		// ">": {
+		// 	"project1": {1, 3, 5},
+		// },
+		// ">=": {
+		// 	"project1": {1, 3, 5},
+		// },
+		"<": {
+			"project1": {0, 12, 7},
+		},
+		"<=": {
+			"project1": {0, 12, 8},
+		},
+		"~>": {
+			"project1": {0, 12, 31},
+		},
+	}
 
 	type testCase struct {
 		DirStructure  map[string]interface{}
@@ -908,9 +931,7 @@ projects:
 				},
 			},
 			ModifiedFiles: []string{"project1/main.tf"},
-			Exp: map[string][]int{
-				"project1": nil,
-			},
+			Exp:           nonExactVersions[nonExactSymbol],
 		}
 	}
 
diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go
index 7156018add..96f4b17c6e 100644
--- a/server/events/project_command_runner_test.go
+++ b/server/events/project_command_runner_test.go
@@ -27,7 +27,6 @@ import (
 	"github.com/runatlantis/atlantis/server/events"
 	"github.com/runatlantis/atlantis/server/events/command"
 	"github.com/runatlantis/atlantis/server/events/mocks"
-	eventmocks "github.com/runatlantis/atlantis/server/events/mocks"
 	"github.com/runatlantis/atlantis/server/events/mocks/matchers"
 	"github.com/runatlantis/atlantis/server/events/models"
 	jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
@@ -189,8 +188,8 @@ func TestProjectOutputWrapper(t *testing.T) {
 			var prjResult command.ProjectResult
 			var expCommitStatus models.CommitStatus
 
-			mockJobURLSetter := eventmocks.NewMockJobURLSetter()
-			mockJobMessageSender := eventmocks.NewMockJobMessageSender()
+			mockJobURLSetter := mocks.NewMockJobURLSetter()
+			mockJobMessageSender := mocks.NewMockJobMessageSender()
 			mockProjectCommandRunner := mocks.NewMockProjectCommandRunner()
 
 			runner := &events.ProjectOutputWrapper{