Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add support for running terragrunt with opentofu #785

Merged
merged 2 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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" {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to avoid setting TerragruntTfBinary without a template of 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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This impacts an environment with a template.
Wanted to test that use-case as well. In this case the user explicitly mentioned here wants to use "terraform" instead of "opentofu".

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": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could not set a deault value to "OpenTofu".
Default will only make sense if this was a "Terragrunt" template.
This is also an issue with existing templates that will use "Terrafrom" and not "OpenTofu".

I tried a few options. At the end I had to manually handle this unique use-case.
This makes the PR a little more complex than usual.

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.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a long explanation here. Hopefully it's clear enough.
Basically, if the user hasn't set anything in the schema ignore the response. This is to avoid drifts.

// 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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, user has set nothing. Defaults to opentofu when the template is created.

}
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) {})
})
}
Loading