Skip to content

Commit 6718320

Browse files
committed
Support project-level Terraform distribution selection
Add support for terraform_distribution config value in project config. This config value behaves similarly to terraform_version whereby defaults taken from the server config will be overridden by project-level values. Also refactor to prevent terraform client slightly to prevent cyclical dependencies.
1 parent af0e9dd commit 6718320

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+728
-430
lines changed

Diff for: cmd/server.go

+40-26
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ const (
7272
CheckoutStrategyFlag = "checkout-strategy"
7373
ConfigFlag = "config"
7474
DataDirFlag = "data-dir"
75+
DefaultTFDistributionFlag = "default-tf-distribution"
7576
DefaultTFVersionFlag = "default-tf-version"
7677
DisableApplyAllFlag = "disable-apply-all"
7778
DisableAutoplanFlag = "disable-autoplan"
@@ -141,21 +142,22 @@ const (
141142
SSLCertFileFlag = "ssl-cert-file"
142143
SSLKeyFileFlag = "ssl-key-file"
143144
RestrictFileList = "restrict-file-list"
144-
TFDistributionFlag = "tf-distribution"
145-
TFDownloadFlag = "tf-download"
146-
TFDownloadURLFlag = "tf-download-url"
147-
UseTFPluginCache = "use-tf-plugin-cache"
148-
VarFileAllowlistFlag = "var-file-allowlist"
149-
VCSStatusName = "vcs-status-name"
150-
IgnoreVCSStatusNames = "ignore-vcs-status-names"
151-
TFEHostnameFlag = "tfe-hostname"
152-
TFELocalExecutionModeFlag = "tfe-local-execution-mode"
153-
TFETokenFlag = "tfe-token"
154-
WriteGitCredsFlag = "write-git-creds" // nolint: gosec
155-
WebBasicAuthFlag = "web-basic-auth"
156-
WebUsernameFlag = "web-username"
157-
WebPasswordFlag = "web-password"
158-
WebsocketCheckOrigin = "websocket-check-origin"
145+
// TFDistributionFlag is deprecated for DefaultTFDistributionFlag
146+
TFDistributionFlag = "tf-distribution"
147+
TFDownloadFlag = "tf-download"
148+
TFDownloadURLFlag = "tf-download-url"
149+
UseTFPluginCache = "use-tf-plugin-cache"
150+
VarFileAllowlistFlag = "var-file-allowlist"
151+
VCSStatusName = "vcs-status-name"
152+
IgnoreVCSStatusNames = "ignore-vcs-status-names"
153+
TFEHostnameFlag = "tfe-hostname"
154+
TFELocalExecutionModeFlag = "tfe-local-execution-mode"
155+
TFETokenFlag = "tfe-token"
156+
WriteGitCredsFlag = "write-git-creds" // nolint: gosec
157+
WebBasicAuthFlag = "web-basic-auth"
158+
WebUsernameFlag = "web-username"
159+
WebPasswordFlag = "web-password"
160+
WebsocketCheckOrigin = "websocket-check-origin"
159161

160162
// NOTE: Must manually set these as defaults in the setDefaults function.
161163
DefaultADBasicUser = ""
@@ -421,8 +423,8 @@ var stringFlags = map[string]stringFlag{
421423
description: fmt.Sprintf("File containing x509 private key matching --%s.", SSLCertFileFlag),
422424
},
423425
TFDistributionFlag: {
424-
description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu),
425-
defaultValue: DefaultTFDistribution,
426+
description: "[Deprecated for --default-tf-distribution].",
427+
hidden: true,
426428
},
427429
TFDownloadURLFlag: {
428430
description: "Base URL to download Terraform versions from.",
@@ -437,6 +439,10 @@ var stringFlags = map[string]stringFlag{
437439
" Only set if using TFC/E as a remote backend." +
438440
" Should be specified via the ATLANTIS_TFE_TOKEN environment variable for security.",
439441
},
442+
DefaultTFDistributionFlag: {
443+
description: fmt.Sprintf("Which TF distribution to use. Can be set to %s or %s.", TFDistributionTerraform, TFDistributionOpenTofu),
444+
defaultValue: DefaultTFDistribution,
445+
},
440446
DefaultTFVersionFlag: {
441447
description: "Terraform version to default to (ex. v0.12.0). Will download if not yet on disk." +
442448
" If not set, Atlantis uses the terraform binary in its PATH.",
@@ -840,12 +846,13 @@ func (s *ServerCmd) run() error {
840846

841847
// Config looks good. Start the server.
842848
server, err := s.ServerCreator.NewServer(userConfig, server.Config{
843-
AllowForkPRsFlag: AllowForkPRsFlag,
844-
AtlantisURLFlag: AtlantisURLFlag,
845-
AtlantisVersion: s.AtlantisVersion,
846-
DefaultTFVersionFlag: DefaultTFVersionFlag,
847-
RepoConfigJSONFlag: RepoConfigJSONFlag,
848-
SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag,
849+
AllowForkPRsFlag: AllowForkPRsFlag,
850+
AtlantisURLFlag: AtlantisURLFlag,
851+
AtlantisVersion: s.AtlantisVersion,
852+
DefaultTFDistributionFlag: DefaultTFDistributionFlag,
853+
DefaultTFVersionFlag: DefaultTFVersionFlag,
854+
RepoConfigJSONFlag: RepoConfigJSONFlag,
855+
SilenceForkPRErrorsFlag: SilenceForkPRErrorsFlag,
849856
})
850857

851858
if err != nil {
@@ -921,8 +928,11 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig, v *viper.Viper) {
921928
if c.RedisPort == 0 {
922929
c.RedisPort = DefaultRedisPort
923930
}
924-
if c.TFDistribution == "" {
925-
c.TFDistribution = DefaultTFDistribution
931+
if c.TFDistribution != "" && c.DefaultTFDistribution == "" {
932+
c.DefaultTFDistribution = c.TFDistribution
933+
}
934+
if c.DefaultTFDistribution == "" {
935+
c.DefaultTFDistribution = DefaultTFDistribution
926936
}
927937
if c.TFDownloadURL == "" {
928938
c.TFDownloadURL = DefaultTFDownloadURL
@@ -953,7 +963,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
953963
return fmt.Errorf("invalid log level: must be one of %v", ValidLogLevels)
954964
}
955965

956-
if userConfig.TFDistribution != TFDistributionTerraform && userConfig.TFDistribution != TFDistributionOpenTofu {
966+
if userConfig.DefaultTFDistribution != TFDistributionTerraform && userConfig.DefaultTFDistribution != TFDistributionOpenTofu {
957967
return fmt.Errorf("invalid tf distribution: expected one of %s or %s",
958968
TFDistributionTerraform, TFDistributionOpenTofu)
959969
}
@@ -1172,6 +1182,10 @@ func (s *ServerCmd) deprecationWarnings(userConfig *server.UserConfig) error {
11721182
// }
11731183
//
11741184

1185+
if userConfig.TFDistribution != "" {
1186+
deprecatedFlags = append(deprecatedFlags, TFDistributionFlag)
1187+
}
1188+
11751189
if len(deprecatedFlags) > 0 {
11761190
warning := "WARNING: "
11771191
if len(deprecatedFlags) == 1 {

Diff for: cmd/server_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ var testFlags = map[string]interface{}{
7373
CheckoutStrategyFlag: CheckoutStrategyMerge,
7474
CheckoutDepthFlag: 0,
7575
DataDirFlag: "/path",
76+
DefaultTFDistributionFlag: "terraform",
7677
DefaultTFVersionFlag: "v0.11.0",
7778
DisableApplyAllFlag: true,
7879
DisableMarkdownFoldingFlag: true,

Diff for: server/controllers/events/events_controller_e2e_test.go

+11-6
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
mock_policy "github.com/runatlantis/atlantis/server/core/runtime/policy/mocks"
3030
"github.com/runatlantis/atlantis/server/core/terraform"
3131
terraform_mocks "github.com/runatlantis/atlantis/server/core/terraform/mocks"
32+
"github.com/runatlantis/atlantis/server/core/terraform/tfclient"
3233
"github.com/runatlantis/atlantis/server/events"
3334
"github.com/runatlantis/atlantis/server/events/command"
3435
"github.com/runatlantis/atlantis/server/events/mocks"
@@ -1319,7 +1320,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
13191320
mockDownloader := terraform_mocks.NewMockDownloader()
13201321
distribution := terraform.NewDistributionTerraformWithDownloader(mockDownloader)
13211322

1322-
terraformClient, err := terraform.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
1323+
terraformClient, err := tfclient.NewClient(logger, distribution, binDir, cacheDir, "", "", "", "default-tf-version", "https://releases.hashicorp.com", true, false, projectCmdOutputHandler)
13231324
Ok(t, err)
13241325
boltdb, err := db.New(dataDir)
13251326
Ok(t, err)
@@ -1346,6 +1347,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
13461347
}
13471348
}
13481349

1350+
defaultTFDistribution := terraformClient.DefaultDistribution()
13491351
defaultTFVersion := terraformClient.DefaultVersion()
13501352
locker := events.NewDefaultWorkingDirLocker()
13511353
parser := &config.ParserValidator{}
@@ -1429,7 +1431,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
14291431
terraformClient,
14301432
)
14311433

1432-
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFVersion)
1434+
showStepRunner, err := runtime.NewShowStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion)
14331435

14341436
Ok(t, err)
14351437

@@ -1440,6 +1442,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
14401442
conftextExec.VersionCache = &LocalConftestCache{}
14411443

14421444
policyCheckRunner, err := runtime.NewPolicyCheckStepRunner(
1445+
defaultTFDistribution,
14431446
defaultTFVersion,
14441447
conftextExec,
14451448
)
@@ -1451,11 +1454,13 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
14511454
Locker: projectLocker,
14521455
LockURLGenerator: &mockLockURLGenerator{},
14531456
InitStepRunner: &runtime.InitStepRunner{
1454-
TerraformExecutor: terraformClient,
1455-
DefaultTFVersion: defaultTFVersion,
1457+
TerraformExecutor: terraformClient,
1458+
DefaultTFDistribution: defaultTFDistribution,
1459+
DefaultTFVersion: defaultTFVersion,
14561460
},
14571461
PlanStepRunner: runtime.NewPlanStepRunner(
14581462
terraformClient,
1463+
defaultTFDistribution,
14591464
defaultTFVersion,
14601465
statusUpdater,
14611466
asyncTfExec,
@@ -1465,8 +1470,8 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
14651470
ApplyStepRunner: &runtime.ApplyStepRunner{
14661471
TerraformExecutor: terraformClient,
14671472
},
1468-
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFVersion),
1469-
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFVersion),
1473+
ImportStepRunner: runtime.NewImportStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
1474+
StateRmStepRunner: runtime.NewStateRmStepRunner(terraformClient, defaultTFDistribution, defaultTFVersion),
14701475
RunStepRunner: &runtime.RunStepRunner{
14711476
TerraformExecutor: terraformClient,
14721477
DefaultTFVersion: defaultTFVersion,

Diff for: server/core/config/parser_validator_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,31 @@ workflows:
610610
},
611611
},
612612
},
613+
{
614+
description: "project field with terraform_distribution set to opentofu",
615+
input: `
616+
version: 3
617+
projects:
618+
- dir: .
619+
workspace: myworkspace
620+
terraform_distribution: opentofu
621+
`,
622+
exp: valid.RepoCfg{
623+
Version: 3,
624+
Projects: []valid.Project{
625+
{
626+
Dir: ".",
627+
Workspace: "myworkspace",
628+
TerraformDistribution: String("opentofu"),
629+
Autoplan: valid.Autoplan{
630+
WhenModified: raw.DefaultAutoPlanWhenModified,
631+
Enabled: true,
632+
},
633+
},
634+
},
635+
Workflows: make(map[string]valid.Workflow),
636+
},
637+
},
613638
{
614639
description: "project dir with ..",
615640
input: `

Diff for: server/core/config/raw/project.go

+13
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type Project struct {
2626
Dir *string `yaml:"dir,omitempty"`
2727
Workspace *string `yaml:"workspace,omitempty"`
2828
Workflow *string `yaml:"workflow,omitempty"`
29+
TerraformDistribution *string `yaml:"terraform_distribution,omitempty"`
2930
TerraformVersion *string `yaml:"terraform_version,omitempty"`
3031
Autoplan *Autoplan `yaml:"autoplan,omitempty"`
3132
PlanRequirements []string `yaml:"plan_requirements,omitempty"`
@@ -86,6 +87,7 @@ func (p Project) Validate() error {
8687
validation.Field(&p.PlanRequirements, validation.By(validPlanReq)),
8788
validation.Field(&p.ApplyRequirements, validation.By(validApplyReq)),
8889
validation.Field(&p.ImportRequirements, validation.By(validImportReq)),
90+
validation.Field(&p.TerraformDistribution, validation.By(validDistribution)),
8991
validation.Field(&p.TerraformVersion, validation.By(VersionValidator)),
9092
validation.Field(&p.DependsOn, validation.By(DependsOn)),
9193
validation.Field(&p.Name, validation.By(validName)),
@@ -118,6 +120,9 @@ func (p Project) ToValid() valid.Project {
118120
if p.TerraformVersion != nil {
119121
v.TerraformVersion, _ = version.NewVersion(*p.TerraformVersion)
120122
}
123+
if p.TerraformDistribution != nil {
124+
v.TerraformDistribution = p.TerraformDistribution
125+
}
121126
if p.Autoplan == nil {
122127
v.Autoplan = DefaultAutoPlan()
123128
} else {
@@ -202,3 +207,11 @@ func validImportReq(value interface{}) error {
202207
}
203208
return nil
204209
}
210+
211+
func validDistribution(value interface{}) error {
212+
distribution := value.(*string)
213+
if distribution != nil && *distribution != "terraform" && *distribution != "opentofu" {
214+
return fmt.Errorf("%q is not a valid terraform_distribution, only %q and %q are supported", *distribution, "terraform", "opentofu")
215+
}
216+
return nil
217+
}

Diff for: server/core/config/valid/global_cfg.go

+3
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ type MergedProjectCfg struct {
105105
AutoplanEnabled bool
106106
AutoMergeDisabled bool
107107
AutoMergeMethod string
108+
TerraformDistribution *string
108109
TerraformVersion *version.Version
109110
RepoCfgVersion int
110111
PolicySets PolicySets
@@ -412,6 +413,7 @@ func (g GlobalCfg) MergeProjectCfg(log logging.SimpleLogging, repoID string, pro
412413
DependsOn: proj.DependsOn,
413414
Name: proj.GetName(),
414415
AutoplanEnabled: proj.Autoplan.Enabled,
416+
TerraformDistribution: proj.TerraformDistribution,
415417
TerraformVersion: proj.TerraformVersion,
416418
RepoCfgVersion: rCfg.Version,
417419
PolicySets: g.PolicySets,
@@ -438,6 +440,7 @@ func (g GlobalCfg) DefaultProjCfg(log logging.SimpleLogging, repoID string, repo
438440
Workspace: workspace,
439441
Name: "",
440442
AutoplanEnabled: DefaultAutoPlanEnabled,
443+
TerraformDistribution: nil,
441444
TerraformVersion: nil,
442445
PolicySets: g.PolicySets,
443446
DeleteSourceBranchOnMerge: deleteSourceBranchOnMerge,

Diff for: server/core/config/valid/repo_cfg.go

+1
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ type Project struct {
147147
Workspace string
148148
Name *string
149149
WorkflowName *string
150+
TerraformDistribution *string
150151
TerraformVersion *version.Version
151152
Autoplan Autoplan
152153
PlanRequirements []string

Diff for: server/core/runtime/apply_step_runner.go

+18-7
Original file line numberDiff line numberDiff line change
@@ -10,17 +10,19 @@ import (
1010
"github.com/pkg/errors"
1111

1212
version "github.com/hashicorp/go-version"
13+
"github.com/runatlantis/atlantis/server/core/terraform"
1314
"github.com/runatlantis/atlantis/server/events/command"
1415
"github.com/runatlantis/atlantis/server/events/models"
1516
"github.com/runatlantis/atlantis/server/utils"
1617
)
1718

1819
// ApplyStepRunner runs `terraform apply`.
1920
type ApplyStepRunner struct {
20-
TerraformExecutor TerraformExec
21-
DefaultTFVersion *version.Version
22-
CommitStatusUpdater StatusUpdater
23-
AsyncTFExec AsyncTFExec
21+
TerraformExecutor TerraformExec
22+
DefaultTFDistribution terraform.Distribution
23+
DefaultTFVersion *version.Version
24+
CommitStatusUpdater StatusUpdater
25+
AsyncTFExec AsyncTFExec
2426
}
2527

2628
func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, path string, envs map[string]string) (string, error) {
@@ -39,19 +41,27 @@ func (a *ApplyStepRunner) Run(ctx command.ProjectContext, extraArgs []string, pa
3941

4042
ctx.Log.Info("starting apply")
4143
var out string
44+
tfDistribution := a.DefaultTFDistribution
45+
if ctx.TerraformDistribution != nil {
46+
tfDistribution = terraform.NewDistribution(*ctx.TerraformDistribution)
47+
}
48+
tfVersion := a.DefaultTFVersion
49+
if ctx.TerraformVersion != nil {
50+
tfVersion = ctx.TerraformVersion
51+
}
4252

4353
// TODO: Leverage PlanTypeStepRunnerDelegate here
4454
if IsRemotePlan(contents) {
4555
args := append(append([]string{"apply", "-input=false", "-no-color"}, extraArgs...), ctx.EscapedCommentArgs...)
46-
out, err = a.runRemoteApply(ctx, args, path, planPath, ctx.TerraformVersion, envs)
56+
out, err = a.runRemoteApply(ctx, args, path, planPath, tfDistribution, tfVersion, envs)
4757
if err == nil {
4858
out = a.cleanRemoteApplyOutput(out)
4959
}
5060
} else {
5161
// NOTE: we need to quote the plan path because Bitbucket Server can
5262
// have spaces in its repo owner names which is part of the path.
5363
args := append(append(append([]string{"apply", "-input=false"}, extraArgs...), ctx.EscapedCommentArgs...), fmt.Sprintf("%q", planPath))
54-
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, ctx.TerraformVersion, ctx.Workspace)
64+
out, err = a.TerraformExecutor.RunCommandWithVersion(ctx, path, args, envs, tfDistribution, tfVersion, ctx.Workspace)
5565
}
5666

5767
// If the apply was successful, delete the plan.
@@ -115,6 +125,7 @@ func (a *ApplyStepRunner) runRemoteApply(
115125
applyArgs []string,
116126
path string,
117127
absPlanPath string,
128+
tfDistribution terraform.Distribution,
118129
tfVersion *version.Version,
119130
envs map[string]string) (string, error) {
120131
// The planfile contents are needed to ensure that the plan didn't change
@@ -133,7 +144,7 @@ func (a *ApplyStepRunner) runRemoteApply(
133144

134145
// Start the async command execution.
135146
ctx.Log.Debug("starting async tf remote operation")
136-
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfVersion, ctx.Workspace)
147+
inCh, outCh := a.AsyncTFExec.RunCommandAsync(ctx, filepath.Clean(path), applyArgs, envs, tfDistribution, tfVersion, ctx.Workspace)
137148
var lines []string
138149
nextLineIsRunURL := false
139150
var runURL string

0 commit comments

Comments
 (0)