Skip to content

Commit ac08066

Browse files
authored
Feat: Breaking Change - default terragrunt_tf_binary to 'opentofu' (#980)
* Feat: Breaking Change - default terragrunt_tf_binary to 'opentofu' * add omit empty * added tests * update bad input use-cases
1 parent c43e444 commit ac08066

File tree

5 files changed

+119
-88
lines changed

5 files changed

+119
-88
lines changed

client/template.go

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,22 +127,37 @@ func (payload *TemplateCreatePayload) Invalidate() error {
127127
return errors.New("must not specify organizationId")
128128
}
129129

130-
if payload.Type != TERRAGRUNT && payload.TerragruntVersion != "" {
131-
return errors.New("can't define terragrunt version for non-terragrunt template")
132-
}
130+
if payload.Type == TERRAGRUNT {
131+
if payload.TerragruntVersion == "" {
132+
return errors.New("must supply terragrunt version")
133+
}
134+
135+
// The provider implicitly defaults to "opentofu".
136+
if payload.TerragruntTfBinary == "" {
137+
payload.TerragruntTfBinary = OPENTOFU
138+
}
139+
140+
if payload.TerragruntTfBinary == OPENTOFU && payload.OpentofuVersion == "" {
141+
return errors.New("must supply opentofu version")
142+
}
133143

134-
if payload.Type == TERRAGRUNT && payload.TerragruntVersion == "" {
135-
return errors.New("must supply terragrunt version")
144+
if payload.TerragruntTfBinary == TERRAFORM && payload.TerraformVersion == "" {
145+
return errors.New("must supply terraform version")
146+
}
147+
} else {
148+
if payload.TerragruntVersion != "" {
149+
return errors.New("can't define terragrunt version for non-terragrunt template")
150+
}
151+
152+
if payload.TerragruntTfBinary != "" {
153+
return errors.New("can't define terragrunt_tf_binary for non-terragrunt template")
154+
}
136155
}
137156

138157
if payload.Type == OPENTOFU && payload.OpentofuVersion == "" {
139158
return errors.New("must supply opentofu version")
140159
}
141160

142-
if payload.TerragruntTfBinary != "" && payload.Type != TERRAGRUNT {
143-
return fmt.Errorf("terragrunt_tf_binary should only be used when the template type is 'terragrunt', but type is '%s'", payload.Type)
144-
}
145-
146161
if payload.IsTerragruntRunAll {
147162
if payload.Type != TERRAGRUNT {
148163
return errors.New(`can't set is_terragrunt_run_all to "true" for non-terragrunt template`)

env0/resource_environment.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -674,7 +674,7 @@ func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta i
674674
return diag.Errorf("could not get template: %v", err)
675675
}
676676

677-
if err := templateRead("without_template_settings", template, d, false); err != nil {
677+
if err := templateRead("without_template_settings", template, d); err != nil {
678678
return diag.Errorf("schema resource data serialization failed: %v", err)
679679
}
680680
}
@@ -1443,7 +1443,7 @@ func resourceEnvironmentImport(ctx context.Context, d *schema.ResourceData, meta
14431443
return nil, fmt.Errorf("failed to get template with id %s: %w", templateId, err)
14441444
}
14451445

1446-
if err := templateRead("without_template_settings", template, d, true); err != nil {
1446+
if err := templateRead("without_template_settings", template, d); err != nil {
14471447
return nil, fmt.Errorf("failed to write template to schema: %w", err)
14481448
}
14491449
}

env0/resource_environment_test.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2736,11 +2736,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
27362736
terragruntVersion = "terragrunt_version = \"" + template.TerragruntVersion + "\""
27372737
}
27382738

2739-
terragruntTfBinary := ""
2740-
if template.TerragruntTfBinary != "" {
2741-
terragruntTfBinary = "terragrunt_tf_binary = \"" + template.TerragruntTfBinary + "\""
2742-
}
2743-
27442739
openTofuVersion := ""
27452740
if template.OpentofuVersion != "" {
27462741
openTofuVersion = "opentofu_version = \"" + template.OpentofuVersion + "\""
@@ -2769,7 +2764,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
27692764
github_installation_id = %d
27702765
%s
27712766
%s
2772-
%s
27732767
}
27742768
}`,
27752769
resourceType, resourceName,
@@ -2790,7 +2784,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
27902784
template.Description,
27912785
template.GithubInstallationId,
27922786
terragruntVersion,
2793-
terragruntTfBinary,
27942787
openTofuVersion,
27952788
)
27962789
}
@@ -2837,7 +2830,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
28372830
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.type", updatedTemplate.Type),
28382831
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.path", updatedTemplate.Path),
28392832
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.terragrunt_version", updatedTemplate.TerragruntVersion),
2840-
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.terragrunt_tf_binary", updatedTemplate.TerragruntTfBinary),
28412833
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.revision", updatedTemplate.Revision),
28422834
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retries_on_deploy", strconv.Itoa(updatedTemplate.Retry.OnDeploy.Times)),
28432835
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retry_on_deploy_only_when_matches_regex", updatedTemplate.Retry.OnDeploy.ErrorRegex),

env0/resource_template.go

Lines changed: 10 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ func getTemplateSchema(prefix string) map[string]*schema.Schema {
244244
"terragrunt_tf_binary": {
245245
Type: schema.TypeString,
246246
Optional: true,
247-
Description: "the binary to use if the template type is 'terragrunt'. Valid values 'opentofu' and 'terraform'. For new templates defaults to 'opentofu'",
247+
Description: "the binary to use if the template type is 'terragrunt'. Valid values 'opentofu' and 'terraform'. Defaults to 'opentofu'",
248248
ValidateDiagFunc: NewStringInValidator([]string{client.OPENTOFU, client.TERRAFORM}),
249249
},
250250
"token_name": {
@@ -324,7 +324,7 @@ func resourceTemplateRead(ctx context.Context, d *schema.ResourceData, meta inte
324324
return nil
325325
}
326326

327-
if err := templateRead("", template, d, false); err != nil {
327+
if err := templateRead("", template, d); err != nil {
328328
return diag.Errorf("%v", err)
329329
}
330330

@@ -410,25 +410,6 @@ func templateCreatePayloadFromParameters(prefix string, d *schema.ResourceData)
410410
return payload, diag.Errorf("schema resource data serialization failed: %v", err)
411411
}
412412

413-
isNew := d.IsNewResource()
414-
415-
terragruntTfBinaryKey := "terragrunt_tf_binary"
416-
templateTypeKey := "type"
417-
418-
if prefix != "" {
419-
terragruntTfBinaryKey = prefix + "." + terragruntTfBinaryKey
420-
templateTypeKey = prefix + "." + templateTypeKey
421-
}
422-
423-
if templateType, ok := d.GetOk(templateTypeKey); ok {
424-
// If the user has set a value - use it.
425-
if terragruntTfBinary := d.Get(terragruntTfBinaryKey).(string); terragruntTfBinary != "" {
426-
payload.TerragruntTfBinary = terragruntTfBinary
427-
} else if templateType.(string) == client.TERRAGRUNT && isNew {
428-
payload.TerragruntTfBinary = client.OPENTOFU
429-
}
430-
}
431-
432413
templateCreatePayloadRetryOnHelper(prefix, d, "deploy", &payload.Retry.OnDeploy)
433414
templateCreatePayloadRetryOnHelper(prefix, d, "destroy", &payload.Retry.OnDestroy)
434415

@@ -440,33 +421,24 @@ func templateCreatePayloadFromParameters(prefix string, d *schema.ResourceData)
440421
}
441422

442423
// Reads template and writes to the resource data.
443-
func templateRead(prefix string, template client.Template, d *schema.ResourceData, isImport bool) error {
424+
func templateRead(prefix string, template client.Template, d *schema.ResourceData) error {
444425
pathPrefix := "path"
445426
terragruntTfBinaryPrefix := "terragrunt_tf_binary"
446-
terraformVersionPrefix := "terraform_version"
447427

448428
if prefix != "" {
449-
pathPrefix = prefix + ".0." + pathPrefix
450429
terragruntTfBinaryPrefix = prefix + ".0." + terragruntTfBinaryPrefix
451-
terraformVersionPrefix = prefix + ".0." + terraformVersionPrefix
430+
pathPrefix = prefix + ".0." + pathPrefix
452431
}
453432

454433
path, pathOk := d.GetOk(pathPrefix)
455-
terragruntTfBinary := d.Get(terragruntTfBinaryPrefix).(string)
456-
terraformVersion := d.Get(terraformVersionPrefix).(string)
457-
458-
// If this value isn't set, ignore whatever is returned from the response.
459-
// This helps avoid drifts when defaulting to 'opentofu' for new 'terragrunt' templates, and 'terraform' for existing 'terragrunt' templates.
460-
// 'template.TerragruntTfBinary' field is set to 'omitempty'. Therefore, the state isn't modified if `template.TerragruntTfBinary` is an empty string.
461-
// This is not true for imports - because the shcema is empty irrespective in that case.
462-
if !isImport {
463-
if terragruntTfBinary == "" {
434+
435+
// This is done to avoid drifts in case the backend returns "opentofu", but non is configured in the provider.
436+
// (The provider implicitly defaults to "opentofu").
437+
if template.TerragruntTfBinary == client.OPENTOFU {
438+
terragruntTfBinary, terragruntTfBinaryOk := d.GetOk(terragruntTfBinaryPrefix)
439+
if !terragruntTfBinaryOk || terragruntTfBinary.(string) == "" {
464440
template.TerragruntTfBinary = ""
465441
}
466-
// Same explanation as above.
467-
if terraformVersion == "" {
468-
template.TerraformVersion = ""
469-
}
470442
}
471443

472444
if err := writeResourceDataEx(prefix, &template, d); err != nil {

env0/resource_template_test.go

Lines changed: 83 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func TestUnitTemplateResource(t *testing.T) {
4343
IsGitlabEnterprise: true,
4444
TerraformVersion: "0.12.24",
4545
TerragruntVersion: "0.35.1",
46-
TerragruntTfBinary: "opentofu",
46+
TerragruntTfBinary: "terraform",
4747
}
4848
gleeUpdatedTemplate := client.Template{
4949
Id: gleeTemplate.Id,
@@ -67,6 +67,7 @@ func TestUnitTemplateResource(t *testing.T) {
6767
IsGitlabEnterprise: true,
6868
TerraformVersion: "0.15.1",
6969
IsTerragruntRunAll: true,
70+
TerragruntTfBinary: "terraform",
7071
}
7172
gitlabTemplate := client.Template{
7273
Id: "id0-gitlab",
@@ -108,11 +109,12 @@ func TestUnitTemplateResource(t *testing.T) {
108109
ErrorRegex: "NewForDestroy.*",
109110
},
110111
},
111-
Type: "terragrunt",
112-
TerragruntVersion: "0.26.1",
113-
TokenId: "2",
114-
TerraformVersion: "0.15.1",
115-
TokenName: "token_name2",
112+
Type: "terragrunt",
113+
TerragruntVersion: "0.26.1",
114+
TokenId: "2",
115+
TerraformVersion: "0.15.1",
116+
TokenName: "token_name2",
117+
TerragruntTfBinary: "terraform",
116118
}
117119
githubTemplate := client.Template{
118120
Id: "id0",
@@ -157,6 +159,7 @@ func TestUnitTemplateResource(t *testing.T) {
157159
GithubInstallationId: 2,
158160
TerraformVersion: "0.15.1",
159161
IsTerragruntRunAll: true,
162+
TerragruntTfBinary: "terraform",
160163
}
161164
bitbucketTemplate := client.Template{
162165
Id: "id0",
@@ -200,6 +203,7 @@ func TestUnitTemplateResource(t *testing.T) {
200203
BitbucketClientKey: "clientkey2",
201204
TerragruntVersion: "0.35.1",
202205
TerraformVersion: "0.15.1",
206+
TerragruntTfBinary: "terraform",
203207
}
204208
gheeTemplate := client.Template{
205209
Id: "id0",
@@ -366,11 +370,12 @@ func TestUnitTemplateResource(t *testing.T) {
366370
ErrorRegex: "NewForDestroy.*",
367371
},
368372
},
369-
Type: "terragrunt",
370-
TerragruntVersion: "0.26.1",
371-
TokenId: "2",
372-
TerraformVersion: "0.15.1",
373-
IsAzureDevOps: true,
373+
Type: "terragrunt",
374+
TerragruntVersion: "0.26.1",
375+
TokenId: "2",
376+
TerraformVersion: "0.15.1",
377+
TerragruntTfBinary: "terraform",
378+
IsAzureDevOps: true,
374379
}
375380

376381
helmTemplate := client.Template{
@@ -594,6 +599,10 @@ func TestUnitTemplateResource(t *testing.T) {
594599
templateAsDictionary["ansible_version"] = template.AnsibleVersion
595600
}
596601

602+
if template.TerragruntTfBinary != "" {
603+
templateAsDictionary["terragrunt_tf_binary"] = template.TerragruntTfBinary
604+
}
605+
597606
return resourceConfigCreate(resourceType, resourceName, templateAsDictionary)
598607
}
599608
fullTemplateResourceCheck := func(resourceFullName string, template client.Template) resource.TestCheckFunc {
@@ -729,31 +738,34 @@ func TestUnitTemplateResource(t *testing.T) {
729738
GithubInstallationId: templateUseCase.updatedTemplate.GithubInstallationId,
730739
IsGitlabEnterprise: templateUseCase.updatedTemplate.IsGitlabEnterprise,
731740
IsGitlab: templateUseCase.updatedTemplate.IsGitlab,
732-
733-
TokenId: templateUseCase.updatedTemplate.TokenId,
734-
Path: templateUseCase.updatedTemplate.Path,
735-
Revision: templateUseCase.updatedTemplate.Revision,
736-
Type: templateUseCase.updatedTemplate.Type,
737-
Retry: templateUseCase.updatedTemplate.Retry,
738-
TerraformVersion: templateUseCase.updatedTemplate.TerraformVersion,
739-
BitbucketClientKey: templateUseCase.updatedTemplate.BitbucketClientKey,
740-
IsGithubEnterprise: templateUseCase.updatedTemplate.IsGithubEnterprise,
741-
IsBitbucketServer: templateUseCase.updatedTemplate.IsBitbucketServer,
742-
FileName: templateUseCase.updatedTemplate.FileName,
743-
TerragruntVersion: templateUseCase.updatedTemplate.TerragruntVersion,
744-
IsTerragruntRunAll: templateUseCase.updatedTemplate.IsTerragruntRunAll,
745-
IsAzureDevOps: templateUseCase.updatedTemplate.IsAzureDevOps,
746-
IsHelmRepository: templateUseCase.updatedTemplate.IsHelmRepository,
747-
HelmChartName: templateUseCase.updatedTemplate.HelmChartName,
748-
OpentofuVersion: templateUseCase.updatedTemplate.OpentofuVersion,
749-
TokenName: templateUseCase.updatedTemplate.TokenName,
750-
AnsibleVersion: templateUseCase.updatedTemplate.AnsibleVersion,
741+
TokenId: templateUseCase.updatedTemplate.TokenId,
742+
Path: templateUseCase.updatedTemplate.Path,
743+
Revision: templateUseCase.updatedTemplate.Revision,
744+
Type: templateUseCase.updatedTemplate.Type,
745+
Retry: templateUseCase.updatedTemplate.Retry,
746+
TerraformVersion: templateUseCase.updatedTemplate.TerraformVersion,
747+
BitbucketClientKey: templateUseCase.updatedTemplate.BitbucketClientKey,
748+
IsGithubEnterprise: templateUseCase.updatedTemplate.IsGithubEnterprise,
749+
IsBitbucketServer: templateUseCase.updatedTemplate.IsBitbucketServer,
750+
FileName: templateUseCase.updatedTemplate.FileName,
751+
TerragruntVersion: templateUseCase.updatedTemplate.TerragruntVersion,
752+
IsTerragruntRunAll: templateUseCase.updatedTemplate.IsTerragruntRunAll,
753+
IsAzureDevOps: templateUseCase.updatedTemplate.IsAzureDevOps,
754+
IsHelmRepository: templateUseCase.updatedTemplate.IsHelmRepository,
755+
HelmChartName: templateUseCase.updatedTemplate.HelmChartName,
756+
OpentofuVersion: templateUseCase.updatedTemplate.OpentofuVersion,
757+
TokenName: templateUseCase.updatedTemplate.TokenName,
758+
AnsibleVersion: templateUseCase.updatedTemplate.AnsibleVersion,
751759
}
752760

753761
if templateUseCase.template.Type == "terragrunt" {
754762
templateCreatePayload.TerragruntTfBinary = templateUseCase.template.TerragruntTfBinary
755763
}
756764

765+
if templateUseCase.updatedTemplate.Type == "terragrunt" {
766+
updateTemplateCreateTemplate.TerragruntTfBinary = templateUseCase.updatedTemplate.TerragruntTfBinary
767+
}
768+
757769
if templateUseCase.template.Type != "terraform" && templateUseCase.template.Type != "terragrunt" {
758770
templateCreatePayload.TerraformVersion = ""
759771
updateTemplateCreateTemplate.TerraformVersion = ""
@@ -1366,6 +1378,7 @@ func TestUnitTemplateResource(t *testing.T) {
13661378
"type": "terragrunt",
13671379
"terraform_version": "0.15.1",
13681380
"terragrunt_version": "0.27.50",
1381+
"terragrunt_tf_binary": "terraform",
13691382
"is_terragrunt_run_all": "true",
13701383
}),
13711384
ExpectError: regexp.MustCompile(`can't set is_terragrunt_run_all to 'true' for terragrunt versions lower than 0.28.1`),
@@ -1387,7 +1400,46 @@ func TestUnitTemplateResource(t *testing.T) {
13871400
"terraform_version": "0.15.1",
13881401
"terragrunt_tf_binary": "opentofu",
13891402
}),
1390-
ExpectError: regexp.MustCompile(`terragrunt_tf_binary should only be used when the template type is 'terragrunt', but type is 'terraform'`),
1403+
ExpectError: regexp.MustCompile(`can't define terragrunt_tf_binary for non-terragrunt template`),
1404+
},
1405+
},
1406+
}
1407+
1408+
runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {})
1409+
})
1410+
1411+
t.Run("run with terragrunt without an opentofu version", func(t *testing.T) {
1412+
testCase := resource.TestCase{
1413+
Steps: []resource.TestStep{
1414+
{
1415+
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
1416+
"name": "template0",
1417+
"repository": "env0/repo",
1418+
"type": "terragrunt",
1419+
"terragrunt_version": "0.56.50",
1420+
"is_terragrunt_run_all": "true",
1421+
}),
1422+
ExpectError: regexp.MustCompile("must supply opentofu version"),
1423+
},
1424+
},
1425+
}
1426+
1427+
runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {})
1428+
})
1429+
1430+
t.Run("run terragrunt with terraform binary and no terraform version", func(t *testing.T) {
1431+
testCase := resource.TestCase{
1432+
Steps: []resource.TestStep{
1433+
{
1434+
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
1435+
"name": "template0",
1436+
"repository": "env0/repo",
1437+
"type": "terragrunt",
1438+
"terragrunt_version": "0.56.50",
1439+
"is_terragrunt_run_all": "true",
1440+
"terragrunt_tf_binary": "terraform",
1441+
}),
1442+
ExpectError: regexp.MustCompile("must supply terraform version"),
13911443
},
13921444
},
13931445
}

0 commit comments

Comments
 (0)