diff --git a/client/template.go b/client/template.go index e9ea258f..b2f07573 100644 --- a/client/template.go +++ b/client/template.go @@ -51,6 +51,7 @@ type Template struct { 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"` BitbucketClientKey string `json:"bitbucketClientKey" tfschema:",omitempty"` IsGithubEnterprise bool `json:"isGitHubEnterprise"` @@ -80,6 +81,7 @@ type TemplateCreatePayload struct { OrganizationId string `json:"organizationId"` TerraformVersion string `json:"terraformVersion,omitempty"` TerragruntVersion string `json:"terragruntVersion,omitempty"` + OpentofuVersion string `json:"opentofuVersion,omitempty"` IsGitlabEnterprise bool `json:"isGitLabEnterprise"` BitbucketClientKey string `json:"bitbucketClientKey,omitempty"` IsGithubEnterprise bool `json:"isGitHubEnterprise"` @@ -122,6 +124,9 @@ func (payload *TemplateCreatePayload) Invalidate() error { if payload.Type == "terragrunt" && payload.TerragruntVersion == "" { return errors.New("must supply terragrunt version") } + if payload.Type == "opentofu" && payload.OpentofuVersion == "" { + return errors.New("must supply opentofu version") + } if payload.IsTerragruntRunAll { if payload.Type != "terragrunt" { @@ -163,6 +168,10 @@ func (payload *TemplateCreatePayload) Invalidate() error { payload.TerraformVersion = "" } + if payload.Type != "opentofu" { + payload.OpentofuVersion = "" + } + return nil } diff --git a/docs/data-sources/template.md b/docs/data-sources/template.md index 472b4f07..4d72a8e9 100644 --- a/docs/data-sources/template.md +++ b/docs/data-sources/template.md @@ -46,7 +46,7 @@ data "env0_template" "example" { - `retry_on_destroy_only_when_matches_regex` (String) if specified, will only retry (on destroy) if error matches specified regex - `revision` (String) source code revision (branch / tag) to use - `terraform_version` (String) terraform version to use -- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm) +- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm, opentofu) ### Nested Schema for `ssh_keys` diff --git a/docs/resources/environment.md b/docs/resources/environment.md index aa516780..5d043bbb 100644 --- a/docs/resources/environment.md +++ b/docs/resources/environment.md @@ -168,6 +168,7 @@ Optional: - `is_gitlab_enterprise` (Boolean) true if this template uses gitlab enterprise repository - `is_helm_repository` (Boolean) true if this template integrates with a helm repository - `is_terragrunt_run_all` (Boolean) true if this template should execute run-all commands on multiple modules (check https://terragrunt.gruntwork.io/docs/features/execute-terraform-commands-on-multiple-modules-at-once/#the-run-all-command for additional details). Can only be true with "terragrunt" template type and terragrunt version 0.28.1 and above +- `opentofu_version` (String) the Opentofu version to use (example: 0.36.5) - `path` (String) terraform / terragrunt file folder inside source code - `retries_on_deploy` (Number) number of times to retry when deploying an environment based on this template - `retries_on_destroy` (Number) number of times to retry when destroying an environment based on this template @@ -178,7 +179,7 @@ Optional: - `terraform_version` (String) the Terraform version to use (example: 0.15.1). Setting to `RESOLVE_FROM_TERRAFORM_CODE` defaults to the version of `terraform.required_version` during run-time (resolve from terraform code). Setting to `latest`, the version used will be the most recent one available for Terraform. - `terragrunt_version` (String) the Terragrunt version to use (example: 0.36.5) - `token_id` (String) the git token id to be used -- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm) +- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm, opentofu) Read-Only: diff --git a/docs/resources/template.md b/docs/resources/template.md index 8004a1f3..6e397f89 100644 --- a/docs/resources/template.md +++ b/docs/resources/template.md @@ -75,6 +75,7 @@ resource "env0_template_project_assignment" "assignment" { - `is_gitlab_enterprise` (Boolean) true if this template uses gitlab enterprise repository - `is_helm_repository` (Boolean) true if this template integrates with a helm repository - `is_terragrunt_run_all` (Boolean) true if this template should execute run-all commands on multiple modules (check https://terragrunt.gruntwork.io/docs/features/execute-terraform-commands-on-multiple-modules-at-once/#the-run-all-command for additional details). Can only be true with "terragrunt" template type and terragrunt version 0.28.1 and above +- `opentofu_version` (String) the Opentofu version to use (example: 0.36.5) - `path` (String) terraform / terragrunt file folder inside source code - `retries_on_deploy` (Number) number of times to retry when deploying an environment based on this template - `retries_on_destroy` (Number) number of times to retry when destroying an environment based on this template @@ -85,7 +86,7 @@ resource "env0_template_project_assignment" "assignment" { - `terraform_version` (String) the Terraform version to use (example: 0.15.1). Setting to `RESOLVE_FROM_TERRAFORM_CODE` defaults to the version of `terraform.required_version` during run-time (resolve from terraform code). Setting to `latest`, the version used will be the most recent one available for Terraform. - `terragrunt_version` (String) the Terragrunt version to use (example: 0.36.5) - `token_id` (String) the git token id to be used -- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm) +- `type` (String) template type (allowed values: terraform, terragrunt, pulumi, k8s, workflow, cloudformation, helm, opentofu) ### Read-Only diff --git a/env0/resource_template.go b/env0/resource_template.go index 83da2e3c..33aa2998 100644 --- a/env0/resource_template.go +++ b/env0/resource_template.go @@ -21,6 +21,7 @@ var allowedTemplateTypes = []string{ "workflow", "cloudformation", "helm", + "opentofu", } func getTemplateSchema(prefix string) map[string]*schema.Schema { @@ -172,6 +173,12 @@ func getTemplateSchema(prefix string) map[string]*schema.Schema { ValidateDiagFunc: NewRegexValidator(`^[0-9]\.[0-9]{1,2}\.[0-9]{1,2}$`), Optional: true, }, + "opentofu_version": { + Type: schema.TypeString, + Description: "the Opentofu version to use (example: 0.36.5)", + ValidateDiagFunc: NewRegexValidator(`^(?:[0-9]\.[0-9]{1,2}\.[0-9]{1,2})|1\.6\.0-alpha$`), + Optional: true, + }, "is_gitlab_enterprise": { Type: schema.TypeBool, Description: "true if this template uses gitlab enterprise repository", diff --git a/env0/resource_template_test.go b/env0/resource_template_test.go index b2fa7273..7b02ac49 100644 --- a/env0/resource_template_test.go +++ b/env0/resource_template_test.go @@ -407,6 +407,45 @@ func TestUnitTemplateResource(t *testing.T) { TerraformVersion: "0.12.24", } + opentofuTemplate := client.Template{ + Id: "opentofu", + Name: "template0", + Description: "description0", + Repository: "env0/repo", + Type: "opentofu", + OpentofuVersion: "1.6.0-alpha", + TerraformVersion: "0.15.1", + Retry: client.TemplateRetry{ + OnDeploy: &client.TemplateRetryOn{ + Times: 2, + ErrorRegex: "RetryMeForDeploy.*", + }, + OnDestroy: &client.TemplateRetryOn{ + Times: 1, + ErrorRegex: "RetryMeForDestroy.*", + }, + }, + } + opentofuUpdatedTemplate := client.Template{ + Id: opentofuTemplate.Id, + Name: "new-name", + Description: "new-description", + Repository: "env0/repo-new", + Type: "opentofu", + OpentofuVersion: "1.7.0", + TerraformVersion: "0.15.1", + Retry: client.TemplateRetry{ + OnDeploy: &client.TemplateRetryOn{ + Times: 2, + ErrorRegex: "RetryMeForDeploy.*", + }, + OnDestroy: &client.TemplateRetryOn{ + Times: 1, + ErrorRegex: "RetryMeForDestroy.*", + }, + }, + } + fullTemplateResourceConfig := func(resourceType string, resourceName string, template client.Template) string { templateAsDictionary := map[string]interface{}{ "name": template.Name, @@ -440,6 +479,9 @@ func TestUnitTemplateResource(t *testing.T) { if template.TerraformVersion != "" { templateAsDictionary["terraform_version"] = template.TerraformVersion } + if template.OpentofuVersion != "" { + templateAsDictionary["opentofu_version"] = template.OpentofuVersion + } if template.TokenId != "" { templateAsDictionary["token_id"] = template.TokenId } @@ -516,6 +558,11 @@ func TestUnitTemplateResource(t *testing.T) { terragruntVersionAssertion = resource.TestCheckNoResourceAttr(resourceFullName, "terragrunt_version") } + opentofuVersionAssertion := resource.TestCheckResourceAttr(resourceFullName, "opentofu_version", template.OpentofuVersion) + if template.OpentofuVersion == "" { + opentofuVersionAssertion = resource.TestCheckNoResourceAttr(resourceFullName, "opentofu_version") + } + githubInstallationIdAssertion := resource.TestCheckResourceAttr(resourceFullName, "github_installation_id", strconv.Itoa(template.GithubInstallationId)) if template.GithubInstallationId == 0 { githubInstallationIdAssertion = resource.TestCheckNoResourceAttr(resourceFullName, "github_installation_id") @@ -544,6 +591,7 @@ func TestUnitTemplateResource(t *testing.T) { githubInstallationIdAssertion, helmChartNameAssertion, pathAssertion, + opentofuVersionAssertion, resource.TestCheckResourceAttr(resourceFullName, "terraform_version", template.TerraformVersion), resource.TestCheckResourceAttr(resourceFullName, "is_terragrunt_run_all", strconv.FormatBool(template.IsTerragruntRunAll)), resource.TestCheckResourceAttr(resourceFullName, "is_azure_devops", strconv.FormatBool(template.IsAzureDevOps)), @@ -565,6 +613,7 @@ func TestUnitTemplateResource(t *testing.T) { {"Cloudformation", cloudformationTemplate, cloudformationUpdatedTemplate}, {"Azure DevOps", azureDevOpsTemplate, azureDevOpsUpdatedTemplate}, {"Helm Chart", helmTemplate, helmUpdatedTemplate}, + {"Opentofu", opentofuTemplate, opentofuUpdatedTemplate}, } for _, templateUseCase := range templateUseCases { t.Run("Full "+templateUseCase.vcs+" template (without SSH keys)", func(t *testing.T) { @@ -599,6 +648,7 @@ func TestUnitTemplateResource(t *testing.T) { IsAzureDevOps: templateUseCase.template.IsAzureDevOps, IsHelmRepository: templateUseCase.template.IsHelmRepository, HelmChartName: templateUseCase.template.HelmChartName, + OpentofuVersion: templateUseCase.template.OpentofuVersion, } updateTemplateCreateTemplate := client.TemplateCreatePayload{ @@ -622,8 +672,9 @@ func TestUnitTemplateResource(t *testing.T) { TerragruntVersion: templateUseCase.updatedTemplate.TerragruntVersion, IsTerragruntRunAll: templateUseCase.updatedTemplate.IsTerragruntRunAll, IsAzureDevOps: templateUseCase.updatedTemplate.IsAzureDevOps, - IsHelmRepository: templateUseCase.template.IsHelmRepository, - HelmChartName: templateUseCase.template.HelmChartName, + IsHelmRepository: templateUseCase.updatedTemplate.IsHelmRepository, + HelmChartName: templateUseCase.updatedTemplate.HelmChartName, + OpentofuVersion: templateUseCase.updatedTemplate.OpentofuVersion, } if templateUseCase.template.Type != "terraform" && templateUseCase.template.Type != "terragrunt" { @@ -1110,6 +1161,45 @@ func TestUnitTemplateResource(t *testing.T) { runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) }) + t.Run("Invalid Opentofu Version", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": "template0", + "repository": "env0/repo", + "type": "opentofu", + "gitlab_project_id": 123456, + "token_id": "abcdefg", + "opentofu_version": "v0.20.1", + }), + ExpectError: regexp.MustCompile("must match pattern"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + + t.Run("Opentofu type with no Opentofu version", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": "template0", + "repository": "env0/repo", + "type": "opentofu", + "gitlab_project_id": 123456, + "token_id": "abcdefg", + }), + ExpectError: regexp.MustCompile("must supply opentofu version"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {}) + }) + t.Run("Cloudformation type with no file_name", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ diff --git a/tests/integration/004_template/main.tf b/tests/integration/004_template/main.tf index ca7426ae..6a0fb76c 100644 --- a/tests/integration/004_template/main.tf +++ b/tests/integration/004_template/main.tf @@ -54,6 +54,19 @@ resource "env0_template" "template_tg" { terragrunt_version = "0.35.0" } +resource "env0_template" "template_opentofu" { + name = "Opentofu-${random_string.random.result}" + description = "Template description - OpenTofu and GitHub" + type = "opentofu" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = "/misc/null-resource" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + opentofu_version = "1.6.0" +} + resource "env0_configuration_variable" "in_a_template" { name = "fake_key" value = "fake value"