Skip to content

Commit

Permalink
Feat: add support for running terragrunt with opentofu (#785)
Browse files Browse the repository at this point in the history
* Feat: add support for running terragrunt with opentofu

* Fix crash
  • Loading branch information
TomerHeber committed Jan 23, 2024
1 parent bb308d9 commit be4500f
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 11 deletions.
20 changes: 13 additions & 7 deletions client/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type Template struct {
Name string `json:"name"`
Description string `json:"description"`
OrganizationId string `json:"organizationId"`
Path string `json:"path,omitempty" tfschema:",omitempty"`
Path string `json:"path" tfschema:",omitempty"`
Revision string `json:"revision"`
ProjectId string `json:"projectId"`
ProjectIds []string `json:"projectIds"`
Expand All @@ -47,21 +47,22 @@ type Template struct {
Type string `json:"type"`
GithubInstallationId int `json:"githubInstallationId" tfschema:",omitempty"`
IsGitlabEnterprise bool `json:"isGitLabEnterprise"`
TokenId string `json:"tokenId,omitempty" tfschema:",omitempty"`
TokenId string `json:"tokenId" tfschema:",omitempty"`
UpdatedAt string `json:"updatedAt"`
TerraformVersion string `json:"terraformVersion" tfschema:",omitempty"`
TerragruntVersion string `json:"terragruntVersion,omitempty" tfschema:",omitempty"`
OpentofuVersion string `json:"opentofuVersion,omitempty" tfschema:",omitempty"`
IsDeleted bool `json:"isDeleted,omitempty"`
TerragruntVersion string `json:"terragruntVersion" tfschema:",omitempty"`
OpentofuVersion string `json:"opentofuVersion" tfschema:",omitempty"`
IsDeleted bool `json:"isDeleted"`
BitbucketClientKey string `json:"bitbucketClientKey" tfschema:",omitempty"`
IsGithubEnterprise bool `json:"isGitHubEnterprise"`
IsBitbucketServer bool `json:"isBitbucketServer"`
FileName string `json:"fileName,omitempty" tfschema:",omitempty"`
FileName string `json:"fileName" tfschema:",omitempty"`
IsTerragruntRunAll bool `json:"isTerragruntRunAll"`
IsAzureDevOps bool `json:"isAzureDevOps" tfschema:"is_azure_devops"`
IsHelmRepository bool `json:"isHelmRepository"`
HelmChartName string `json:"helmChartName,omitempty" tfschema:",omitempty"`
HelmChartName string `json:"helmChartName" tfschema:",omitempty"`
IsGitLab bool `json:"isGitLab" tfschema:"is_gitlab"`
TerragruntTfBinary string `json:"terragruntTfBinary" tfschema:",omitempty"`
}

type TemplateCreatePayload struct {
Expand Down Expand Up @@ -91,6 +92,7 @@ type TemplateCreatePayload struct {
IsAzureDevOps bool `json:"isAzureDevOps" tfschema:"is_azure_devops"`
IsHelmRepository bool `json:"isHelmRepository"`
HelmChartName string `json:"helmChartName,omitempty"`
TerragruntTfBinary string `json:"terragruntTfBinary,omitempty"`
}

type TemplateAssignmentToProjectPayload struct {
Expand Down Expand Up @@ -128,6 +130,10 @@ func (payload *TemplateCreatePayload) Invalidate() error {
return errors.New("must supply opentofu version")
}

if payload.TerragruntTfBinary != "" && payload.Type != "terragrunt" {
return fmt.Errorf("terragrunt_tf_binary should only be used when the template type is 'terragrunt', but type is '%s'", payload.Type)
}

if payload.IsTerragruntRunAll {
if payload.Type != "terragrunt" {
return errors.New(`can't set is_terragrunt_run_all to "true" for non-terragrunt template`)
Expand Down
1 change: 1 addition & 0 deletions env0/resource_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,7 @@ func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta i
if err != nil {
return diag.Errorf("could not get template: %v", err)
}

if err := templateRead("without_template_settings", template, d); err != nil {
return diag.Errorf("schema resource data serialization failed: %v", err)
}
Expand Down
23 changes: 21 additions & 2 deletions env0/resource_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1899,9 +1899,11 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
ErrorRegex: "RetryMeForDestroy.*",
},
},
Type: "terraform",
Type: "terragrunt",
GithubInstallationId: 2,
TerraformVersion: "0.12.25",
TerragruntVersion: "0.26.1",
TerragruntTfBinary: "terraform",
}

environmentCreatePayload := client.EnvironmentCreate{
Expand Down Expand Up @@ -1946,7 +1948,7 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
TokenId: updatedTemplate.TokenId,
Path: updatedTemplate.Path,
Revision: updatedTemplate.Revision,
Type: "terraform",
Type: "terragrunt",
Retry: updatedTemplate.Retry,
TerraformVersion: updatedTemplate.TerraformVersion,
BitbucketClientKey: updatedTemplate.BitbucketClientKey,
Expand All @@ -1956,6 +1958,7 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
TerragruntVersion: updatedTemplate.TerragruntVersion,
IsTerragruntRunAll: updatedTemplate.IsTerragruntRunAll,
OrganizationId: updatedTemplate.OrganizationId,
TerragruntTfBinary: updatedTemplate.TerragruntTfBinary,
}

createPayload := client.EnvironmentCreateWithoutTemplate{
Expand All @@ -1964,6 +1967,16 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
}

createEnvironmentResourceConfig := func(environment client.Environment, template client.Template) string {
terragruntVersion := ""
if template.TerragruntVersion != "" {
terragruntVersion = "terragrunt_version = \"" + template.TerragruntVersion + "\""
}

terragruntTfBinary := ""
if template.TerragruntTfBinary != "" {
terragruntTfBinary = "terragrunt_tf_binary = \"" + template.TerragruntTfBinary + "\""
}

return fmt.Sprintf(`
resource "%s" "%s" {
name = "%s"
Expand All @@ -1984,6 +1997,8 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
retry_on_destroy_only_when_matches_regex = "%s"
description = "%s"
github_installation_id = %d
%s
%s
}
}`,
resourceType, resourceName,
Expand All @@ -2003,6 +2018,8 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
template.Retry.OnDestroy.ErrorRegex,
template.Description,
template.GithubInstallationId,
terragruntVersion,
terragruntTfBinary,
)
}

Expand Down Expand Up @@ -2047,6 +2064,8 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) {
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.terraform_version", updatedTemplate.TerraformVersion),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.type", updatedTemplate.Type),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.path", updatedTemplate.Path),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.terragrunt_version", updatedTemplate.TerragruntVersion),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.terragrunt_tf_binary", updatedTemplate.TerragruntTfBinary),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.revision", updatedTemplate.Revision),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retries_on_deploy", strconv.Itoa(updatedTemplate.Retry.OnDeploy.Times)),
resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retry_on_deploy_only_when_matches_regex", updatedTemplate.Retry.OnDeploy.ErrorRegex),
Expand Down
37 changes: 36 additions & 1 deletion env0/resource_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,12 @@ func getTemplateSchema(prefix string) map[string]*schema.Schema {
ConflictsWith: allVCSAttributesBut("helm_chart_name", "is_helm_repository"),
RequiredWith: requiredWith("helm_chart_name"),
},
"terragrunt_tf_binary": {
Type: schema.TypeString,
Optional: true,
Description: "the binary to use if the template type is 'terragrunt'. Valid values 'opentofu' and 'terraform'. For new templates defaults to 'opentofu'",
ValidateDiagFunc: NewStringInValidator([]string{"opentofu", "terraform"}),
},
}

if prefix == "" {
Expand Down Expand Up @@ -373,12 +379,30 @@ func templateCreatePayloadFromParameters(prefix string, d *schema.ResourceData)
return payload, diag.Errorf("schema resource data serialization failed: %v", err)
}

isNew := d.IsNewResource()

tokenIdKey := "token_id"
isAzureDevOpsKey := "is_azure_devops"
terragruntTfBinaryKey := "terragrunt_tf_binary"
templateTypeKey := "type"

if prefix != "" {
tokenIdKey = prefix + "." + tokenIdKey
isAzureDevOpsKey = prefix + "." + isAzureDevOpsKey
terragruntTfBinaryKey = prefix + "." + terragruntTfBinaryKey
templateTypeKey = prefix + "." + templateTypeKey
}

if templateType, ok := d.GetOk(templateTypeKey); ok {
// If the user has set a value - use it.
if terragruntTfBinary := d.Get(terragruntTfBinaryKey).(string); terragruntTfBinary != "" {
payload.TerragruntTfBinary = terragruntTfBinary
} else {
// No value was set - if it's a new template resource of type 'terragrunt' - default to 'opentofu'
if templateType.(string) == "terragrunt" && isNew {
payload.TerragruntTfBinary = "opentofu"
}
}
}

// IsGitLab is implicitly assumed to be true if tokenId is non-empty. Unless AzureDevOps is explicitly used.
Expand All @@ -401,11 +425,22 @@ func templateCreatePayloadFromParameters(prefix string, d *schema.ResourceData)
// Reads template and writes to the resource data.
func templateRead(prefix string, template client.Template, d *schema.ResourceData) error {
pathPrefix := "path"
terragruntTfBinaryPrefix := "terragrunt_tf_binary"

if prefix != "" {
pathPrefix = prefix + ".0.path"
pathPrefix = prefix + ".0." + pathPrefix
terragruntTfBinaryPrefix = prefix + ".0." + terragruntTfBinaryPrefix
}

path, pathOk := d.GetOk(pathPrefix)
terragruntTfBinary := d.Get(terragruntTfBinaryPrefix).(string)

// If this value isn't set, ignore whatever is returned from the response.
// This helps avoid drifts when defaulting to 'opentofu' for new 'terragrunt' templates, and 'terraform' for existing 'terragrunt' templates.
// 'template.TerragruntTfBinary' field is set to 'omitempty'. Therefore, the state isn't modified if `template.TerragruntTfBinary` is an empty string.
if terragruntTfBinary == "" {
template.TerragruntTfBinary = ""
}

if err := writeResourceDataEx(prefix, &template, d); err != nil {
return fmt.Errorf("schema resource data serialization failed: %v", err)
Expand Down
27 changes: 26 additions & 1 deletion env0/resource_template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,11 @@ func TestUnitTemplateResource(t *testing.T) {
ErrorRegex: "RetryMeForDestroy.*",
},
},
Type: "terraform",
Type: "terragrunt",
IsGitlabEnterprise: true,
TerraformVersion: "0.12.24",
TerragruntVersion: "0.35.1",
TerragruntTfBinary: "opentofu",
}
gleeUpdatedTemplate := client.Template{
Id: gleeTemplate.Id,
Expand Down Expand Up @@ -677,6 +679,10 @@ func TestUnitTemplateResource(t *testing.T) {
OpentofuVersion: templateUseCase.updatedTemplate.OpentofuVersion,
}

if templateUseCase.template.Type == "terragrunt" {
templateCreatePayload.TerragruntTfBinary = templateUseCase.template.TerragruntTfBinary
}

if templateUseCase.template.Type != "terraform" && templateUseCase.template.Type != "terragrunt" {
templateCreatePayload.TerraformVersion = ""
updateTemplateCreateTemplate.TerraformVersion = ""
Expand Down Expand Up @@ -1293,4 +1299,23 @@ func TestUnitTemplateResource(t *testing.T) {

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {})
})

t.Run("terragrunt_tf_binary set with a non terragrunt template type", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
"name": "template0",
"repository": "env0/repo",
"type": "terraform",
"terraform_version": "0.15.1",
"terragrunt_tf_binary": "opentofu",
}),
ExpectError: regexp.MustCompile(`terragrunt_tf_binary should only be used when the template type is 'terragrunt', but type is 'terraform'`),
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {})
})
}

0 comments on commit be4500f

Please sign in to comment.