diff --git a/client/environment.go b/client/environment.go index 8834c647..ed0ad8ab 100644 --- a/client/environment.go +++ b/client/environment.go @@ -93,6 +93,7 @@ type Environment struct { IsArchived bool `json:"isArchived"` TerragruntWorkingDirectory string `json:"terragruntWorkingDirectory,omitempty"` VcsCommandsAlias string `json:"vcsCommandsAlias"` + BlueprintId string `json:"blueprintId" tfschema:"-"` } type EnvironmentCreate struct { diff --git a/env0/data_template.go b/env0/data_template.go index 6d918dbd..d0adf230 100644 --- a/env0/data_template.go +++ b/env0/data_template.go @@ -166,8 +166,8 @@ func dataTemplateRead(ctx context.Context, d *schema.ResourceData, meta interfac return diag.Errorf("schema resource data serialization failed: %v", err) } - templateReadRetryOnHelper(d, "deploy", template.Retry.OnDeploy) - templateReadRetryOnHelper(d, "destroy", template.Retry.OnDestroy) + templateReadRetryOnHelper("", d, "deploy", template.Retry.OnDeploy) + templateReadRetryOnHelper("", d, "destroy", template.Retry.OnDestroy) return nil } diff --git a/env0/resource_environment.go b/env0/resource_environment.go index 5321a67d..454a2578 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -339,6 +339,17 @@ func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta i setEnvironmentSchema(d, environment, environmentConfigurationVariables) + if d.Get("template_id").(string) == "" { + // envrionment with no template. + template, err := apiClient.Template(environment.BlueprintId) + 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) + } + } + return nil } @@ -346,22 +357,26 @@ func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, meta apiClient := meta.(client.ApiClientInterface) if shouldUpdate(d) { - err := update(d, apiClient) - if err != nil { + if err := update(d, apiClient); err != nil { return err } } if shouldUpdateTTL(d) { - err := updateTTL(d, apiClient) - if err != nil { + + if err := updateTTL(d, apiClient); err != nil { + return err + } + } + + if shouldUpdateTemplate(d) { + if err := updateTemplate(d, apiClient); err != nil { return err } } if shouldDeploy(d) { - err := deploy(d, apiClient) - if err != nil { + if err := deploy(d, apiClient); err != nil { return err } } @@ -369,6 +384,15 @@ func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, meta return nil } +func shouldUpdateTemplate(d *schema.ResourceData) bool { + if d.Get("template_id") != "" { + // Using an environment with a template. + return false + } + + return d.HasChange("without_template_settings.0") +} + func shouldDeploy(d *schema.ResourceData) bool { return d.HasChanges("revision", "configuration") } @@ -381,6 +405,24 @@ func shouldUpdateTTL(d *schema.ResourceData) bool { return d.HasChange("ttl") } +func updateTemplate(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Diagnostics { + payload, problem := templateCreatePayloadFromParameters("without_template_settings.0", d) + if problem != nil { + return problem + } + + environment, err := apiClient.Environment(d.Id()) + if err != nil { + return diag.Errorf("could not get environment: %v", err) + } + + if _, err := apiClient.TemplateUpdate(environment.BlueprintId, payload); err != nil { + return diag.Errorf("could not update template: %v", err) + } + + return nil +} + func deploy(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Diagnostics { deployPayload := getDeployPayload(d, apiClient, true) deployResponse, err := apiClient.EnvironmentDeploy(d.Id(), deployPayload) diff --git a/env0/resource_environment_test.go b/env0/resource_environment_test.go index 70fa9bb9..24f453e5 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -997,6 +997,7 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { WorkspaceName: "workspace-name", TerragruntWorkingDirectory: "/terragrunt/directory/", VcsCommandsAlias: "alias", + BlueprintId: "id-template-0", } template := client.Template{ @@ -1021,6 +1022,28 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { TerraformVersion: "0.12.24", } + updatedTemplate := client.Template{ + Id: "id-template-0", + Name: "single-use-template-for-" + environment.Name, + Description: "description1", + Repository: "env0/repo1", + Path: "path/zero1", + Revision: "branch-zero1", + Retry: client.TemplateRetry{ + OnDeploy: &client.TemplateRetryOn{ + Times: 3, + ErrorRegex: "RetryMeForDeploy.*", + }, + OnDestroy: &client.TemplateRetryOn{ + Times: 3, + ErrorRegex: "RetryMeForDestroy.*", + }, + }, + Type: "terraform", + GithubInstallationId: 2, + TerraformVersion: "0.12.25", + } + environmentCreatePayload := client.EnvironmentCreate{ Name: environment.Name, ProjectId: environment.ProjectId, @@ -1055,6 +1078,27 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { OrganizationId: template.OrganizationId, } + templateUpdatePayload := client.TemplateCreatePayload{ + Repository: updatedTemplate.Repository, + Description: updatedTemplate.Description, + GithubInstallationId: updatedTemplate.GithubInstallationId, + IsGitlabEnterprise: updatedTemplate.IsGitlabEnterprise, + IsGitLab: updatedTemplate.TokenId != "", + TokenId: updatedTemplate.TokenId, + Path: updatedTemplate.Path, + Revision: updatedTemplate.Revision, + Type: client.TemplateTypeTerraform, + Retry: updatedTemplate.Retry, + TerraformVersion: updatedTemplate.TerraformVersion, + BitbucketClientKey: updatedTemplate.BitbucketClientKey, + IsGithubEnterprise: updatedTemplate.IsGithubEnterprise, + IsBitbucketServer: updatedTemplate.IsBitbucketServer, + FileName: updatedTemplate.FileName, + TerragruntVersion: updatedTemplate.TerragruntVersion, + IsTerragruntRunAll: updatedTemplate.IsTerragruntRunAll, + OrganizationId: updatedTemplate.OrganizationId, + } + createPayload := client.EnvironmentCreateWithoutTemplate{ EnvironmentCreate: environmentCreatePayload, TemplateCreate: templateCreatePayload, @@ -1106,6 +1150,7 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { t.Run("Success in create", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ + // Create the environment and template { Config: createEnvironmentResourceConfig(environment, template), Check: resource.ComposeAggregateTestCheckFunc( @@ -1127,13 +1172,72 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retry_on_destroy_only_when_matches_regex", template.Retry.OnDestroy.ErrorRegex), ), }, + // Update the template. + { + Config: createEnvironmentResourceConfig(environment, updatedTemplate), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environment.Id), + resource.TestCheckResourceAttr(accessor, "name", environment.Name), + resource.TestCheckResourceAttr(accessor, "project_id", environment.ProjectId), + resource.TestCheckNoResourceAttr(accessor, "template_id"), + resource.TestCheckResourceAttr(accessor, "workspace", environment.WorkspaceName), + resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", environment.TerragruntWorkingDirectory), + resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", environment.VcsCommandsAlias), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.repository", updatedTemplate.Repository), + 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.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), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retries_on_destroy", strconv.Itoa(updatedTemplate.Retry.OnDestroy.Times)), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retry_on_destroy_only_when_matches_regex", updatedTemplate.Retry.OnDestroy.ErrorRegex), + ), + }, + // No need to update template + { + Config: createEnvironmentResourceConfig(environment, updatedTemplate), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environment.Id), + resource.TestCheckResourceAttr(accessor, "name", environment.Name), + resource.TestCheckResourceAttr(accessor, "project_id", environment.ProjectId), + resource.TestCheckNoResourceAttr(accessor, "template_id"), + resource.TestCheckResourceAttr(accessor, "workspace", environment.WorkspaceName), + resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", environment.TerragruntWorkingDirectory), + resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", environment.VcsCommandsAlias), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.repository", updatedTemplate.Repository), + 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.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), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retries_on_destroy", strconv.Itoa(updatedTemplate.Retry.OnDestroy.Times)), + resource.TestCheckResourceAttr(accessor, "without_template_settings.0.retry_on_destroy_only_when_matches_regex", updatedTemplate.Retry.OnDestroy.ErrorRegex), + ), + }, }, } runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + // Step1 mock.EXPECT().EnvironmentCreateWithoutTemplate(createPayload).Times(1).Return(environment, nil) mock.EXPECT().Environment(environment.Id).Times(1).Return(environment, nil) mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil) + mock.EXPECT().Template(environment.BlueprintId).Times(1).Return(template, nil) + + // Step2 + mock.EXPECT().Environment(environment.Id).Times(2).Return(environment, nil) + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(2).Return(client.ConfigurationChanges{}, nil) + mock.EXPECT().Template(environment.BlueprintId).Times(1).Return(template, nil) + mock.EXPECT().Environment(environment.Id).Times(1).Return(environment, nil) + mock.EXPECT().TemplateUpdate(environment.BlueprintId, templateUpdatePayload).Times(1).Return(updatedTemplate, nil) + mock.EXPECT().Template(environment.BlueprintId).Times(1).Return(updatedTemplate, nil) + + // Step3 + mock.EXPECT().Environment(environment.Id).Times(2).Return(environment, nil) + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(2).Return(client.ConfigurationChanges{}, nil) + mock.EXPECT().Template(environment.BlueprintId).Times(2).Return(updatedTemplate, nil) mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1) }) }) diff --git a/env0/resource_template.go b/env0/resource_template.go index 492b334b..31da9b3b 100644 --- a/env0/resource_template.go +++ b/env0/resource_template.go @@ -257,7 +257,7 @@ func resourceTemplateRead(ctx context.Context, d *schema.ResourceData, meta inte return nil } - if err := templateRead(template, d); err != nil { + if err := templateRead("", template, d); err != nil { return diag.Errorf("%v", err) } @@ -354,24 +354,36 @@ func templateCreatePayloadFromParameters(prefix string, d *schema.ResourceData) } // Reads template and writes to the resource data. -func templateRead(template client.Template, d *schema.ResourceData) error { - if err := writeResourceData(&template, d); err != nil { +func templateRead(prefix string, template client.Template, d *schema.ResourceData) error { + if err := writeResourceDataEx(prefix, &template, d); err != nil { return fmt.Errorf("schema resource data serialization failed: %v", err) } - templateReadRetryOnHelper(d, "deploy", template.Retry.OnDeploy) - templateReadRetryOnHelper(d, "destroy", template.Retry.OnDestroy) + templateReadRetryOnHelper(prefix, d, "deploy", template.Retry.OnDeploy) + templateReadRetryOnHelper(prefix, d, "destroy", template.Retry.OnDestroy) return nil } // Helpers function for templateRead. -func templateReadRetryOnHelper(d *schema.ResourceData, retryType string, retryOn *client.TemplateRetryOn) { - if retryOn != nil { - d.Set("retries_on_"+retryType, retryOn.Times) - d.Set("retry_on_"+retryType+"_only_when_matches_regex", retryOn.ErrorRegex) +func templateReadRetryOnHelper(prefix string, d *schema.ResourceData, retryType string, retryOn *client.TemplateRetryOn) { + if prefix != "" { + value := d.Get(prefix + ".0").(map[string]interface{}) + if retryOn != nil { + value["retries_on_"+retryType] = retryOn.Times + value["retry_on_"+retryType+"_only_when_matches_regex"] = retryOn.ErrorRegex + } else { + value["retries_on_"+retryType] = 0 + value["retry_on_"+retryType+"_only_when_matches_regex"] = "" + } + d.Set(prefix, []interface{}{value}) } else { - d.Set("retries_on_"+retryType, 0) - d.Set("retry_on_"+retryType+"_only_when_matches_regex", "") + if retryOn != nil { + d.Set("retries_on_"+retryType, retryOn.Times) + d.Set("retry_on_"+retryType+"_only_when_matches_regex", retryOn.ErrorRegex) + } else { + d.Set("retries_on_"+retryType, 0) + d.Set("retry_on_"+retryType+"_only_when_matches_regex", "") + } } } diff --git a/env0/utils.go b/env0/utils.go index 38275afe..9becc4a0 100644 --- a/env0/utils.go +++ b/env0/utils.go @@ -404,3 +404,10 @@ func writeResourceDataSlice(i interface{}, name string, d *schema.ResourceData) return nil } + +func writeResourceDataEx(prefix string, i interface{}, d *schema.ResourceData) error { + if prefix == "" { + return writeResourceData(i, d) + } + return writeResourceDataSlice([]interface{}{i}, prefix, d) +} diff --git a/tests/integration/012_environment/main.tf b/tests/integration/012_environment/main.tf index 5758e56f..f1db06d7 100644 --- a/tests/integration/012_environment/main.tf +++ b/tests/integration/012_environment/main.tf @@ -76,3 +76,28 @@ output "revision" { output "terragrunt_working_directory" { value = env0_environment.terragrunt_environment.terragrunt_working_directory } + +data "env0_template" "github_template" { + name = "Github Integrated Template" +} + +resource "env0_environment" "environment-without-template" { + force_destroy = true + name = "environment-without-template-${random_string.random.result}" + project_id = env0_project.test_project.id + approve_plan_automatically = true + revision = "master" + auto_deploy_on_path_changes_only = false + + without_template_settings { + description = "Template description - GitHub" + type = "terraform" + repository = data.env0_template.github_template.repository + github_installation_id = data.env0_template.github_template.github_installation_id + path = var.second_run ? "second" : "misc/null-resource" + retries_on_deploy = 3 + retry_on_deploy_only_when_matches_regex = "abc" + retries_on_destroy = 1 + terraform_version = "0.15.1" + } +} \ No newline at end of file