feat: Support project-level Terraform distribution selection (#5167)

Signed-off-by: Andrew Borg <andrew.b.borg@gmail.com>
Co-authored-by: PePe Amengual <2208324+jamengual@users.noreply.github.com>
Co-authored-by: Simon Heather <32168619+X-Guardian@users.noreply.github.com>
This commit is contained in:
Andrew Borg
2025-01-03 00:06:01 -05:00
committed by GitHub
parent a9c3730c5f
commit 9e0f3d1bb5
54 changed files with 1326 additions and 412 deletions

View File

@@ -72,6 +72,7 @@ const (
CheckoutStrategyFlag = "checkout-strategy"
ConfigFlag = "config"
DataDirFlag = "data-dir"
DefaultTFDistributionFlag = "default-tf-distribution"
DefaultTFVersionFlag = "default-tf-version"
DisableApplyAllFlag = "disable-apply-all"
DisableAutoplanFlag = "disable-autoplan"
@@ -141,7 +142,7 @@ const (
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
RestrictFileList = "restrict-file-list"
TFDistributionFlag = "tf-distribution"
TFDistributionFlag = "tf-distribution" // deprecated for DefaultTFDistributionFlag
TFDownloadFlag = "tf-download"
TFDownloadURLFlag = "tf-download-url"
UseTFPluginCache = "use-tf-plugin-cache"
@@ -421,8 +422,8 @@ var stringFlags = map[string]stringFlag{
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
},
TFDistributionFlag: {
description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu),
defaultValue: DefaultTFDistribution,
description: "[Deprecated for --default-tf-distribution].",
hidden: true,
},
TFDownloadURLFlag: {
description: "Base URL to download Terraform versions from.",
@@ -437,6 +438,10 @@ var stringFlags = map[string]stringFlag{
" Only set if using TFC/E as a remote backend." +
" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.",
},
DefaultTFDistributionFlag: {
description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu),
defaultValue: DefaultTFDistribution,
},
DefaultTFVersionFlag: {
description: "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk." +
" If not set, Atlantis uses the terraform binary in its PATH.",
@@ -840,12 +845,13 @@ func (s *ServerCmd) run() error {
// Config looks good. Start the server.
server, err := s.ServerCreator.NewServer(userConfig, server.Config{
AllowForkPRsFlag: AllowForkPRsFlag,
AtlantisURLFlag: AtlantisURLFlag,
AtlantisVersion: s.AtlantisVersion,
DefaultTFVersionFlag: DefaultTFVersionFlag,
RepoConfigJSONFlag: RepoConfigJSONFlag,
SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag,
AllowForkPRsFlag: AllowForkPRsFlag,
AtlantisURLFlag: AtlantisURLFlag,
AtlantisVersion: s.AtlantisVersion,
DefaultTFDistributionFlag: DefaultTFDistributionFlag,
DefaultTFVersionFlag: DefaultTFVersionFlag,
RepoConfigJSONFlag: RepoConfigJSONFlag,
SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag,
})
if err != nil {
@@ -921,8 +927,11 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) {
if c.RedisPort == 0 {
c.RedisPort = DefaultRedisPort
}
if c.TFDistribution == "" {
c.TFDistribution = DefaultTFDistribution
if c.TFDistribution != "" && c.DefaultTFDistribution == "" {
c.DefaultTFDistribution = c.TFDistribution
}
if c.DefaultTFDistribution == "" {
c.DefaultTFDistribution = DefaultTFDistribution
}
if c.TFDownloadURL == "" {
c.TFDownloadURL = DefaultTFDownloadURL
@@ -953,7 +962,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels)
}
if userConfig.TFDistribution != TFDistributionTerraform && userConfig.TFDistribution != TFDistributionOpenTofu {
if userConfig.DefaultTFDistribution != TFDistributionTerraform && userConfig.DefaultTFDistribution != TFDistributionOpenTofu {
return fmt.Errorf("invalid tf distribution: expected one of %s or %s",
TFDistributionTerraform, TFDistributionOpenTofu)
}
@@ -1172,6 +1181,10 @@ func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error {
// }
//
if userConfig.TFDistribution != "" {
deprecatedFlags = append(deprecatedFlags, TFDistributionFlag)
}
if len(deprecatedFlags) > 0 {
warning := "WARNING: "
if len(deprecatedFlags) == 1 {

View File

@@ -73,6 +73,7 @@ var testFlags = map[string]interface{}{
CheckoutStrategyFlag: CheckoutStrategyMerge,
CheckoutDepthFlag: 0,
DataDirFlag: "/path",
DefaultTFDistributionFlag: "terraform",
DefaultTFVersionFlag: "v0.11.0",
DisableApplyAllFlag: true,
DisableMarkdownFoldingFlag: true,
@@ -977,6 +978,46 @@ func TestExecute_AutoplanFileList(t *testing.T) {
}
}
func TestExecute_ValidateDefaultTFDistribution(t *testing.T) {
cases := []struct {
description string
flags map[string]interface{}
expectErr string
}{
{
"terraform",
map[string]interface{}{
DefaultTFDistributionFlag: "terraform",
},
"",
},
{
"opentofu",
map[string]interface{}{
DefaultTFDistributionFlag: "opentofu",
},
"",
},
{
"errs on invalid distribution",
map[string]interface{}{
DefaultTFDistributionFlag: "invalid_distribution",
},
"invalid tf distribution: expected one of terraform or opentofu",
},
}
for _, testCase := range cases {
t.Log("Should validate default tf distribution when " + testCase.description)
c := setupWithDefaults(testCase.flags, t)
err := c.Execute()
if testCase.expectErr != "" {
ErrEquals(t, testCase.expectErr, err)
} else {
Ok(t, err)
}
}
}
func setup(flags map[string]interface{}, t *testing.T) *cobra.Command {
vipr := viper.New()
for k, v := range flags {

View File

@@ -66,6 +66,7 @@ projects:
branch: /main/
dir: .
workspace: default
terraform_distribution: terraform
terraform_version: v0.11.0
delete_source_branch_on_merge: true
repo_locking: true # deprecated: use repo_locks instead
@@ -262,6 +263,20 @@ See [Custom Workflow Use Cases: Terragrunt](custom-workflows.md#terragrunt)
See [Custom Workflow Use Cases: Running custom commands](custom-workflows.md#running-custom-commands)
### Terraform Distributions
If you'd like to use a different distribution of Terraform than what is set
by the `--default-tf-version` flag, then set the `terraform_distribution` key:
```yaml
version: 3
projects:
- dir: project1
terraform_distribution: opentofu
```
Atlantis will automatically download and use this distribution. Valid values are `terraform` and `opentofu`.
### Terraform Versions
If you'd like to use a different version of Terraform than what is in Atlantis'

View File

@@ -386,6 +386,16 @@ and set `--autoplan-modules` to `false`.
Note that the atlantis user is restricted to `~/.atlantis`.
If you set the `--data-dir` flag to a path outside of Atlantis its home directory, ensure that you grant the atlantis user the correct permissions.
### `--default-tf-distribution`
```bash
atlantis server --default-tf-distribution="terraform"
# or
ATLANTIS_DEFAULT_TF_DISTRIBUTION="terraform"
```
Which TF distribution to use. Can be set to `terraform` or `opentofu`.
### `--default-tf-version`
```bash
@@ -1259,13 +1269,8 @@ This is useful when you have many projects and want to keep the pull request cle
### `--tf-distribution`
```bash
atlantis server --tf-distribution="terraform"
# or
ATLANTIS_TF_DISTRIBUTION="terraform"
```
Which TF distribution to use. Can be set to `terraform` or `opentofu`.
<Badge text="Deprecated" type="warn"/>
Deprecated for `--default-tf-distribution`.
### `--tf-download`

View File

@@ -29,6 +29,7 @@ import (
mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks"
"github.com/runatlantis/atlantis/server/core/terraform"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks"
@@ -1319,7 +1320,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
terraformClient, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
terraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
Ok(t, err)
boltdb, err := db.New(dataDir)
Ok(t, err)
@@ -1346,6 +1347,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
}
}
defaultTFDistribution := terraformClient.DefaultDistribution()
defaultTFVersion := terraformClient.DefaultVersion()
locker := events.NewDefaultWorkingDirLocker()
parser := &config.ParserValidator{}
@@ -1429,7 +1431,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
terraformClient,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion)
Ok(t, err)
@@ -1440,6 +1442,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
conftextExec.VersionCache = &LocalConftestCache{}
policyCheckRunner, err := runtime.NewPolicyCheckStepRunner(
defaultTFDistribution,
defaultTFVersion,
conftextExec,
)
@@ -1451,11 +1454,13 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
Locker: projectLocker,
LockURLGenerator: &mockLockURLGenerator{},
InitStepRunner: &runtime.InitStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTFVersion,
TerraformExecutor: terraformClient,
DefaultTFDistribution: defaultTFDistribution,
DefaultTFVersion: defaultTFVersion,
},
PlanStepRunner: runtime.NewPlanStepRunner(
terraformClient,
defaultTFDistribution,
defaultTFVersion,
statusUpdater,
asyncTfExec,
@@ -1465,10 +1470,11 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
ApplyStepRunner: &runtime.ApplyStepRunner{
TerraformExecutor: terraformClient,
},
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFVersion),
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
RunStepRunner: &runtime.RunStepRunner{
TerraformExecutor: terraformClient,
DefaultTFDistribution: defaultTFDistribution,
DefaultTFVersion: defaultTFVersion,
ProjectCmdOutputHandler: projectCmdOutputHandler,
},

View File

@@ -610,6 +610,31 @@ workflows:
},
},
},
{
description: "project field with terraform_distribution set to opentofu",
input: `
version: 3
projects:
- dir: .
workspace: myworkspace
terraform_distribution: opentofu
`,
exp: valid.RepoCfg{
Version: 3,
Projects: []valid.Project{
{
Dir: ".",
Workspace: "myworkspace",
TerraformDistribution: String("opentofu"),
Autoplan: valid.Autoplan{
WhenModified: raw.DefaultAutoPlanWhenModified,
Enabled: true,
},
},
},
Workflows: make(map[string]valid.Workflow),
},
},
{
description: "project dir with ..",
input: `

View File

@@ -26,6 +26,7 @@ type Project struct {
Dir *string `yaml:"dir,omitempty"`
Workspace *string `yaml:"workspace,omitempty"`
Workflow *string `yaml:"workflow,omitempty"`
TerraformDistribution *string `yaml:"terraform_distribution,omitempty"`
TerraformVersion *string `yaml:"terraform_version,omitempty"`
Autoplan *Autoplan `yaml:"autoplan,omitempty"`
PlanRequirements []string `yaml:"plan_requirements,omitempty"`
@@ -86,6 +87,7 @@ func (p Project) Validate() error {
validation.Field(&p.PlanRequirements, validation.By(validPlanReq)),
validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)),
validation.Field(&p.ImportRequirements, validation.By(validImportReq)),
validation.Field(&p.TerraformDistribution, validation.By(validDistribution)),
validation.Field(&p.TerraformVersion, validation.By(VersionValidator)),
validation.Field(&p.DependsOn, validation.By(DependsOn)),
validation.Field(&p.Name, validation.By(validName)),
@@ -118,6 +120,9 @@ func (p Project) ToValid() valid.Project {
if p.TerraformVersion != nil {
v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion)
}
if p.TerraformDistribution != nil {
v.TerraformDistribution = p.TerraformDistribution
}
if p.Autoplan == nil {
v.Autoplan = DefaultAutoPlan()
} else {
@@ -202,3 +207,11 @@ func validImportReq(value interface{}) error {
}
return nil
}
func validDistribution(value interface{}) error {
distribution := value.(*string)
if distribution != nil && *distribution != "terraform" && *distribution != "opentofu" {
return fmt.Errorf("'%s' is not a valid terraform_distribution, only '%s' and '%s' are supported", *distribution, "terraform", "opentofu")
}
return nil
}

View File

@@ -105,6 +105,7 @@ type MergedProjectCfg struct {
AutoplanEnabled bool
AutoMergeDisabled bool
AutoMergeMethod string
TerraformDistribution *string
TerraformVersion *version.Version
RepoCfgVersion int
PolicySets PolicySets
@@ -412,6 +413,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
DependsOn: proj.DependsOn,
Name: proj.GetName(),
AutoplanEnabled: proj.Autoplan.Enabled,
TerraformDistribution: proj.TerraformDistribution,
TerraformVersion: proj.TerraformVersion,
RepoCfgVersion: rCfg.Version,
PolicySets: g.PolicySets,
@@ -438,6 +440,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo
Workspace: workspace,
Name: "",
AutoplanEnabled: DefaultAutoPlanEnabled,
TerraformDistribution: nil,
TerraformVersion: nil,
PolicySets: g.PolicySets,
DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,

View File

@@ -147,6 +147,7 @@ type Project struct {
Workspace string
Name *string
WorkflowName *string
TerraformDistribution *string
TerraformVersion *version.Version
Autoplan Autoplan
PlanRequirements []string

View File

@@ -10,6 +10,7 @@ import (
"github.com/pkg/errors"
version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/utils"
@@ -17,10 +18,11 @@ import (
// ApplyStepRunner runs `terraform apply`.
type ApplyStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
CommitStatusUpdater StatusUpdater
AsyncTFExec AsyncTFExec
TerraformExecutor TerraformExec
DefaultTFDistribution terraform.Distribution
DefaultTFVersion *version.Version
CommitStatusUpdater StatusUpdater
AsyncTFExec AsyncTFExec
}
func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
@@ -39,11 +41,19 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa
ctx.Log.Info("starting apply")
var out string
tfDistribution := a.DefaultTFDistribution
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
tfVersion := a.DefaultTFVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
// TODO: Leverage PlanTypeStepRunnerDelegate here
if IsRemotePlan(contents) {
args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...)
out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs)
out, err = a.runRemoteApply(ctx, args, path, planPath, tfDistribution, tfVersion, envs)
if err == nil {
out = a.cleanRemoteApplyOutput(out)
}
@@ -51,7 +61,7 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa
// NOTE: we need to quote the plan path because Bitbucket Server can
// have spaces in its repo owner names which is part of the path.
args := append(append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath))
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, ctx.TerraformVersion, ctx.Workspace)
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, tfDistribution, tfVersion, ctx.Workspace)
}
// If the apply was successful, delete the plan.
@@ -115,6 +125,7 @@ func (a *ApplyStepRunner) runRemoteApply(
applyArgs []string,
path string,
absPlanPath string,
tfDistribution terraform.Distribution,
tfVersion *version.Version,
envs map[string]string) (string, error) {
// The planfile contents are needed to ensure that the plan didn't change
@@ -133,7 +144,7 @@ func (a *ApplyStepRunner) runRemoteApply(
// Start the async command execution.
ctx.Log.Debug("starting async tf remote operation")
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace)
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfDistribution, tfVersion, ctx.Workspace)
var lines []string
nextLineIsRunURL := false
var runURL string

View File

@@ -14,7 +14,9 @@ import (
"github.com/runatlantis/atlantis/server/core/runtime"
runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks"
runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -59,17 +61,20 @@ func TestRun_Success(t *testing.T) {
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
o := runtime.ApplyStepRunner{
TerraformExecutor: terraform,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "workspace")
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
@@ -91,22 +96,24 @@ func TestRun_AppliesCorrectProjectPlan(t *testing.T) {
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
o := runtime.ApplyStepRunner{
TerraformExecutor: terraform,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), nil, "default")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, nil, "default")
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
func TestRun_UsesConfiguredTFVersion(t *testing.T) {
func TestApplyStepRunner_TestRun_UsesConfiguredTFVersion(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, nil, 0600)
@@ -123,17 +130,55 @@ func TestRun_UsesConfiguredTFVersion(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
o := runtime.ApplyStepRunner{
TerraformExecutor: terraform,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, []string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}, map[string]string(nil), tfDistribution, tfVersion, "workspace")
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
func TestApplyStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
logger := logging.NewNoopLogger(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.11.0")
projTFDistribution := "opentofu"
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
EscapedCommentArgs: []string{"comment", "args"},
TerraformDistribution: &projTFDistribution,
Log: logger,
}
RegisterMockTestingT(t)
terraform := tfclientmocks.NewMockClient()
o := runtime.ApplyStepRunner{
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), NotEq[tf.Distribution](tfDistribution), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := o.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq(tmpDir), Eq([]string{"apply", "-input=false", "extra", "args", "comment", "args", fmt.Sprintf("%q", planPath)}), Eq(map[string]string(nil)), NotEq[tf.Distribution](tfDistribution), Eq(tfVersion), Eq("workspace"))
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
@@ -197,7 +242,7 @@ func TestRun_UsingTarget(t *testing.T) {
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
step := runtime.ApplyStepRunner{
TerraformExecutor: terraform,
}
@@ -361,7 +406,7 @@ type remoteApplyMock struct {
}
// RunCommandAsync fakes out running terraform async.
func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {
func (r *remoteApplyMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {
r.CalledArgs = args
in := make(chan string)

View File

@@ -5,7 +5,9 @@ import (
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
@@ -38,12 +40,15 @@ func TestEnvStepRunner_Run(t *testing.T) {
},
}
RegisterMockTestingT(t)
tfClient := mocks.NewMockClient()
tfClient := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, err := version.NewVersion("0.12.0")
Ok(t, err)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
runStepRunner := runtime.RunStepRunner{
TerraformExecutor: tfClient,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
ProjectCmdOutputHandler: projectCmdOutputHandler,
}

View File

@@ -5,25 +5,32 @@ import (
"path/filepath"
version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/utils"
)
type importStepRunner struct {
terraformExecutor TerraformExec
defaultTFVersion *version.Version
terraformExecutor TerraformExec
defaultTFDistribution terraform.Distribution
defaultTFVersion *version.Version
}
func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner {
func NewImportStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner {
runner := &importStepRunner{
terraformExecutor: terraformExecutor,
defaultTFVersion: defaultTfVersion,
terraformExecutor: terraformExecutor,
defaultTFDistribution: defaultTfDistribution,
defaultTFVersion: defaultTfVersion,
}
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner)
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)
}
func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfDistribution := p.defaultTFDistribution
tfVersion := p.defaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
@@ -31,7 +38,7 @@ func (p *importStepRunner) Run(ctx command.ProjectContext, extraArgs []string, p
importCmd := []string{"import"}
importCmd = append(importCmd, extraArgs...)
importCmd = append(importCmd, ctx.EscapedCommentArgs...)
out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfVersion, ctx.Workspace)
out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), importCmd, envs, tfDistribution, tfVersion, ctx.Workspace)
// If the import was successful and a plan file exists, delete the plan.
planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))

View File

@@ -8,7 +8,9 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
@@ -29,17 +31,19 @@ func TestImportStepRunner_Run_Success(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.15.0")
s := NewImportStepRunner(terraform, tfVersion)
s := NewImportStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
commands := []string{"import", "-var", "foo=bar", "addr", "id"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
@@ -59,23 +63,66 @@ func TestImportStepRunner_Run_Workspace(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewImportStepRunner(terraform, tfVersion)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
s := NewImportStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
// switch workspace
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace)
// exec import
commands := []string{"import", "-var", "foo=bar", "addr", "id"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
func TestImportStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {
logger := logging.NewNoopLogger(t)
workspace := "something"
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace))
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
projTFDistribution := "opentofu"
context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"-var", "foo=bar", "addr", "id"},
Workspace: workspace,
TerraformDistribution: &projTFDistribution,
}
RegisterMockTestingT(t)
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
s := NewImportStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
// switch workspace
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
// exec import
commands := []string{"import", "-var", "foo=bar", "addr", "id"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")

View File

@@ -5,14 +5,16 @@ import (
version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/runtime/common"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/utils"
)
// InitStep runs `terraform init`.
type InitStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
TerraformExecutor TerraformExec
DefaultTFDistribution terraform.Distribution
DefaultTFVersion *version.Version
}
func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
@@ -33,6 +35,11 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat
}
}
tfDistribution := i.DefaultTFDistribution
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
tfVersion := i.DefaultTFVersion
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
@@ -56,7 +63,7 @@ func (i *InitStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pat
terraformInitCmd := append(terraformInitVerb, finalArgs...)
out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfVersion, ctx.Workspace)
out, err := i.TerraformExecutor.RunCommandWithVersion(ctx, path, terraformInitCmd, envs, tfDistribution, tfVersion, ctx.Workspace)
// Only include the init output if there was an error. Otherwise it's
// unnecessary and lengthens the comment.
if err != nil {

View File

@@ -12,7 +12,9 @@ import (
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/runtime"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
@@ -20,6 +22,8 @@ import (
func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
RegisterMockTestingT(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
cases := []struct {
version string
expCmd string
@@ -44,7 +48,7 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
for _, c := range cases {
t.Run(c.version, func(t *testing.T) {
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
@@ -55,10 +59,11 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
tfVersion, _ := version.NewVersion(c.version)
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
@@ -71,7 +76,74 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
if c.expCmd == "get" {
expArgs = []string{c.expCmd, "-upgrade", "extra", "args"}
}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
})
}
}
func TestInitStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {
RegisterMockTestingT(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
cases := []struct {
version string
distribution string
expCmd string
}{
{
"0.8.9",
"opentofu",
"get",
},
{
"0.8.9",
"terraform",
"get",
},
{
"0.9.0",
"opentofu",
"init",
},
{
"0.9.1",
"terraform",
"init",
},
}
for _, c := range cases {
t.Run(c.version, func(t *testing.T) {
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
Log: logger,
TerraformDistribution: &c.distribution,
}
tfVersion, _ := version.NewVersion(c.version)
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
// When there is no error, should not return init output to PR.
Equals(t, "", output)
// If using init then we specify -input=false but not for get.
expArgs := []string{c.expCmd, "-input=false", "-upgrade", "extra", "args"}
if c.expCmd == "get" {
expArgs = []string{c.expCmd, "-upgrade", "extra", "args"}
}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("workspace"))
})
}
}
@@ -79,15 +151,17 @@ func TestRun_UsesGetOrInitForRightVersion(t *testing.T) {
func TestRun_ShowInitOutputOnError(t *testing.T) {
// If there was an error during init then we want the output to be returned.
RegisterMockTestingT(t)
tfClient := mocks.NewMockClient()
tfClient := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(tfClient.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", errors.New("error"))
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.11.0")
iso := runtime.InitStepRunner{
TerraformExecutor: tfClient,
DefaultTFVersion: tfVersion,
TerraformExecutor: tfClient,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
output, err := iso.Run(command.ProjectContext{
@@ -118,14 +192,16 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.14.0")
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, []string{"extra", "args"}, repoDir, map[string]string(nil))
@@ -134,27 +210,29 @@ func TestRun_InitOmitsUpgradeFlagIfLockFileTracked(t *testing.T) {
Equals(t, "", output)
expectedArgs := []string{"init", "-input=false", "extra", "args"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
}
func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) {
tmpDir := t.TempDir()
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
Log: logger,
}
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.14.0")
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
@@ -163,7 +241,7 @@ func TestRun_InitKeepsUpgradeFlagIfLockFileNotPresent(t *testing.T) {
Equals(t, "", output)
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
}
func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing.T) {
@@ -173,7 +251,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
@@ -181,13 +259,15 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing
RepoRelDir: ".",
Log: logger,
}
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.13.0")
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
@@ -196,7 +276,7 @@ func TestRun_InitKeepUpgradeFlagIfLockFilePresentAndTFLessThanPoint14(t *testing
Equals(t, "", output)
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
}
func TestRun_InitExtraArgsDeDupe(t *testing.T) {
@@ -240,7 +320,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) {
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
@@ -248,13 +328,15 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) {
RepoRelDir: ".",
Log: logger,
}
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.10.0")
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := iso.Run(ctx, c.extraArgs, "/path", map[string]string(nil))
@@ -262,7 +344,7 @@ func TestRun_InitExtraArgsDeDupe(t *testing.T) {
// When there is no error, should not return init output to PR.
Equals(t, "", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", c.expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
})
}
}
@@ -276,17 +358,19 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) {
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
logger := logging.NewNoopLogger(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.14.0")
iso := runtime.InitStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
ctx := command.ProjectContext{
@@ -300,7 +384,7 @@ func TestRun_InitDeletesLockFileIfPresentAndNotTracked(t *testing.T) {
Equals(t, "", output)
expectedArgs := []string{"init", "-input=false", "-upgrade", "extra", "args"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, repoDir, expectedArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
}
func runCmd(t *testing.T, dir string, name string, args ...string) string {

View File

@@ -7,6 +7,7 @@ import (
go_version "github.com/hashicorp/go-version"
pegomock "github.com/petergtz/pegomock/v4"
models "github.com/runatlantis/atlantis/server/core/runtime/models"
terraform "github.com/runatlantis/atlantis/server/core/terraform"
command "github.com/runatlantis/atlantis/server/events/command"
"reflect"
"time"
@@ -27,11 +28,11 @@ func NewMockAsyncTFExec(options ...pegomock.Option) *MockAsyncTFExec {
func (mock *MockAsyncTFExec) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh }
func (mock *MockAsyncTFExec) FailHandler() pegomock.FailHandler { return mock.fail }
func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) {
func (mock *MockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (chan<- string, <-chan models.Line) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockAsyncTFExec().")
}
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}
_result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandAsync", _params, []reflect.Type{reflect.TypeOf((*chan<- string)(nil)).Elem(), reflect.TypeOf((*<-chan models.Line)(nil)).Elem()})
var _ret0 chan<- string
var _ret1 <-chan models.Line
@@ -91,8 +92,8 @@ type VerifierMockAsyncTFExec struct {
timeout time.Duration
}
func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification {
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
func (verifier *VerifierMockAsyncTFExec) RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockAsyncTFExec_RunCommandAsync_OngoingVerification {
_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandAsync", _params, verifier.timeout)
return &MockAsyncTFExec_RunCommandAsync_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -102,12 +103,12 @@ type MockAsyncTFExec_RunCommandAsync_OngoingVerification struct {
methodInvocations []pegomock.MethodInvocation
}
func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) {
ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1]
func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) {
ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1]
}
func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) {
func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) {
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
@@ -135,15 +136,21 @@ func (c *MockAsyncTFExec_RunCommandAsync_OngoingVerification) GetAllCapturedArgu
}
}
if len(_params) > 4 {
_param4 = make([]*go_version.Version, len(c.methodInvocations))
_param4 = make([]terraform.Distribution, len(c.methodInvocations))
for u, param := range _params[4] {
_param4[u] = param.(*go_version.Version)
_param4[u] = param.(terraform.Distribution)
}
}
if len(_params) > 5 {
_param5 = make([]string, len(c.methodInvocations))
_param5 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[5] {
_param5[u] = param.(string)
_param5[u] = param.(*go_version.Version)
}
}
if len(_params) > 6 {
_param6 = make([]string, len(c.methodInvocations))
for u, param := range _params[6] {
_param6[u] = param.(string)
}
}
}

View File

@@ -10,8 +10,8 @@ import (
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/terraform/ansi"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/terraform/ansi"
"github.com/runatlantis/atlantis/server/jobs"
)

View File

@@ -7,7 +7,9 @@ import (
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/core/terraform"
terraformmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
@@ -45,12 +47,15 @@ func TestMultiEnvStepRunner_Run(t *testing.T) {
},
}
RegisterMockTestingT(t)
tfClient := mocks.NewMockClient()
tfClient := tfclientmocks.NewMockClient()
mockDownloader := terraformmocks.NewMockDownloader()
tfDistribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, err := version.NewVersion("0.12.0")
Ok(t, err)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
runStepRunner := runtime.RunStepRunner{
TerraformExecutor: tfClient,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
ProjectCmdOutputHandler: projectCmdOutputHandler,
}

View File

@@ -9,6 +9,7 @@ import (
version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
)
@@ -26,34 +27,40 @@ var (
)
type planStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
CommitStatusUpdater StatusUpdater
AsyncTFExec AsyncTFExec
TerraformExecutor TerraformExec
DefaultTFDistribution terraform.Distribution
DefaultTFVersion *version.Version
CommitStatusUpdater StatusUpdater
AsyncTFExec AsyncTFExec
}
func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner {
func NewPlanStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, commitStatusUpdater StatusUpdater, asyncTFExec AsyncTFExec) Runner {
runner := &planStepRunner{
TerraformExecutor: terraformExecutor,
DefaultTFVersion: defaultTfVersion,
CommitStatusUpdater: commitStatusUpdater,
AsyncTFExec: asyncTFExec,
TerraformExecutor: terraformExecutor,
DefaultTFDistribution: defaultTfDistribution,
DefaultTFVersion: defaultTfVersion,
CommitStatusUpdater: commitStatusUpdater,
AsyncTFExec: asyncTFExec,
}
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner)
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)
}
func (p *planStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfDistribution := p.DefaultTFDistribution
tfVersion := p.DefaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
planFile := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))
planCmd := p.buildPlanCmd(ctx, extraArgs, path, tfVersion, planFile)
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfVersion, ctx.Workspace)
output, err := p.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), planCmd, envs, tfDistribution, tfVersion, ctx.Workspace)
if p.isRemoteOpsErr(output, err) {
ctx.Log.Debug("detected that this project is using TFE remote ops")
return p.remotePlan(ctx, extraArgs, path, tfVersion, planFile, envs)
return p.remotePlan(ctx, extraArgs, path, tfDistribution, tfVersion, planFile, envs)
}
if err != nil {
return output, err
@@ -72,14 +79,14 @@ func (p *planStepRunner) isRemoteOpsErr(output string, err error) bool {
// remotePlan runs a terraform plan command compatible with TFE remote
// operations.
func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) {
func (p *planStepRunner) remotePlan(ctx command.ProjectContext, extraArgs []string, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, planFile string, envs map[string]string) (string, error) {
argList := [][]string{
{"plan", "-input=false", "-refresh", "-no-color"},
extraArgs,
ctx.EscapedCommentArgs,
}
args := p.flatten(argList)
output, err := p.runRemotePlan(ctx, args, path, tfVersion, envs)
output, err := p.runRemotePlan(ctx, args, path, tfDistribution, tfVersion, envs)
if err != nil {
return output, err
}
@@ -193,6 +200,7 @@ func (p *planStepRunner) runRemotePlan(
ctx command.ProjectContext,
cmdArgs []string,
path string,
tfDistribution terraform.Distribution,
tfVersion *version.Version,
envs map[string]string) (string, error) {
@@ -205,7 +213,7 @@ func (p *planStepRunner) runRemotePlan(
// Start the async command execution.
ctx.Log.Debug("starting async tf remote operation")
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfVersion, ctx.Workspace)
_, outCh := p.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), cmdArgs, envs, tfDistribution, tfVersion, ctx.Workspace)
var lines []string
nextLineIsRunURL := false
var runURL string

View File

@@ -13,7 +13,9 @@ import (
"github.com/runatlantis/atlantis/server/core/runtime"
runtimemocks "github.com/runatlantis/atlantis/server/core/runtime/mocks"
runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -24,7 +26,7 @@ import (
func TestRun_AddsEnvVarFile(t *testing.T) {
// Test that if env/workspace.tfvars file exists we use -var-file option.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
@@ -36,10 +38,12 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
err = os.WriteFile(envVarsFile, nil, 0600)
Ok(t, err)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
// Using version >= 0.10 here so we don't expect any env commands.
tfVersion, _ := version.NewVersion("0.10.0")
logger := logging.NewNoopLogger(t)
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
expPlanArgs := []string{"plan",
"-input=false",
@@ -78,14 +82,14 @@ func TestRun_AddsEnvVarFile(t *testing.T) {
Name: "repo",
},
}
When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("output", nil)
When(terraform.RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("output", nil)
output, err := s.Run(ctx, []string{"extra", "args"}, tmpDir, map[string]string(nil))
Ok(t, err)
// Verify that env select was never called since we're in version >= 0.10
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, tmpDir, []string{"env", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, tmpDir, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
Equals(t, "output", output)
}
@@ -93,12 +97,14 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
// Test that if running for a project, uses a different path for the plan
// file.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.10.0")
logger := logging.NewNoopLogger(t)
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
ctx := command.ProjectContext{
Log: logger,
Workspace: "default",
@@ -115,7 +121,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
Name: "repo",
},
}
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil)
expPlanArgs := []string{"plan",
"-input=false",
@@ -137,7 +143,7 @@ func TestRun_UsesDiffPathForProject(t *testing.T) {
"comment",
"args",
}
When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")).ThenReturn("output", nil)
When(terraform.RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")).ThenReturn("output", nil)
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
@@ -173,16 +179,19 @@ Terraform will perform the following actions:
- aws_security_group_rule.allow_all
`
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.10.0")
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
When(terraform.RunCommandWithVersion(
Any[command.ProjectContext](),
Any[string](),
Any[[]string](),
Any[map[string]string](),
Any[tf.Distribution](),
Any[*version.Version](),
Any[string]())).
Then(func(params []Param) ReturnValues {
@@ -223,11 +232,13 @@ Terraform will perform the following actions:
// Test that even if there's an error, we get the returned output.
func TestRun_OutputOnErr(t *testing.T) {
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.10.0")
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
expOutput := "expected output"
expErrMsg := "error!"
When(terraform.RunCommandWithVersion(
@@ -235,6 +246,7 @@ func TestRun_OutputOnErr(t *testing.T) {
Any[string](),
Any[[]string](),
Any[map[string]string](),
Any[tf.Distribution](),
Any[*version.Version](),
Any[string]())).
Then(func(params []Param) ReturnValues {
@@ -287,7 +299,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
When(terraform.RunCommandWithVersion(
@@ -295,11 +307,14 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
Any[string](),
Any[[]string](),
Any[map[string]string](),
Any[tf.Distribution](),
Any[*version.Version](),
Any[string]())).ThenReturn("output", nil)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion(c.tfVersion)
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTfExec)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
ctx := command.ProjectContext{
Workspace: "default",
RepoRelDir: ".",
@@ -319,7 +334,7 @@ func TestRun_NoOptionalVarsIn012(t *testing.T) {
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfVersion, "default")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")
})
}
@@ -385,11 +400,13 @@ locally at this time.
},
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion(c.tfVersion)
asyncTf := &remotePlanMock{}
s := runtime.NewPlanStepRunner(terraform, tfVersion, commitStatusUpdater, asyncTf)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTf)
absProjectPath := t.TempDir()
// First, terraform workspace gets run.
@@ -398,6 +415,7 @@ locally at this time.
absProjectPath,
[]string{"workspace", "show"},
map[string]string(nil),
tfDistribution,
tfVersion,
"default")).ThenReturn("default\n", nil)
@@ -438,7 +456,7 @@ locally at this time.
planErr := errors.New("exit status 1: err")
planOutput := "\n" + c.remoteOpsErr
asyncTf.LinesToSend = remotePlanOutput
When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfVersion, "default")).
When(terraform.RunCommandWithVersion(ctx, absProjectPath, expPlanArgs, map[string]string(nil), tfDistribution, tfVersion, "default")).
ThenReturn(planOutput, planErr)
output, err := s.Run(ctx, []string{"extra", "args"}, absProjectPath, map[string]string(nil))
@@ -536,6 +554,82 @@ Plan: 0 to add, 0 to change, 1 to destroy.`, output)
}
}
func TestPlanStepRunner_TestRun_UsesConfiguredDistribution(t *testing.T) {
RegisterMockTestingT(t)
expPlanArgs := []string{
"plan",
"-input=false",
"-refresh",
"-out",
fmt.Sprintf("%q", "/path/default.tfplan"),
"extra",
"args",
"comment",
"args",
}
cases := []struct {
name string
tfVersion string
tfDistribution string
}{
{
"stable version",
"0.12.0",
"terraform",
},
{
"with prerelease",
"0.14.0-rc1",
"opentofu",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
terraform := tfclientmocks.NewMockClient()
commitStatusUpdater := runtimemocks.NewMockStatusUpdater()
asyncTfExec := runtimemocks.NewMockAsyncTFExec()
When(terraform.RunCommandWithVersion(
Any[command.ProjectContext](),
Any[string](),
Any[[]string](),
Any[map[string]string](),
Any[tf.Distribution](),
Any[*version.Version](),
Any[string]())).ThenReturn("output", nil)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion(c.tfVersion)
s := runtime.NewPlanStepRunner(terraform, tfDistribution, tfVersion, commitStatusUpdater, asyncTfExec)
ctx := command.ProjectContext{
Workspace: "default",
RepoRelDir: ".",
User: models.User{Username: "username"},
EscapedCommentArgs: []string{"comment", "args"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
Owner: "owner",
Name: "repo",
},
TerraformDistribution: &c.tfDistribution,
}
output, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx), Eq("/path"), Eq(expPlanArgs), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default"))
})
}
}
type remotePlanMock struct {
// LinesToSend will be sent on the channel.
LinesToSend string
@@ -543,7 +637,7 @@ type remotePlanMock struct {
CalledArgs []string
}
func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {
func (r *remotePlanMock) RunCommandAsync(_ command.ProjectContext, _ string, args []string, _ map[string]string, _ tf.Distribution, _ *version.Version, _ string) (chan<- string, <-chan runtimemodels.Line) {
r.CalledArgs = args
in := make(chan string)
out := make(chan runtimemodels.Line)

View File

@@ -153,3 +153,148 @@ func TestRunDelegate(t *testing.T) {
})
}
var openTofuPlanFileContents = `
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
OpenTofu will perform the following actions:
- null_resource.hi[1]
Plan: 0 to add, 0 to change, 1 to destroy.`
func TestRunDelegate_UsesConfiguredDistribution(t *testing.T) {
RegisterMockTestingT(t)
mockDefaultRunner := mocks.NewMockRunner()
mockRemoteRunner := mocks.NewMockRunner()
subject := &planTypeStepRunnerDelegate{
defaultRunner: mockDefaultRunner,
remotePlanRunner: mockRemoteRunner,
}
tfDistribution := "opentofu"
tfVersion, _ := version.NewVersion("1.7.0")
t.Run("Remote Runner Success", func(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600)
Ok(t, err)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
EscapedCommentArgs: []string{"comment", "args"},
TerraformDistribution: &tfDistribution,
TerraformVersion: tfVersion,
}
extraArgs := []string{"extra", "args"}
envs := map[string]string{}
expectedOut := "some random output"
When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)
output, err := subject.Run(ctx, extraArgs, tmpDir, envs)
mockDefaultRunner.VerifyWasCalled(Never())
Equals(t, expectedOut, output)
Ok(t, err)
})
t.Run("Remote Runner Failure", func(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, []byte("Atlantis: this plan was created by remote ops\n"+openTofuPlanFileContents), 0600)
Ok(t, err)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
EscapedCommentArgs: []string{"comment", "args"},
TerraformDistribution: &tfDistribution,
TerraformVersion: tfVersion,
}
extraArgs := []string{"extra", "args"}
envs := map[string]string{}
expectedOut := "some random output"
When(mockRemoteRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err"))
output, err := subject.Run(ctx, extraArgs, tmpDir, envs)
mockDefaultRunner.VerifyWasCalled(Never())
Equals(t, expectedOut, output)
Assert(t, err != nil, "err should not be nil")
})
t.Run("Local Runner Success", func(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600)
Ok(t, err)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
EscapedCommentArgs: []string{"comment", "args"},
TerraformDistribution: &tfDistribution,
TerraformVersion: tfVersion,
}
extraArgs := []string{"extra", "args"}
envs := map[string]string{}
expectedOut := "some random output"
When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, nil)
output, err := subject.Run(ctx, extraArgs, tmpDir, envs)
mockRemoteRunner.VerifyWasCalled(Never())
Equals(t, expectedOut, output)
Ok(t, err)
})
t.Run("Local Runner Failure", func(t *testing.T) {
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, "workspace.tfplan")
err := os.WriteFile(planPath, []byte(openTofuPlanFileContents), 0600)
Ok(t, err)
ctx := command.ProjectContext{
Workspace: "workspace",
RepoRelDir: ".",
EscapedCommentArgs: []string{"comment", "args"},
TerraformDistribution: &tfDistribution,
TerraformVersion: tfVersion,
}
extraArgs := []string{"extra", "args"}
envs := map[string]string{}
expectedOut := "some random output"
When(mockDefaultRunner.Run(ctx, extraArgs, tmpDir, envs)).ThenReturn(expectedOut, errors.New("err"))
output, err := subject.Run(ctx, extraArgs, tmpDir, envs)
mockRemoteRunner.VerifyWasCalled(Never())
Equals(t, expectedOut, output)
Assert(t, err != nil, "err should not be nil")
})
}

View File

@@ -3,6 +3,7 @@ package runtime
import (
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
)
@@ -13,7 +14,7 @@ type policyCheckStepRunner struct {
}
// NewPolicyCheckStepRunner creates a new step runner from an executor workflow
func NewPolicyCheckStepRunner(defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) {
func NewPolicyCheckStepRunner(defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, executorWorkflow VersionedExecutorWorkflow) (Runner, error) {
policyCheckStepRunner := &policyCheckStepRunner{
versionEnsurer: executorWorkflow,
executor: executorWorkflow,

View File

@@ -8,7 +8,8 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tf "github.com/runatlantis/atlantis/server/core/terraform"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
"github.com/runatlantis/atlantis/server/logging"
@@ -142,8 +143,8 @@ func TestPostWorkflowHookRunner_Run(t *testing.T) {
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())).
terraform := tfclientmocks.NewMockClient()
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).
ThenReturn(nil)
logger := logging.NewNoopLogger(t)

View File

@@ -9,7 +9,8 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tf "github.com/runatlantis/atlantis/server/core/terraform"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
"github.com/runatlantis/atlantis/server/logging"
@@ -162,8 +163,8 @@ func TestPreWorkflowHookRunner_Run(t *testing.T) {
Ok(t, err)
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())).
terraform := tfclientmocks.NewMockClient()
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).
ThenReturn(nil)
logger := logging.NewNoopLogger(t)

View File

@@ -9,14 +9,16 @@ import (
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/jobs"
)
// RunStepRunner runs custom commands.
type RunStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
TerraformExecutor TerraformExec
DefaultTFDistribution terraform.Distribution
DefaultTFVersion *version.Version
// TerraformBinDir is the directory where Atlantis downloads Terraform binaries.
TerraformBinDir string
ProjectCmdOutputHandler jobs.ProjectCommandOutputHandler
@@ -31,12 +33,16 @@ func (r *RunStepRunner) Run(
streamOutput bool,
postProcessOutput valid.PostProcessRunOutputOption,
) (string, error) {
tfDistribution := r.DefaultTFDistribution
tfVersion := r.DefaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfVersion)
err := r.TerraformExecutor.EnsureVersion(ctx.Log, tfDistribution, tfVersion)
if err != nil {
err = fmt.Errorf("%s: Downloading terraform Version %s", err, tfVersion.String())
ctx.Log.Debug("error: %s", err)
@@ -45,27 +51,28 @@ func (r *RunStepRunner) Run(
baseEnvVars := os.Environ()
customEnvVars := map[string]string{
"ATLANTIS_TERRAFORM_VERSION": tfVersion.String(),
"BASE_BRANCH_NAME": ctx.Pull.BaseBranch,
"BASE_REPO_NAME": ctx.BaseRepo.Name,
"BASE_REPO_OWNER": ctx.BaseRepo.Owner,
"COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","),
"DIR": path,
"HEAD_BRANCH_NAME": ctx.Pull.HeadBranch,
"HEAD_COMMIT": ctx.Pull.HeadCommit,
"HEAD_REPO_NAME": ctx.HeadRepo.Name,
"HEAD_REPO_OWNER": ctx.HeadRepo.Owner,
"PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir),
"PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)),
"SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()),
"POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()),
"PROJECT_NAME": ctx.ProjectName,
"PULL_AUTHOR": ctx.Pull.Author,
"PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num),
"PULL_URL": ctx.Pull.URL,
"REPO_REL_DIR": ctx.RepoRelDir,
"USER_NAME": ctx.User.Username,
"WORKSPACE": ctx.Workspace,
"ATLANTIS_TERRAFORM_DISTRIBUTION": tfDistribution.BinName(),
"ATLANTIS_TERRAFORM_VERSION": tfVersion.String(),
"BASE_BRANCH_NAME": ctx.Pull.BaseBranch,
"BASE_REPO_NAME": ctx.BaseRepo.Name,
"BASE_REPO_OWNER": ctx.BaseRepo.Owner,
"COMMENT_ARGS": strings.Join(ctx.EscapedCommentArgs, ","),
"DIR": path,
"HEAD_BRANCH_NAME": ctx.Pull.HeadBranch,
"HEAD_COMMIT": ctx.Pull.HeadCommit,
"HEAD_REPO_NAME": ctx.HeadRepo.Name,
"HEAD_REPO_OWNER": ctx.HeadRepo.Owner,
"PATH": fmt.Sprintf("%s:%s", os.Getenv("PATH"), r.TerraformBinDir),
"PLANFILE": filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName)),
"SHOWFILE": filepath.Join(path, ctx.GetShowResultFileName()),
"POLICYCHECKFILE": filepath.Join(path, ctx.GetPolicyCheckResultFileName()),
"PROJECT_NAME": ctx.ProjectName,
"PULL_AUTHOR": ctx.Pull.Author,
"PULL_NUM": fmt.Sprintf("%d", ctx.Pull.Num),
"PULL_URL": ctx.Pull.URL,
"REPO_REL_DIR": ctx.RepoRelDir,
"USER_NAME": ctx.User.Username,
"WORKSPACE": ctx.Workspace,
}
finalEnvVars := baseEnvVars

View File

@@ -10,7 +10,9 @@ import (
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
@@ -20,11 +22,12 @@ import (
func TestRunStepRunner_Run(t *testing.T) {
cases := []struct {
Command string
ProjectName string
ExpOut string
ExpErr string
Version string
Command string
ProjectName string
ExpOut string
ExpErr string
Version string
Distribution string
}{
{
Command: "",
@@ -69,6 +72,18 @@ func TestRunStepRunner_Run(t *testing.T) {
ProjectName: "my/project/name",
ExpOut: "workspace=myworkspace version=0.11.0 dir=$DIR planfile=$DIR/my::project::name-myworkspace.tfplan showfile=$DIR/my::project::name-myworkspace.json project=my/project/name\n",
},
{
Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION",
ProjectName: "my/project/name",
ExpOut: "distribution=terraform\n",
Distribution: "terraform",
},
{
Command: "echo distribution=$ATLANTIS_TERRAFORM_DISTRIBUTION",
ProjectName: "my/project/name",
ExpOut: "distribution=tofu\n",
Distribution: "opentofu",
},
{
Command: "echo base_repo_name=$BASE_REPO_NAME base_repo_owner=$BASE_REPO_OWNER head_repo_name=$HEAD_REPO_NAME head_repo_owner=$HEAD_REPO_OWNER head_branch_name=$HEAD_BRANCH_NAME head_commit=$HEAD_COMMIT base_branch_name=$BASE_BRANCH_NAME pull_num=$PULL_NUM pull_url=$PULL_URL pull_author=$PULL_AUTHOR repo_rel_dir=$REPO_REL_DIR",
ExpOut: "base_repo_name=basename base_repo_owner=baseowner head_repo_name=headname head_repo_owner=headowner head_branch_name=add-feat head_commit=12345abcdef base_branch_name=main pull_num=2 pull_url=https://github.com/runatlantis/atlantis/pull/2 pull_author=acme repo_rel_dir=mydir\n",
@@ -100,11 +115,17 @@ func TestRunStepRunner_Run(t *testing.T) {
Ok(t, err)
projTFDistribution := "terraform"
if c.Distribution != "" {
projTFDistribution = c.Distribution
}
defaultVersion, _ := version.NewVersion("0.8")
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[*version.Version]())).
terraform := tfclientmocks.NewMockClient()
defaultDistribution := tf.NewDistributionTerraformWithDownloader(mocks.NewMockDownloader())
When(terraform.EnsureVersion(Any[logging.SimpleLogging](), Any[tf.Distribution](), Any[*version.Version]())).
ThenReturn(nil)
logger := logging.NewNoopLogger(t)
@@ -113,6 +134,7 @@ func TestRunStepRunner_Run(t *testing.T) {
r := runtime.RunStepRunner{
TerraformExecutor: terraform,
DefaultTFDistribution: defaultDistribution,
DefaultTFVersion: defaultVersion,
TerraformBinDir: "/bin/dir",
ProjectCmdOutputHandler: projectCmdOutputHandler,
@@ -138,12 +160,13 @@ func TestRunStepRunner_Run(t *testing.T) {
User: models.User{
Username: "acme-user",
},
Log: logger,
Workspace: "myworkspace",
RepoRelDir: "mydir",
TerraformVersion: projVersion,
ProjectName: c.ProjectName,
EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"},
Log: logger,
Workspace: "myworkspace",
RepoRelDir: "mydir",
TerraformDistribution: &projTFDistribution,
TerraformVersion: projVersion,
ProjectName: c.ProjectName,
EscapedCommentArgs: []string{"-target=resource1", "-target=resource2"},
}
out, err := r.Run(ctx, nil, c.Command, tmpDir, map[string]string{"test": "var"}, true, valid.PostProcessRunOutputShow)
if c.ExpErr != "" {
@@ -157,8 +180,8 @@ func TestRunStepRunner_Run(t *testing.T) {
expOut := strings.Replace(c.ExpOut, "$DIR", tmpDir, -1)
Equals(t, expOut, out)
terraform.VerifyWasCalledOnce().EnsureVersion(logger, projVersion)
terraform.VerifyWasCalled(Never()).EnsureVersion(logger, defaultVersion)
terraform.VerifyWasCalledOnce().EnsureVersion(Eq(logger), NotEq(defaultDistribution), Eq(projVersion))
terraform.VerifyWasCalled(Never()).EnsureVersion(Eq(logger), Eq(defaultDistribution), Eq(defaultVersion))
})
}

View File

@@ -11,6 +11,7 @@ import (
version "github.com/hashicorp/go-version"
"github.com/pkg/errors"
runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -26,8 +27,8 @@ const (
// TerraformExec brings the interface from TerraformClient into this package
// without causing circular imports.
type TerraformExec interface {
RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error)
EnsureVersion(log logging.SimpleLogging, v *version.Version) error
RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error)
EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error
}
// AsyncTFExec brings the interface from TerraformClient into this package
@@ -43,7 +44,7 @@ type AsyncTFExec interface {
// Callers can use the input channel to pass stdin input to the command.
// If any error is passed on the out channel, there will be no
// further output (so callers are free to exit).
RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line)
RunCommandAsync(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan runtimemodels.Line)
}
// StatusUpdater brings the interface from CommitStatusUpdater into this package

View File

@@ -6,15 +6,17 @@ import (
"github.com/hashicorp/go-version"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
)
const minimumShowTfVersion string = "0.12.0"
func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version) (Runner, error) {
func NewShowStepRunner(executor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTFVersion *version.Version) (Runner, error) {
showStepRunner := &showStepRunner{
terraformExecutor: executor,
defaultTFVersion: defaultTFVersion,
terraformExecutor: executor,
defaultTfDistribution: defaultTfDistribution,
defaultTFVersion: defaultTFVersion,
}
remotePlanRunner := NullRunner{}
runner := NewPlanTypeStepRunnerDelegate(showStepRunner, remotePlanRunner)
@@ -23,12 +25,17 @@ func NewShowStepRunner(executor TerraformExec, defaultTFVersion *version.Version
// showStepRunner runs terraform show on an existing plan file and outputs it to a json file
type showStepRunner struct {
terraformExecutor TerraformExec
defaultTFVersion *version.Version
terraformExecutor TerraformExec
defaultTfDistribution terraform.Distribution
defaultTFVersion *version.Version
}
func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) {
tfDistribution := p.defaultTfDistribution
tfVersion := p.defaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
@@ -41,6 +48,7 @@ func (p *showStepRunner) Run(ctx command.ProjectContext, _ []string, path string
path,
[]string{"show", "-json", filepath.Clean(planFile)},
envs,
tfDistribution,
tfVersion,
ctx.Workspace,
)

View File

@@ -9,7 +9,9 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
@@ -20,6 +22,8 @@ func TestShowStepRunnner(t *testing.T) {
path := t.TempDir()
resultPath := filepath.Join(path, "test-default.json")
envs := map[string]string{"key": "val"}
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.12")
context := command.ProjectContext{
Workspace: "default",
@@ -29,17 +33,18 @@ func TestShowStepRunnner(t *testing.T) {
RegisterMockTestingT(t)
mockExecutor := mocks.NewMockClient()
mockExecutor := tfclientmocks.NewMockClient()
subject := showStepRunner{
terraformExecutor: mockExecutor,
defaultTFVersion: tfVersion,
terraformExecutor: mockExecutor,
defaultTfDistribution: tfDistribution,
defaultTFVersion: tfVersion,
}
t.Run("success", func(t *testing.T) {
When(mockExecutor.RunCommandWithVersion(
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace,
)).ThenReturn("success", nil)
r, err := subject.Run(context, []string{}, path, envs)
@@ -57,6 +62,8 @@ func TestShowStepRunnner(t *testing.T) {
t.Run("success w/ version override", func(t *testing.T) {
v, _ := version.NewVersion("0.13.0")
mockDownloader := mocks.NewMockDownloader()
d := tf.NewDistributionTerraformWithDownloader(mockDownloader)
contextWithVersionOverride := command.ProjectContext{
Workspace: "default",
@@ -66,7 +73,7 @@ func TestShowStepRunnner(t *testing.T) {
}
When(mockExecutor.RunCommandWithVersion(
contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, v, context.Workspace,
contextWithVersionOverride, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, d, v, context.Workspace,
)).ThenReturn("success", nil)
r, err := subject.Run(contextWithVersionOverride, []string{}, path, envs)
@@ -81,9 +88,39 @@ func TestShowStepRunnner(t *testing.T) {
})
t.Run("success w/ distribution override", func(t *testing.T) {
v, _ := version.NewVersion("0.13.0")
mockDownloader := mocks.NewMockDownloader()
d := tf.NewDistributionTerraformWithDownloader(mockDownloader)
projTFDistribution := "opentofu"
contextWithDistributionOverride := command.ProjectContext{
Workspace: "default",
ProjectName: "test",
Log: logger,
TerraformDistribution: &projTFDistribution,
}
When(mockExecutor.RunCommandWithVersion(
Eq(contextWithDistributionOverride), Eq(path), Eq([]string{"show", "-json", filepath.Join(path, "test-default.tfplan")}), Eq(envs), NotEq(d), NotEq(v), Eq(context.Workspace),
)).ThenReturn("success", nil)
r, err := subject.Run(contextWithDistributionOverride, []string{}, path, envs)
Ok(t, err)
actual, _ := os.ReadFile(resultPath)
actualStr := string(actual)
Assert(t, actualStr == "success", "got expected result")
Assert(t, r == "success", "returned expected result")
})
t.Run("failure running command", func(t *testing.T) {
When(mockExecutor.RunCommandWithVersion(
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfVersion, context.Workspace,
context, path, []string{"show", "-json", filepath.Join(path, "test-default.tfplan")}, envs, tfDistribution, tfVersion, context.Workspace,
)).ThenReturn("success", errors.New("error"))
_, err := subject.Run(context, []string{}, path, envs)

View File

@@ -5,25 +5,32 @@ import (
"path/filepath"
version "github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/utils"
)
type stateRmStepRunner struct {
terraformExecutor TerraformExec
defaultTFVersion *version.Version
terraformExecutor TerraformExec
defaultTFDistribution terraform.Distribution
defaultTFVersion *version.Version
}
func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfVersion *version.Version) Runner {
func NewStateRmStepRunner(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version) Runner {
runner := &stateRmStepRunner{
terraformExecutor: terraformExecutor,
defaultTFVersion: defaultTfVersion,
terraformExecutor: terraformExecutor,
defaultTFDistribution: defaultTfDistribution,
defaultTFVersion: defaultTfVersion,
}
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfVersion, runner)
return NewWorkspaceStepRunnerDelegate(terraformExecutor, defaultTfDistribution, defaultTfVersion, runner)
}
func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfDistribution := p.defaultTFDistribution
tfVersion := p.defaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
@@ -31,7 +38,7 @@ func (p *stateRmStepRunner) Run(ctx command.ProjectContext, extraArgs []string,
stateRmCmd := []string{"state", "rm"}
stateRmCmd = append(stateRmCmd, extraArgs...)
stateRmCmd = append(stateRmCmd, ctx.EscapedCommentArgs...)
out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfVersion, ctx.Workspace)
out, err := p.terraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), stateRmCmd, envs, tfDistribution, tfVersion, ctx.Workspace)
// If the state rm was successful and a plan file exists, delete the plan.
planPath := filepath.Join(path, GetPlanFilename(ctx.Workspace, ctx.ProjectName))

View File

@@ -8,7 +8,9 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/logging"
. "github.com/runatlantis/atlantis/testing"
@@ -29,17 +31,19 @@ func TestStateRmStepRunner_Run_Success(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewStateRmStepRunner(terraform, tfVersion)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, "default")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, "default")
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
@@ -59,23 +63,67 @@ func TestStateRmStepRunner_Run_Workspace(t *testing.T) {
}
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
s := NewStateRmStepRunner(terraform, tfVersion)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[*version.Version](), Any[string]())).
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
// switch workspace
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"workspace", "select", workspace}, map[string]string(nil), tfDistribution, tfVersion, workspace)
// exec state rm
commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfVersion, workspace)
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, commands, map[string]string(nil), tfDistribution, tfVersion, workspace)
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")
}
func TestStateRmStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {
logger := logging.NewNoopLogger(t)
workspace := "something"
tmpDir := t.TempDir()
planPath := filepath.Join(tmpDir, fmt.Sprintf("%s.tfplan", workspace))
err := os.WriteFile(planPath, nil, 0600)
Ok(t, err)
projTFDistribution := "opentofu"
context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"-lock=false", "addr1", "addr2", "addr3"},
Workspace: workspace,
TerraformDistribution: &projTFDistribution,
}
RegisterMockTestingT(t)
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion("0.15.0")
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
s := NewStateRmStepRunner(terraform, tfDistribution, tfVersion)
When(terraform.RunCommandWithVersion(Any[command.ProjectContext](), Any[string](), Any[[]string](), Any[map[string]string](), Any[tf.Distribution](), Any[*version.Version](), Any[string]())).
ThenReturn("output", nil)
output, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
Ok(t, err)
Equals(t, "output", output)
// switch workspace
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "show"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"workspace", "select", workspace}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
// exec state rm
commands := []string{"state", "rm", "-lock=false", "addr1", "addr2", "addr3"}
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq(commands), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq(workspace))
_, err = os.Stat(planPath)
Assert(t, os.IsNotExist(err), "planfile should be deleted")

View File

@@ -4,22 +4,28 @@ import (
"path/filepath"
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
)
// VersionStepRunner runs a version command given a ctx
type VersionStepRunner struct {
TerraformExecutor TerraformExec
DefaultTFVersion *version.Version
TerraformExecutor TerraformExec
DefaultTFDistribution terraform.Distribution
DefaultTFVersion *version.Version
}
// Run ensures a given version for the executable, builds the args from the project context and then runs executable returning the result
func (v *VersionStepRunner) Run(ctx command.ProjectContext, _ []string, path string, envs map[string]string) (string, error) {
tfDistribution := v.DefaultTFDistribution
tfVersion := v.DefaultTFVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
versionCmd := []string{"version"}
return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfVersion, ctx.Workspace)
return v.TerraformExecutor.RunCommandWithVersion(ctx, filepath.Clean(path), versionCmd, envs, tfDistribution, tfVersion, ctx.Workspace)
}

View File

@@ -5,7 +5,9 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -33,18 +35,62 @@ func TestRunVersionStep(t *testing.T) {
},
}
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.15.0")
tmpDir := t.TempDir()
s := &VersionStepRunner{
TerraformExecutor: terraform,
DefaultTFVersion: tfVersion,
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
t.Run("ensure runs", func(t *testing.T) {
_, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfVersion, "default")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(context, tmpDir, []string{"version"}, map[string]string(nil), tfDistribution, tfVersion, "default")
Ok(t, err)
})
}
func TestVersionStepRunner_Run_UsesConfiguredDistribution(t *testing.T) {
RegisterMockTestingT(t)
logger := logging.NewNoopLogger(t)
workspace := "default"
projTFDistribution := "opentofu"
context := command.ProjectContext{
Log: logger,
EscapedCommentArgs: []string{"comment", "args"},
Workspace: workspace,
RepoRelDir: ".",
User: models.User{Username: "username"},
Pull: models.PullRequest{
Num: 2,
},
BaseRepo: models.Repo{
FullName: "owner/repo",
Owner: "owner",
Name: "repo",
},
TerraformDistribution: &projTFDistribution,
}
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.15.0")
tmpDir := t.TempDir()
s := &VersionStepRunner{
TerraformExecutor: terraform,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
}
t.Run("ensure runs", func(t *testing.T) {
_, err := s.Run(context, []string{}, tmpDir, map[string]string(nil))
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(context), Eq(tmpDir), Eq([]string{"version"}), Eq(map[string]string(nil)), NotEq(tfDistribution), Eq(tfVersion), Eq("default"))
Ok(t, err)
})
}

View File

@@ -5,33 +5,40 @@ import (
"strings"
"github.com/hashicorp/go-version"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/events/command"
)
// workspaceStepRunnerDelegate ensures that a given step runner run on switched workspace
type workspaceStepRunnerDelegate struct {
terraformExecutor TerraformExec
defaultTfVersion *version.Version
delegate Runner
terraformExecutor TerraformExec
defaultTfDistribution terraform.Distribution
defaultTfVersion *version.Version
delegate Runner
}
func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfVersion *version.Version, delegate Runner) Runner {
func NewWorkspaceStepRunnerDelegate(terraformExecutor TerraformExec, defaultTfDistribution terraform.Distribution, defaultTfVersion *version.Version, delegate Runner) Runner {
return &workspaceStepRunnerDelegate{
terraformExecutor: terraformExecutor,
defaultTfVersion: defaultTfVersion,
delegate: delegate,
terraformExecutor: terraformExecutor,
defaultTfDistribution: defaultTfDistribution,
defaultTfVersion: defaultTfVersion,
delegate: delegate,
}
}
func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
tfDistribution := r.defaultTfDistribution
tfVersion := r.defaultTfVersion
if ctx.TerraformDistribution != nil {
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
}
if ctx.TerraformVersion != nil {
tfVersion = ctx.TerraformVersion
}
// We only need to switch workspaces in version 0.9.*. In older versions,
// there is no such thing as a workspace so we don't need to do anything.
if err := r.switchWorkspace(ctx, path, tfVersion, envs); err != nil {
if err := r.switchWorkspace(ctx, path, tfDistribution, tfVersion, envs); err != nil {
return "", err
}
@@ -40,7 +47,7 @@ func (r *workspaceStepRunnerDelegate) Run(ctx command.ProjectContext, extraArgs
// switchWorkspace changes the terraform workspace if necessary and will create
// it if it doesn't exist. It handles differences between versions.
func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfVersion *version.Version, envs map[string]string) error {
func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext, path string, tfDistribution terraform.Distribution, tfVersion *version.Version, envs map[string]string) error {
// In versions less than 0.9 there is no support for workspaces.
noWorkspaceSupport := MustConstraint("<0.9").Check(tfVersion)
// If the user tried to set a specific workspace in the comment but their
@@ -63,7 +70,7 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext
// already in the right workspace then no need to switch. This will save us
// about ten seconds. This command is only available in > 0.10.
if !runningZeroPointNine {
workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfVersion, ctx.Workspace)
workspaceShowOutput, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "show"}, envs, tfDistribution, tfVersion, ctx.Workspace)
if err != nil {
return err
}
@@ -78,11 +85,11 @@ func (r *workspaceStepRunnerDelegate) switchWorkspace(ctx command.ProjectContext
// To do this we can either select and catch the error or use list and then
// look for the workspace. Both commands take the same amount of time so
// that's why we're running select here.
_, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
_, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "select", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace)
if err != nil {
// If terraform workspace select fails we run terraform workspace
// new to create a new workspace automatically.
out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfVersion, ctx.Workspace)
out, err := r.terraformExecutor.RunCommandWithVersion(ctx, path, []string{workspaceCmd, "new", ctx.Workspace}, envs, tfDistribution, tfVersion, ctx.Workspace)
if err != nil {
return fmt.Errorf("%s: %s", err, out)
}

View File

@@ -6,7 +6,9 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
tf "github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/logging"
@@ -16,7 +18,9 @@ import (
func TestRun_NoWorkspaceIn08(t *testing.T) {
// We don't want any workspace commands to be run in 0.8.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.8")
workspace := "default"
logger := logging.NewNoopLogger(t)
@@ -24,7 +28,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
Log: logger,
Workspace: workspace,
}
s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{})
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
_, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
@@ -36,6 +40,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
"select",
"workspace"},
map[string]string(nil),
tfDistribution,
tfVersion,
workspace)
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx,
@@ -44,6 +49,7 @@ func TestRun_NoWorkspaceIn08(t *testing.T) {
"select",
"workspace"},
map[string]string(nil),
tfDistribution,
tfVersion,
workspace)
}
@@ -52,11 +58,13 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) {
// If they attempt to use a workspace other than default in 0.8
// we should error.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.8")
logger := logging.NewNoopLogger(t)
workspace := "notdefault"
s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{})
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
_, err := s.Run(command.ProjectContext{
Log: logger,
@@ -67,6 +75,8 @@ func TestRun_ErrWorkspaceIn08(t *testing.T) {
func TestRun_SwitchesWorkspace(t *testing.T) {
RegisterMockTestingT(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
cases := []struct {
tfVersion string
@@ -92,14 +102,14 @@ func TestRun_SwitchesWorkspace(t *testing.T) {
for _, c := range cases {
t.Run(c.tfVersion, func(t *testing.T) {
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion(c.tfVersion)
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
Log: logger,
Workspace: "workspace",
}
s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{})
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
_, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
@@ -111,12 +121,74 @@ func TestRun_SwitchesWorkspace(t *testing.T) {
"select",
"workspace"},
map[string]string(nil),
tfDistribution,
tfVersion,
"workspace")
})
}
}
func TestRun_SwitchesWorkspaceDistribution(t *testing.T) {
RegisterMockTestingT(t)
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
cases := []struct {
tfVersion string
tfDistribution string
expWorkspaceCmd string
}{
{
"0.9.0",
"opentofu",
"env",
},
{
"0.9.11",
"terraform",
"env",
},
{
"0.10.0",
"terraform",
"workspace",
},
{
"0.11.0",
"opentofu",
"workspace",
},
}
for _, c := range cases {
t.Run(c.tfVersion, func(t *testing.T) {
terraform := tfclientmocks.NewMockClient()
tfVersion, _ := version.NewVersion(c.tfVersion)
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
Log: logger,
Workspace: "workspace",
TerraformDistribution: &c.tfDistribution,
}
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
_, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
// Verify that env select was called as well as plan.
terraform.VerifyWasCalledOnce().RunCommandWithVersion(Eq(ctx),
Eq("/path"),
Eq([]string{c.expWorkspaceCmd,
"select",
"workspace"}),
Eq(map[string]string(nil)),
NotEq(tfDistribution),
Eq(tfVersion),
Eq("workspace"))
})
}
}
func TestRun_CreatesWorkspace(t *testing.T) {
// Test that if `workspace select` fails, we call `workspace new`.
RegisterMockTestingT(t)
@@ -145,7 +217,9 @@ func TestRun_CreatesWorkspace(t *testing.T) {
for _, c := range cases {
t.Run(c.tfVersion, func(t *testing.T) {
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion(c.tfVersion)
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
@@ -163,20 +237,20 @@ func TestRun_CreatesWorkspace(t *testing.T) {
Name: "repo",
},
}
s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{})
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
// Ensure that we actually try to switch workspaces by making the
// output of `workspace show` to be a different name.
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil)
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("diffworkspace\n", nil)
expWorkspaceArgs := []string{c.expWorkspaceCommand, "select", "workspace"}
When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
When(terraform.RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("", errors.New("workspace does not exist"))
_, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
// Verify that env select was called as well as plan.
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalledOnce().RunCommandWithVersion(ctx, "/path", expWorkspaceArgs, map[string]string(nil), tfDistribution, tfVersion, "workspace")
})
}
}
@@ -185,7 +259,9 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
// Tests that if workspace show says we're on the right workspace we don't
// switch.
RegisterMockTestingT(t)
terraform := mocks.NewMockClient()
terraform := tfclientmocks.NewMockClient()
mockDownloader := mocks.NewMockDownloader()
tfDistribution := tf.NewDistributionTerraformWithDownloader(mockDownloader)
tfVersion, _ := version.NewVersion("0.10.0")
logger := logging.NewNoopLogger(t)
ctx := command.ProjectContext{
@@ -203,12 +279,12 @@ func TestRun_NoWorkspaceSwitchIfNotNecessary(t *testing.T) {
Name: "repo",
},
}
s := NewWorkspaceStepRunnerDelegate(terraform, tfVersion, &NullRunner{})
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfVersion, "workspace")).ThenReturn("workspace\n", nil)
s := NewWorkspaceStepRunnerDelegate(terraform, tfDistribution, tfVersion, &NullRunner{})
When(terraform.RunCommandWithVersion(ctx, "/path", []string{"workspace", "show"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")).ThenReturn("workspace\n", nil)
_, err := s.Run(ctx, []string{"extra", "args"}, "/path", map[string]string(nil))
Ok(t, err)
// Verify that workspace select was never called.
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfVersion, "workspace")
terraform.VerifyWasCalled(Never()).RunCommandWithVersion(ctx, "/path", []string{"workspace", "select", "workspace"}, map[string]string(nil), tfDistribution, tfVersion, "workspace")
}

View File

@@ -18,6 +18,14 @@ type Distribution interface {
ResolveConstraint(context.Context, string) (*version.Version, error)
}
func NewDistribution(distribution string) Distribution {
tfDistribution := NewDistributionTerraform()
if distribution == "opentofu" {
tfDistribution = NewDistributionOpenTofu()
}
return tfDistribution
}
type DistributionOpenTofu struct {
downloader Downloader
}

View File

@@ -1,11 +1,12 @@
// Code generated by pegomock. DO NOT EDIT.
// Source: github.com/runatlantis/atlantis/server/core/terraform (interfaces: Client)
// Source: github.com/runatlantis/atlantis/server/core/terraform/tfclient (interfaces: Client)
package mocks
import (
go_version "github.com/hashicorp/go-version"
pegomock "github.com/petergtz/pegomock/v4"
terraform "github.com/runatlantis/atlantis/server/core/terraform"
command "github.com/runatlantis/atlantis/server/events/command"
logging "github.com/runatlantis/atlantis/server/logging"
"reflect"
@@ -42,11 +43,11 @@ func (mock *MockClient) DetectVersion(log logging.SimpleLogging, projectDirector
return _ret0
}
func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) error {
func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) error {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClient().")
}
_params := []pegomock.Param{log, v}
_params := []pegomock.Param{log, d, v}
_result := pegomock.GetGenericMockFrom(mock).Invoke("EnsureVersion", _params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 error
if len(_result) != 0 {
@@ -57,11 +58,11 @@ func (mock *MockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.V
return _ret0
}
func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) (string, error) {
func (mock *MockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) (string, error) {
if mock == nil {
panic("mock must not be nil. Use myMock := NewMockClient().")
}
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}
_result := pegomock.GetGenericMockFrom(mock).Invoke("RunCommandWithVersion", _params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()})
var _ret0 string
var _ret1 error
@@ -148,8 +149,8 @@ func (c *MockClient_DetectVersion_OngoingVerification) GetAllCapturedArguments()
return
}
func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification {
_params := []pegomock.Param{log, v}
func (verifier *VerifierMockClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *go_version.Version) *MockClient_EnsureVersion_OngoingVerification {
_params := []pegomock.Param{log, d, v}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "EnsureVersion", _params, verifier.timeout)
return &MockClient_EnsureVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -159,12 +160,12 @@ type MockClient_EnsureVersion_OngoingVerification struct {
methodInvocations []pegomock.MethodInvocation
}
func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, *go_version.Version) {
log, v := c.GetAllCapturedArguments()
return log[len(log)-1], v[len(v)-1]
func (c *MockClient_EnsureVersion_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, terraform.Distribution, *go_version.Version) {
log, d, v := c.GetAllCapturedArguments()
return log[len(log)-1], d[len(d)-1], v[len(v)-1]
}
func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []*go_version.Version) {
func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []terraform.Distribution, _param2 []*go_version.Version) {
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
@@ -174,17 +175,23 @@ func (c *MockClient_EnsureVersion_OngoingVerification) GetAllCapturedArguments()
}
}
if len(_params) > 1 {
_param1 = make([]*go_version.Version, len(c.methodInvocations))
_param1 = make([]terraform.Distribution, len(c.methodInvocations))
for u, param := range _params[1] {
_param1[u] = param.(*go_version.Version)
_param1[u] = param.(terraform.Distribution)
}
}
if len(_params) > 2 {
_param2 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[2] {
_param2[u] = param.(*go_version.Version)
}
}
}
return
}
func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {
_params := []pegomock.Param{ctx, path, args, envs, v, workspace}
func (verifier *VerifierMockClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *go_version.Version, workspace string) *MockClient_RunCommandWithVersion_OngoingVerification {
_params := []pegomock.Param{ctx, path, args, envs, d, v, workspace}
methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RunCommandWithVersion", _params, verifier.timeout)
return &MockClient_RunCommandWithVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations}
}
@@ -194,12 +201,12 @@ type MockClient_RunCommandWithVersion_OngoingVerification struct {
methodInvocations []pegomock.MethodInvocation
}
func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, *go_version.Version, string) {
ctx, path, args, envs, v, workspace := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], v[len(v)-1], workspace[len(workspace)-1]
func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetCapturedArguments() (command.ProjectContext, string, []string, map[string]string, terraform.Distribution, *go_version.Version, string) {
ctx, path, args, envs, d, v, workspace := c.GetAllCapturedArguments()
return ctx[len(ctx)-1], path[len(path)-1], args[len(args)-1], envs[len(envs)-1], d[len(d)-1], v[len(v)-1], workspace[len(workspace)-1]
}
func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []*go_version.Version, _param5 []string) {
func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArguments() (_param0 []command.ProjectContext, _param1 []string, _param2 [][]string, _param3 []map[string]string, _param4 []terraform.Distribution, _param5 []*go_version.Version, _param6 []string) {
_params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations)
if len(_params) > 0 {
if len(_params) > 0 {
@@ -227,15 +234,21 @@ func (c *MockClient_RunCommandWithVersion_OngoingVerification) GetAllCapturedArg
}
}
if len(_params) > 4 {
_param4 = make([]*go_version.Version, len(c.methodInvocations))
_param4 = make([]terraform.Distribution, len(c.methodInvocations))
for u, param := range _params[4] {
_param4[u] = param.(*go_version.Version)
_param4[u] = param.(terraform.Distribution)
}
}
if len(_params) > 5 {
_param5 = make([]string, len(c.methodInvocations))
_param5 = make([]*go_version.Version, len(c.methodInvocations))
for u, param := range _params[5] {
_param5[u] = param.(string)
_param5[u] = param.(*go_version.Version)
}
}
if len(_params) > 6 {
_param6 = make([]string, len(c.methodInvocations))
for u, param := range _params[6] {
_param6[u] = param.(string)
}
}
}

View File

@@ -13,8 +13,8 @@
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
//
// Package terraform handles the actual running of terraform commands.
package terraform
// Package tfclient handles the actual running of terraform commands.
package tfclient
import (
"context"
@@ -33,8 +33,9 @@ import (
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/ansi"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/terraform/ansi"
"github.com/runatlantis/atlantis/server/jobs"
"github.com/runatlantis/atlantis/server/logging"
)
@@ -47,10 +48,10 @@ type Client interface {
// RunCommandWithVersion executes terraform with args in path. If v is nil,
// it will use the default Terraform version. workspace is the Terraform
// workspace which should be set as an environment variable.
RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, v *version.Version, workspace string) (string, error)
RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, envs map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error)
// EnsureVersion makes sure that terraform version `v` is available to use
EnsureVersion(log logging.SimpleLogging, v *version.Version) error
EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error
// DetectVersion Extracts required_version from Terraform configuration in the specified project directory. Returns nil if unable to determine the version.
DetectVersion(log logging.SimpleLogging, projectDirectory string) *version.Version
@@ -58,7 +59,7 @@ type Client interface {
type DefaultClient struct {
// Distribution handles logic specific to the TF distribution being used by Atlantis
distribution Distribution
distribution terraform.Distribution
// defaultVersion is the default version of terraform to use if another
// version isn't specified.
@@ -102,7 +103,7 @@ var versionRegex = regexp.MustCompile("(?:Terraform|OpenTofu) v(.*?)(\\s.*)?\n")
// NewClientWithDefaultVersion creates a new terraform client and pre-fetches the default version
func NewClientWithDefaultVersion(
log logging.SimpleLogging,
distribution Distribution,
distribution terraform.Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -189,7 +190,7 @@ func NewClientWithDefaultVersion(
func NewTestClient(
log logging.SimpleLogging,
distribution Distribution,
distribution terraform.Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -227,7 +228,7 @@ func NewTestClient(
// Will asynchronously download the required version if it doesn't exist already.
func NewClient(
log logging.SimpleLogging,
distribution Distribution,
distribution terraform.Distribution,
binDir string,
cacheDir string,
tfeToken string,
@@ -256,6 +257,10 @@ func NewClient(
)
}
func (c *DefaultClient) DefaultDistribution() terraform.Distribution {
return c.distribution
}
// Version returns the default version of Terraform we use if no other version
// is defined.
func (c *DefaultClient) DefaultVersion() *version.Version {
@@ -326,14 +331,14 @@ func (c *DefaultClient) DetectVersion(log logging.SimpleLogging, projectDirector
}
// See Client.EnsureVersion.
func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Version) error {
func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, d terraform.Distribution, v *version.Version) error {
if v == nil {
v = c.defaultVersion
}
var err error
c.versionsLock.Lock()
_, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
_, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
c.versionsLock.Unlock()
if err != nil {
return err
@@ -343,9 +348,9 @@ func (c *DefaultClient) EnsureVersion(log logging.SimpleLogging, v *version.Vers
}
// See Client.RunCommandWithVersion.
func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (string, error) {
func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (string, error) {
if isAsyncEligibleCommand(args[0]) {
_, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, v, workspace)
_, outCh := c.RunCommandAsync(ctx, path, args, customEnvVars, d, v, workspace)
var lines []string
var err error
@@ -362,7 +367,7 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s
output = ansi.Strip(output)
return fmt.Sprintf("%s\n", output), err
}
tfCmd, cmd, err := c.prepExecCmd(ctx.Log, v, workspace, path, args)
tfCmd, cmd, err := c.prepExecCmd(ctx.Log, d, v, workspace, path, args)
if err != nil {
return "", err
}
@@ -388,8 +393,8 @@ func (c *DefaultClient) RunCommandWithVersion(ctx command.ProjectContext, path s
// prepExecCmd builds a ready to execute command based on the version of terraform
// v, and args. It returns a printable representation of the command that will
// be run and the actual command.
func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) {
tfCmd, envVars, err := c.prepCmd(log, v, workspace, path, args)
func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, *exec.Cmd, error) {
tfCmd, envVars, err := c.prepCmd(log, d, v, workspace, path, args)
if err != nil {
return "", nil, err
}
@@ -401,7 +406,8 @@ func (c *DefaultClient) prepExecCmd(log logging.SimpleLogging, v *version.Versio
// prepCmd prepares a shell command (to be interpreted with `sh -c <cmd>`) and set of environment
// variables for running terraform.
func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, workspace string, path string, args []string) (string, []string, error) {
func (c *DefaultClient) prepCmd(log logging.SimpleLogging, d terraform.Distribution, v *version.Version, workspace string, path string, args []string) (string, []string, error) {
if v == nil {
v = c.defaultVersion
}
@@ -413,7 +419,7 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w
} else {
var err error
c.versionsLock.Lock()
binPath, err = ensureVersion(log, c.distribution, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
binPath, err = ensureVersion(log, d, c.versions, v, c.binDir, c.downloadBaseURL, c.downloadAllowed)
c.versionsLock.Unlock()
if err != nil {
return "", nil, err
@@ -446,8 +452,8 @@ func (c *DefaultClient) prepCmd(log logging.SimpleLogging, v *version.Version, w
// Callers can use the input channel to pass stdin input to the command.
// If any error is passed on the out channel, there will be no
// further output (so callers are free to exit).
func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, v *version.Version, workspace string) (chan<- string, <-chan models.Line) {
cmd, envVars, err := c.prepCmd(ctx.Log, v, workspace, path, args)
func (c *DefaultClient) RunCommandAsync(ctx command.ProjectContext, path string, args []string, customEnvVars map[string]string, d terraform.Distribution, v *version.Version, workspace string) (chan<- string, <-chan models.Line) {
cmd, envVars, err := c.prepCmd(ctx.Log, d, v, workspace, path, args)
if err != nil {
// The signature of `RunCommandAsync` doesn't provide for returning an immediate error, only one
// once reading the output. Since we won't be spawning a process, simulate that by sending the
@@ -486,7 +492,7 @@ func MustConstraint(v string) version.Constraints {
// It will download this version if we don't have it.
func ensureVersion(
log logging.SimpleLogging,
dist Distribution,
dist terraform.Distribution,
versions map[string]string,
v *version.Version,
binDir string,

View File

@@ -1,4 +1,4 @@
package terraform
package tfclient
import (
"fmt"
@@ -10,6 +10,8 @@ import (
version "github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
runtimemodels "github.com/runatlantis/atlantis/server/core/runtime/models"
"github.com/runatlantis/atlantis/server/core/terraform"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
@@ -120,7 +122,9 @@ func TestDefaultClient_RunCommandWithVersion_EnvVars(t *testing.T) {
"DIR=$DIR",
}
customEnvVars := map[string]string{}
out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
out, err := client.RunCommandWithVersion(ctx, tmp, args, customEnvVars, distribution, nil, "workspace")
Ok(t, err)
exp := fmt.Sprintf("TF_IN_AUTOMATION=true TF_PLUGIN_CACHE_DIR=%s WORKSPACE=workspace ATLANTIS_TERRAFORM_VERSION=0.11.11 DIR=%s\n", tmp, tmp)
Equals(t, exp, out)
@@ -163,7 +167,9 @@ func TestDefaultClient_RunCommandWithVersion_Error(t *testing.T) {
"exit",
"1",
}
out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
out, err := client.RunCommandWithVersion(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace")
ErrEquals(t, fmt.Sprintf(`running 'echo dying && exit 1' in '%s': exit status 1`, tmp), err)
// Test that we still get our output.
Equals(t, "dying\n", out)
@@ -209,7 +215,9 @@ func TestDefaultClient_RunCommandAsync_Success(t *testing.T) {
"ATLANTIS_TERRAFORM_VERSION=$ATLANTIS_TERRAFORM_VERSION",
"DIR=$DIR",
}
_, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, outCh := client.RunCommandAsync(ctx, tmp, args, map[string]string{}, distribution, nil, "workspace")
out, err := waitCh(outCh)
Ok(t, err)
@@ -261,7 +269,9 @@ func TestDefaultClient_RunCommandAsync_BigOutput(t *testing.T) {
_, err = f.WriteString(s)
Ok(t, err)
}
_, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, outCh := client.RunCommandAsync(ctx, tmp, []string{filename}, map[string]string{}, distribution, nil, "workspace")
out, err := waitCh(outCh)
Ok(t, err)
@@ -301,7 +311,9 @@ func TestDefaultClient_RunCommandAsync_StderrOutput(t *testing.T) {
overrideTF: "echo",
projectCmdOutputHandler: projectCmdOutputHandler,
}
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"stderr", ">&2"}, map[string]string{}, distribution, nil, "workspace")
out, err := waitCh(outCh)
Ok(t, err)
@@ -341,7 +353,9 @@ func TestDefaultClient_RunCommandAsync_ExitOne(t *testing.T) {
overrideTF: "echo",
projectCmdOutputHandler: projectCmdOutputHandler,
}
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, outCh := client.RunCommandAsync(ctx, tmp, []string{"dying", "&&", "exit", "1"}, map[string]string{}, distribution, nil, "workspace")
out, err := waitCh(outCh)
ErrEquals(t, fmt.Sprintf(`running 'sh -c' 'echo dying && exit 1' in '%s': exit status 1`, tmp), err)
@@ -383,7 +397,9 @@ func TestDefaultClient_RunCommandAsync_Input(t *testing.T) {
projectCmdOutputHandler: projectCmdOutputHandler,
}
inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, nil, "workspace")
mockDownloader := terraform_mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
inCh, outCh := client.RunCommandAsync(ctx, tmp, []string{"a", "&&", "echo", "$a"}, map[string]string{}, distribution, nil, "workspace")
inCh <- "echo me\n"
out, err := waitCh(outCh)

View File

@@ -11,7 +11,7 @@
// limitations under the License.
// Modified hereafter by contributors to runatlantis/atlantis.
package terraform_test
package tfclient_test
import (
"context"
@@ -28,6 +28,7 @@ import (
"github.com/runatlantis/atlantis/cmd"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
"github.com/runatlantis/atlantis/server/events/command"
jobmocks "github.com/runatlantis/atlantis/server/jobs/mocks"
"github.com/runatlantis/atlantis/server/logging"
@@ -42,12 +43,12 @@ func TestMustConstraint_PanicsOnBadConstraint(t *testing.T) {
}
}()
terraform.MustConstraint("invalid constraint")
tfclient.MustConstraint("invalid constraint")
}
func TestMustConstraint(t *testing.T) {
t.Log("MustConstraint should return the constrain")
c := terraform.MustConstraint(">0.1")
c := tfclient.MustConstraint(">0.1")
expectedConstraint, err := version.NewConstraint(">0.1")
Ok(t, err)
Equals(t, expectedConstraint.String(), c.String())
@@ -80,13 +81,13 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, nil, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{"test": "123"}, distribution, nil, "")
Ok(t, err)
Equals(t, fakeBinOut+"\n", output)
}
@@ -117,13 +118,13 @@ is 0.11.13. You can update by downloading from developer.hashicorp.com/terraform
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "")
Ok(t, err)
Equals(t, fakeBinOut+"\n", output)
}
@@ -141,7 +142,7 @@ func TestNewClient_NoTF(t *testing.T) {
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
_, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
ErrEquals(t, "terraform not found in $PATH. Set --default-tf-version or download terraform from https://developer.hashicorp.com/terraform/downloads", err)
}
@@ -167,13 +168,13 @@ func TestNewClient_DefaultTFFlagInPath(t *testing.T) {
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, false, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "")
Ok(t, err)
Equals(t, fakeBinOut+"\n", output)
}
@@ -198,13 +199,13 @@ func TestNewClient_DefaultTFFlagInBinDir(t *testing.T) {
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logging.NewNoopLogger(t), distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "")
Ok(t, err)
Equals(t, fakeBinOut+"\n", output)
}
@@ -232,7 +233,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
return []ReturnValue{binPath, err}
})
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Ok(t, err)
@@ -243,7 +244,7 @@ func TestNewClient_DefaultTFFlagDownload(t *testing.T) {
// Reset PATH so that it has sh.
Ok(t, os.Setenv("PATH", orig))
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, nil, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, nil, "")
Ok(t, err)
Equals(t, "\nTerraform v0.11.10\n\n", output)
}
@@ -255,7 +256,7 @@ func TestNewClient_BadVersion(t *testing.T) {
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
_, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
_, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "malformed", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
ErrEquals(t, "Malformed version: malformed", err)
}
@@ -283,11 +284,11 @@ func TestRunCommandWithVersion_DLsTF(t *testing.T) {
return []ReturnValue{binPath, err}
})
c, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, v, "")
output, err := c.RunCommandWithVersion(ctx, tmp, []string{"terraform", "init"}, map[string]string{}, distribution, v, "")
Assert(t, err == nil, "err: %s: %s", err, output)
Equals(t, "\nTerraform v99.99.99\n\n", output)
@@ -304,7 +305,7 @@ func TestEnsureVersion_downloaded(t *testing.T) {
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
downloadsAllowed := true
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -318,7 +319,7 @@ func TestEnsureVersion_downloaded(t *testing.T) {
return []ReturnValue{binPath, err}
})
err = c.EnsureVersion(logger, v)
err = c.EnsureVersion(logger, distribution, v)
Ok(t, err)
@@ -337,7 +338,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) {
downloadsAllowed := true
customURL := "http://releases.example.com"
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler)
c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, customURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -351,7 +352,7 @@ func TestEnsureVersion_downloaded_customURL(t *testing.T) {
return []ReturnValue{binPath, err}
})
err = c.EnsureVersion(logger, v)
err = c.EnsureVersion(logger, distribution, v)
Ok(t, err)
@@ -369,7 +370,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) {
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
downloadsAllowed := false
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, downloadsAllowed, true, projectCmdOutputHandler)
Ok(t, err)
Equals(t, "0.11.10", c.DefaultVersion().String())
@@ -377,7 +378,7 @@ func TestEnsureVersion_downloaded_downloadingDisabled(t *testing.T) {
v, err := version.NewVersion("99.99.99")
Ok(t, err)
err = c.EnsureVersion(logger, v)
err = c.EnsureVersion(logger, distribution, v)
ErrContains(t, "could not find terraform version", err)
ErrContains(t, "downloads are disabled", err)
mockDownloader.VerifyWasCalled(Never())
@@ -501,7 +502,7 @@ terraform {
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewTestClient(
c, err := tfclient.NewTestClient(
logger,
distribution,
binDir,
@@ -548,7 +549,7 @@ func TestExtractExactRegex(t *testing.T) {
mockDownloader := mocks.NewMockDownloader()
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
c, err := terraform.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
c, err := tfclient.NewTestClient(logger, distribution, binDir, cacheDir, "", "", "0.11.10", cmd.DefaultTFVersionFlag, cmd.DefaultTFDownloadURL, true, true, projectCmdOutputHandler)
Ok(t, err)
tests := []struct {

View File

@@ -93,6 +93,10 @@ type ProjectContext struct {
// Steps are the sequence of commands we need to run for this project and this
// stage.
Steps []valid.Step
// TerraformDistribution is the distribution of terraform we should use when
// executing commands for this project. This can be set to nil in which case
// we will use the default Atlantis terraform distribution.
TerraformDistribution *string
// TerraformVersion is the version of terraform we should use when executing
// commands for this project. This can be set to nil in which case we will
// use the default Atlantis terraform version.

View File

@@ -7,12 +7,13 @@ import (
)
type ProjectScopeTags struct {
BaseRepo string
PrNumber string
Project string
ProjectPath string
TerraformVersion string
Workspace string
BaseRepo string
PrNumber string
Project string
ProjectPath string
TerraformDistribution string
TerraformVersion string
Workspace string
}
func (s ProjectScopeTags) Loadtags() map[string]string {

View File

@@ -4,12 +4,11 @@
package events
import (
"reflect"
"time"
pegomock "github.com/petergtz/pegomock/v4"
models "github.com/runatlantis/atlantis/server/events/models"
logging "github.com/runatlantis/atlantis/server/logging"
"reflect"
"time"
)
type MockWorkingDir struct {

View File

@@ -11,7 +11,7 @@ import (
tally "github.com/uber-go/tally/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
"github.com/runatlantis/atlantis/server/logging"
"github.com/runatlantis/atlantis/server/metrics"
@@ -59,7 +59,7 @@ func NewInstrumentedProjectCommandBuilder(
IncludeGitUntrackedFiles bool,
AutoDiscoverMode string,
scope tally.Scope,
terraformClient terraform.Client,
terraformClient tfclient.Client,
) *InstrumentedProjectCommandBuilder {
scope = scope.SubScope("builder")
@@ -119,7 +119,7 @@ func NewProjectCommandBuilder(
IncludeGitUntrackedFiles bool,
AutoDiscoverMode string,
scope tally.Scope,
terraformClient terraform.Client,
terraformClient tfclient.Client,
) *DefaultProjectCommandBuilder {
return &DefaultProjectCommandBuilder{
ParserValidator: parserValidator,
@@ -249,7 +249,7 @@ type DefaultProjectCommandBuilder struct {
// User config option: Controls auto-discovery of projects in a repository.
AutoDiscoverMode string
// Handles the actual running of Terraform commands.
TerraformExecutor terraform.Client
TerraformExecutor tfclient.Client
}
// See ProjectCommandBuilder.BuildAutoplanCommands.

View File

@@ -9,7 +9,7 @@ import (
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
vcsmocks "github.com/runatlantis/atlantis/server/events/vcs/mocks"
@@ -648,7 +648,7 @@ projects:
Ok(t, os.WriteFile(filepath.Join(tmp, "atlantis.yaml"), []byte(c.repoCfg), 0600))
}
terraformClient := mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := NewProjectCommandBuilder(
false,
@@ -865,7 +865,7 @@ projects:
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
terraformClient := mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := NewProjectCommandBuilder(
false,
@@ -1112,7 +1112,7 @@ workflows:
}
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
terraformClient := mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := NewProjectCommandBuilder(
true,
@@ -1264,7 +1264,7 @@ projects:
}
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
terraformClient := mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := NewProjectCommandBuilder(
false,
@@ -1406,7 +1406,7 @@ projects:
}
statsScope, _, _ := metrics.NewLoggingScope(logging.NewNoopLogger(t), "atlantis")
terraformClient := mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := NewProjectCommandBuilder(
false,

View File

@@ -9,7 +9,7 @@ import (
"github.com/hashicorp/go-version"
. "github.com/petergtz/pegomock/v4"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/core/config"
"github.com/runatlantis/atlantis/server/core/config/valid"
@@ -233,7 +233,7 @@ terraform {
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
userConfig := defaultUserConfig
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
for _, c := range cases {
t.Run(c.Description, func(t *testing.T) {
@@ -616,7 +616,7 @@ projects:
AllowAllRepoSettings: true,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -804,7 +804,7 @@ projects:
AllowAllRepoSettings: true,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1133,7 +1133,7 @@ projects:
AllowAllRepoSettings: true,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1231,7 +1231,7 @@ func TestDefaultProjectCommandBuilder_BuildMultiApply(t *testing.T) {
globalCfgArgs := valid.GlobalCfgArgs{}
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1317,7 +1317,7 @@ projects:
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
userConfig := defaultUserConfig
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1405,7 +1405,7 @@ func TestDefaultProjectCommandBuilder_EscapeArgs(t *testing.T) {
AllowAllRepoSettings: true,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1558,7 +1558,7 @@ projects:
AllowAllRepoSettings: true,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
When(terraformClient.DetectVersion(Any[logging.SimpleLogging](), Any[string]())).Then(func(params []Param) ReturnValues {
projectName := filepath.Base(params[1].(string))
testVersion := testCase.Exp[projectName]
@@ -1677,7 +1677,7 @@ projects:
AllowAllRepoSettings: true,
}
scope, _, _ := metrics.NewLoggingScope(logger, "atlantis")
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1746,7 +1746,7 @@ func TestDefaultProjectCommandBuilder_WithPolicyCheckEnabled_BuildAutoplanComman
}
globalCfg := valid.NewGlobalCfgFromArgs(globalCfgArgs)
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
true,
@@ -1834,7 +1834,7 @@ func TestDefaultProjectCommandBuilder_BuildVersionCommand(t *testing.T) {
globalCfgArgs := valid.GlobalCfgArgs{
AllowAllRepoSettings: false,
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false,
@@ -1964,7 +1964,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_Single_With_RestrictFile
Ok(t, err)
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false, // policyChecksSupported
@@ -2075,7 +2075,7 @@ func TestDefaultProjectCommandBuilder_BuildPlanCommands_with_IncludeGitUntracked
Ok(t, err)
}
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
builder := events.NewProjectCommandBuilder(
false, // policyChecksSupported

View File

@@ -5,7 +5,7 @@ import (
"github.com/google/uuid"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/terraform"
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/models"
tally "github.com/uber-go/tally/v4"
@@ -38,7 +38,7 @@ type ProjectCommandContextBuilder interface {
prjCfg valid.MergedProjectCfg,
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, terraformClient terraform.Client,
automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool, terraformClient tfclient.Client,
) []command.ProjectContext
}
@@ -59,7 +59,7 @@ func (cb *CommandScopedStatsProjectCommandContextBuilder) BuildProjectContext(
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,
terraformClient terraform.Client,
terraformClient tfclient.Client,
) (projectCmds []command.ProjectContext) {
cb.ProjectCounter.Inc(1)
@@ -93,7 +93,7 @@ func (cb *DefaultProjectCommandContextBuilder) BuildProjectContext(
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,
terraformClient terraform.Client,
terraformClient tfclient.Client,
) (projectCmds []command.ProjectContext) {
ctx.Log.Debug("Building project command context for %s", cmdName)
@@ -166,7 +166,7 @@ func (cb *PolicyCheckProjectCommandContextBuilder) BuildProjectContext(
commentFlags []string,
repoDir string,
automerge, parallelApply, parallelPlan, verbose, abortOnExecutionOrderFail bool,
terraformClient terraform.Client,
terraformClient tfclient.Client,
) (projectCmds []command.ProjectContext) {
if prjCfg.PolicyCheck {
ctx.Log.Debug("PolicyChecks are enabled")
@@ -297,6 +297,7 @@ func newProjectCommandContext(ctx *command.Context,
RePlanCmd: planCmd,
RepoRelDir: projCfg.RepoRelDir,
RepoConfigVersion: projCfg.RepoCfgVersion,
TerraformDistribution: projCfg.TerraformDistribution,
TerraformVersion: projCfg.TerraformVersion,
User: ctx.User,
Verbose: verbose,

View File

@@ -5,7 +5,7 @@ import (
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks"
@@ -47,7 +47,7 @@ func TestProjectCommandContextBuilder_PullStatus(t *testing.T) {
expectedApplyCmt := "Apply Comment"
expectedPlanCmt := "Plan Comment"
terraformClient := terraform_mocks.NewMockClient()
terraformClient := tfclientmocks.NewMockClient()
t.Run("with project name defined", func(t *testing.T) {
When(mockCommentBuilder.BuildPlanComment(projRepoRelDir, projWorkspace, projName, []string{})).ThenReturn(expectedPlanCmt)

View File

@@ -23,7 +23,9 @@ import (
. "github.com/petergtz/pegomock/v4"
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/runtime"
"github.com/runatlantis/atlantis/server/core/terraform"
tmocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
tfclientmocks "github.com/runatlantis/atlantis/server/core/terraform/tfclient/mocks"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/command"
"github.com/runatlantis/atlantis/server/events/mocks"
@@ -542,12 +544,14 @@ func TestDefaultProjectCommandRunner_ApplyRunStepFailure(t *testing.T) {
// not running any Terraform.
func TestDefaultProjectCommandRunner_RunEnvSteps(t *testing.T) {
RegisterMockTestingT(t)
tfClient := tmocks.NewMockClient()
tfClient := tfclientmocks.NewMockClient()
tfDistribution := terraform.NewDistributionTerraformWithDownloader(tmocks.NewMockDownloader())
tfVersion, err := version.NewVersion("0.12.0")
Ok(t, err)
projectCmdOutputHandler := jobmocks.NewMockProjectCommandOutputHandler()
run := runtime.RunStepRunner{
TerraformExecutor: tfClient,
DefaultTFDistribution: tfDistribution,
DefaultTFVersion: tfVersion,
ProjectCmdOutputHandler: projectCmdOutputHandler,
}

View File

@@ -42,6 +42,7 @@ import (
"github.com/runatlantis/atlantis/server/core/config/valid"
"github.com/runatlantis/atlantis/server/core/db"
"github.com/runatlantis/atlantis/server/core/redis"
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
"github.com/runatlantis/atlantis/server/jobs"
"github.com/runatlantis/atlantis/server/metrics"
"github.com/runatlantis/atlantis/server/scheduled"
@@ -127,12 +128,13 @@ type Server struct {
// Config holds config for server that isn't passed in by the user.
type Config struct {
AllowForkPRsFlag string
AtlantisURLFlag string
AtlantisVersion string
DefaultTFVersionFlag string
RepoConfigJSONFlag string
SilenceForkPRErrorsFlag string
AllowForkPRsFlag string
AtlantisURLFlag string
AtlantisVersion string
DefaultTFDistributionFlag string
DefaultTFVersionFlag string
RepoConfigJSONFlag string
SilenceForkPRErrorsFlag string
}
// WebhookConfig is nested within UserConfig. It's used to configure webhooks.
@@ -427,12 +429,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
)
}
distribution := terraform.NewDistributionTerraform()
if userConfig.TFDistribution == "opentofu" {
distribution = terraform.NewDistributionOpenTofu()
}
distribution := terraform.NewDistribution(userConfig.DefaultTFDistribution)
terraformClient, err := terraform.NewClient(
terraformClient, err := tfclient.NewClient(
logger,
distribution,
binDir,
@@ -449,7 +448,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
// are, then we don't error out because we don't have/want terraform
// installed on our CI system where the unit tests run.
if err != nil && flag.Lookup("test.v") == nil {
return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.TFDistribution))
return nil, errors.Wrap(err, fmt.Sprintf("initializing %s", userConfig.DefaultTFDistribution))
}
markdownRenderer := events.NewMarkdownRenderer(
gitlabClient.SupportsCommonMark(),
@@ -586,10 +585,12 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
userConfig.ExecutableName,
allowCommands,
)
defaultTfDistribution := terraformClient.DefaultDistribution()
defaultTfVersion := terraformClient.DefaultVersion()
pendingPlanFinder := &events.DefaultPendingPlanFinder{}
runStepRunner := &runtime.RunStepRunner{
TerraformExecutor: terraformClient,
DefaultTFDistribution: defaultTfDistribution,
DefaultTFVersion: defaultTfVersion,
TerraformBinDir: terraformClient.TerraformBinDir(),
ProjectCmdOutputHandler: projectCmdOutputHandler,
@@ -648,13 +649,14 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
terraformClient,
)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfVersion)
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion)
if err != nil {
return nil, errors.Wrap(err, "initializing show step runner")
}
policyCheckStepRunner, err := runtime.NewPolicyCheckStepRunner(
defaultTfDistribution,
defaultTfVersion,
policy.NewConfTestExecutorWorkflow(logger, binDir, &policy.ConfTestGoGetterVersionDownloader{}),
)
@@ -672,17 +674,19 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
Locker: projectLocker,
LockURLGenerator: router,
InitStepRunner: &runtime.InitStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTfVersion,
TerraformExecutor: terraformClient,
DefaultTFDistribution: defaultTfDistribution,
DefaultTFVersion: defaultTfVersion,
},
PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfVersion, commitStatusUpdater, terraformClient),
PlanStepRunner: runtime.NewPlanStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion, commitStatusUpdater, terraformClient),
ShowStepRunner: showStepRunner,
PolicyCheckStepRunner: policyCheckStepRunner,
ApplyStepRunner: &runtime.ApplyStepRunner{
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTfVersion,
CommitStatusUpdater: commitStatusUpdater,
AsyncTFExec: terraformClient,
TerraformExecutor: terraformClient,
DefaultTFDistribution: defaultTfDistribution,
DefaultTFVersion: defaultTfVersion,
CommitStatusUpdater: commitStatusUpdater,
AsyncTFExec: terraformClient,
},
RunStepRunner: runStepRunner,
EnvStepRunner: &runtime.EnvStepRunner{
@@ -695,8 +699,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
TerraformExecutor: terraformClient,
DefaultTFVersion: defaultTfVersion,
},
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfVersion),
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion),
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTfDistribution, defaultTfVersion),
WorkingDir: workingDir,
Webhooks: webhooksManager,
WorkingDirLocker: workingDirLocker,

View File

@@ -109,7 +109,7 @@ type UserConfig struct {
SSLCertFile string `mapstructure:"ssl-cert-file"`
SSLKeyFile string `mapstructure:"ssl-key-file"`
RestrictFileList bool `mapstructure:"restrict-file-list"`
TFDistribution string `mapstructure:"tf-distribution"`
TFDistribution string `mapstructure:"tf-distribution"` // deprecated in favor of DefaultTFDistribution
TFDownload bool `mapstructure:"tf-download"`
TFDownloadURL string `mapstructure:"tf-download-url"`
TFEHostname string `mapstructure:"tfe-hostname"`
@@ -117,6 +117,7 @@ type UserConfig struct {
TFEToken string `mapstructure:"tfe-token"`
VarFileAllowlist string `mapstructure:"var-file-allowlist"`
VCSStatusName string `mapstructure:"vcs-status-name"`
DefaultTFDistribution string `mapstructure:"default-tf-distribution"`
DefaultTFVersion string `mapstructure:"default-tf-version"`
Webhooks []WebhookConfig `mapstructure:"webhooks" flag:"false"`
WebBasicAuth bool `mapstructure:"web-basic-auth"`