From af332720389ae466832e6f5f3569adf215aced6f Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Mon, 14 Oct 2024 10:54:33 -0500 Subject: [PATCH] Feat: add wait_for_destroy option when destroying and environment (#946) * Feat: add wait_for_destroy option when destroying and environment * changes based on PR comments * minor change * added disclaimer * made changes based on PR feedback --- client/api_client.go | 3 +- client/api_client_mock.go | 19 +- client/environment.go | 24 ++- client/environment_test.go | 44 ++++- env0/data_agent_values_test.go | 1 - env0/data_cloud_credentials.go | 1 + env0/data_credentials_test.go | 1 - env0/data_custom_flow.go | 2 + env0/data_custom_role.go | 1 + env0/data_custom_roles.go | 1 + env0/data_environment.go | 2 + env0/data_git_token.go | 1 + env0/data_kubernetes_credentials_test.go | 1 - env0/data_module_testing_project_test.go | 1 - env0/data_notifications.go | 1 + env0/data_oidc_credentials_test.go | 1 - env0/data_project_policy.go | 2 + env0/data_projects.go | 1 + env0/data_source_code_variables_test.go | 1 - env0/data_sshkey.go | 2 + env0/data_team.go | 1 + env0/data_template.go | 1 + env0/data_variable_set.go | 3 + env0/data_variable_set_test.go | 1 + env0/data_workflow_triggers.go | 4 +- env0/errors.go | 1 + env0/resource_api_key.go | 4 +- env0/resource_api_key_test.go | 1 - env0/resource_approval_policy.go | 2 +- env0/resource_approval_policy_assignment.go | 1 + env0/resource_approval_policy_test.go | 2 + env0/resource_azure_credentials_test.go | 1 - ...ce_cloud_credentials_project_assignment.go | 6 + ...oud_credentials_project_assignment_test.go | 2 + env0/resource_configuration_variable.go | 5 +- env0/resource_cost_credentials.go | 4 +- ...rce_cost_credentials_project_assignment.go | 6 + ...ost_credentials_project_assignment_test.go | 1 + env0/resource_cost_credentials_test.go | 7 +- env0/resource_environment.go | 87 ++++++++- ...ment_output_configuration_variable_test.go | 1 - env0/resource_environment_scheduling_test.go | 3 +- env0/resource_environment_test.go | 182 +++++++++++++++++- env0/resource_gcp_credentials_test.go | 1 - env0/resource_notification.go | 2 + ...esource_notification_project_assignment.go | 1 + env0/resource_template.go | 1 + env0/resource_template_test.go | 3 + env0/resource_user_organization_assignment.go | 1 - env0/resource_user_team_assignment.go | 2 + env0/resource_variable_set.go | 1 + env0/resource_workflow_triggers.go | 6 +- tests/integration/012_environment/main.tf | 1 + 53 files changed, 408 insertions(+), 47 deletions(-) diff --git a/client/api_client.go b/client/api_client.go index e3d55773..26b46473 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -66,7 +66,7 @@ type ApiClientInterface interface { Environment(id string) (Environment, error) EnvironmentCreate(payload EnvironmentCreate) (Environment, error) EnvironmentCreateWithoutTemplate(payload EnvironmentCreateWithoutTemplate) (Environment, error) - EnvironmentDestroy(id string) (Environment, error) + EnvironmentDestroy(id string) (*EnvironmentDestroyResponse, error) EnvironmentMarkAsArchived(id string) error EnvironmentUpdate(id string, payload EnvironmentUpdate) (Environment, error) EnvironmentDeploy(id string, payload DeployRequest) (EnvironmentDeployResponse, error) @@ -75,6 +75,7 @@ type ApiClientInterface interface { EnvironmentScheduling(environmentId string) (EnvironmentScheduling, error) EnvironmentSchedulingUpdate(environmentId string, payload EnvironmentScheduling) (EnvironmentScheduling, error) EnvironmentSchedulingDelete(environmentId string) error + EnvironmentDeploymentLog(id string) (*DeploymentLog, error) WorkflowTrigger(environmentId string) ([]WorkflowTrigger, error) WorkflowTriggerUpsert(environmentId string, request WorkflowTriggerUpsertPayload) ([]WorkflowTrigger, error) EnvironmentDriftDetection(environmentId string) (EnvironmentSchedulingExpression, error) diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 8f275e10..2ad01ad9 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -852,11 +852,26 @@ func (mr *MockApiClientInterfaceMockRecorder) EnvironmentDeploy(arg0, arg1 any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvironmentDeploy", reflect.TypeOf((*MockApiClientInterface)(nil).EnvironmentDeploy), arg0, arg1) } +// EnvironmentDeploymentLog mocks base method. +func (m *MockApiClientInterface) EnvironmentDeploymentLog(arg0 string) (*DeploymentLog, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnvironmentDeploymentLog", arg0) + ret0, _ := ret[0].(*DeploymentLog) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// EnvironmentDeploymentLog indicates an expected call of EnvironmentDeploymentLog. +func (mr *MockApiClientInterfaceMockRecorder) EnvironmentDeploymentLog(arg0 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvironmentDeploymentLog", reflect.TypeOf((*MockApiClientInterface)(nil).EnvironmentDeploymentLog), arg0) +} + // EnvironmentDestroy mocks base method. -func (m *MockApiClientInterface) EnvironmentDestroy(arg0 string) (Environment, error) { +func (m *MockApiClientInterface) EnvironmentDestroy(arg0 string) (*EnvironmentDestroyResponse, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "EnvironmentDestroy", arg0) - ret0, _ := ret[0].(Environment) + ret0, _ := ret[0].(*EnvironmentDestroyResponse) ret1, _ := ret[1].(error) return ret0, ret1 } diff --git a/client/environment.go b/client/environment.go index 029a61ed..498dd159 100644 --- a/client/environment.go +++ b/client/environment.go @@ -29,7 +29,7 @@ func (c *ConfigurationVariableType) WriteResourceData(fieldName string, d *schem switch val := *c; val { case 0: - valStr = "environment" + valStr = ENVIRONMENT case 1: valStr = TERRAFORM default: @@ -93,6 +93,7 @@ type DeploymentLog struct { Output json.RawMessage `json:"output,omitempty"` Error json.RawMessage `json:"error,omitempty"` Type string `json:"type"` + Status string `json:"status"` WorkflowFile *WorkflowFile `json:"workflowFile,omitempty" tfschema:"-"` } @@ -165,6 +166,10 @@ type EnvironmentMoveRequest struct { ProjectId string `json:"projectId"` } +type EnvironmentDestroyResponse struct { + Id string `json:"id"` +} + func GetConfigurationVariableType(variableType string) (ConfigurationVariableType, error) { switch variableType { case "terraform": @@ -258,6 +263,15 @@ func (client *ApiClient) Environment(id string) (Environment, error) { return result, nil } +func (client *ApiClient) EnvironmentDeploymentLog(id string) (*DeploymentLog, error) { + var result DeploymentLog + err := client.http.Get("/environments/deployments/"+id, nil, &result) + if err != nil { + return nil, err + } + return &result, nil +} + func (client *ApiClient) EnvironmentCreate(payload EnvironmentCreate) (Environment, error) { var result Environment @@ -285,13 +299,13 @@ func (client *ApiClient) EnvironmentCreateWithoutTemplate(payload EnvironmentCre return result, nil } -func (client *ApiClient) EnvironmentDestroy(id string) (Environment, error) { - var result Environment +func (client *ApiClient) EnvironmentDestroy(id string) (*EnvironmentDestroyResponse, error) { + var result EnvironmentDestroyResponse err := client.http.Post("/environments/"+id+"/destroy", nil, &result) if err != nil { - return Environment{}, err + return nil, err } - return result, nil + return &result, nil } func (client *ApiClient) EnvironmentUpdate(id string, payload EnvironmentUpdate) (Environment, error) { diff --git a/client/environment_test.go b/client/environment_test.go index 76747cd0..34e95a04 100644 --- a/client/environment_test.go +++ b/client/environment_test.go @@ -280,15 +280,27 @@ var _ = Describe("Environment Client", func() { Describe("EnvironmentDelete", func() { var err error + var res *EnvironmentDestroyResponse + + mockedRes := EnvironmentDestroyResponse{ + Id: "id123", + } BeforeEach(func() { - httpCall = mockHttpClient.EXPECT().Post("/environments/"+mockEnvironment.Id+"/destroy", nil, gomock.Any()).Times(1) - _, err = apiClient.EnvironmentDestroy(mockEnvironment.Id) + httpCall = mockHttpClient.EXPECT().Post("/environments/"+mockEnvironment.Id+"/destroy", nil, gomock.Any()).Times(1). + Do((func(path string, request interface{}, response *EnvironmentDestroyResponse) { + *response = mockedRes + })) + res, err = apiClient.EnvironmentDestroy(mockEnvironment.Id) }) It("Should not return error", func() { Expect(err).To(BeNil()) }) + + It("Should return the expected response", func() { + Expect(*res).To(Equal(mockedRes)) + }) }) Describe("EnvironmentMarkAsArchived", func() { @@ -438,6 +450,34 @@ var _ = Describe("Environment Client", func() { Expect(err).To(BeNil()) }) }) + + Describe("EnvironmentDeployment", func() { + var deployment *DeploymentLog + var err error + + mockDeployment := DeploymentLog{ + Id: "id12345", + Status: "IN_PROGRESS", + } + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/environments/deployments/"+mockDeployment.Id, nil, gomock.Any()). + Do(func(path string, request interface{}, response *DeploymentLog) { + *response = mockDeployment + }).Times(1) + + deployment, err = apiClient.EnvironmentDeploymentLog(mockDeployment.Id) + }) + + It("Should return deployment", func() { + Expect(*deployment).To(Equal(mockDeployment)) + }) + + It("Should not return an error", func() { + Expect(err).To(BeNil()) + }) + }) }) func TestMarshalEnvironmentCreateWithoutTemplate(t *testing.T) { diff --git a/env0/data_agent_values_test.go b/env0/data_agent_values_test.go index 4236349a..a640e91c 100644 --- a/env0/data_agent_values_test.go +++ b/env0/data_agent_values_test.go @@ -63,5 +63,4 @@ func TestAgentValues(t *testing.T) { }, ) }) - } diff --git a/env0/data_cloud_credentials.go b/env0/data_cloud_credentials.go index cbe6ce9e..d631e3a7 100644 --- a/env0/data_cloud_credentials.go +++ b/env0/data_cloud_credentials.go @@ -62,6 +62,7 @@ func dataCloudCredentialsRead(ctx context.Context, d *schema.ResourceData, meta if filter && credential_type != credentials.Type { continue } + data = append(data, credentials.Name) } diff --git a/env0/data_credentials_test.go b/env0/data_credentials_test.go index 5e88f696..099489f1 100644 --- a/env0/data_credentials_test.go +++ b/env0/data_credentials_test.go @@ -146,5 +146,4 @@ func TestCredentialsDataSource(t *testing.T) { ) }) } - } diff --git a/env0/data_custom_flow.go b/env0/data_custom_flow.go index a3c349e8..6679ab10 100644 --- a/env0/data_custom_flow.go +++ b/env0/data_custom_flow.go @@ -31,6 +31,7 @@ func dataCustomFlow() *schema.Resource { func dataCustomFlowRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var err error + var customFlow *client.CustomFlow id, ok := d.GetOk("id") @@ -41,6 +42,7 @@ func dataCustomFlowRead(ctx context.Context, d *schema.ResourceData, meta interf } } else { name := d.Get("name") + customFlow, err = getCustomFlowByName(name.(string), meta) if err != nil { return diag.Errorf("failed to get custom flow by name: %v", err) diff --git a/env0/data_custom_role.go b/env0/data_custom_role.go index 7b03a94b..edf938ab 100644 --- a/env0/data_custom_role.go +++ b/env0/data_custom_role.go @@ -31,6 +31,7 @@ func dataCustomRole() *schema.Resource { func dataCustomRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var err error + var role *client.Role id, ok := d.GetOk("id") diff --git a/env0/data_custom_roles.go b/env0/data_custom_roles.go index 3d35a9f9..e78bb346 100644 --- a/env0/data_custom_roles.go +++ b/env0/data_custom_roles.go @@ -28,6 +28,7 @@ func dataCustomRoles() *schema.Resource { func dataCustomRolesRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) + roles, err := apiClient.Roles() if err != nil { return diag.Errorf("Failed to get custom roles: %v", err) diff --git a/env0/data_environment.go b/env0/data_environment.go index 6ccb0ee8..f280639f 100644 --- a/env0/data_environment.go +++ b/env0/data_environment.go @@ -125,6 +125,7 @@ func dataEnvironment() *schema.Resource { func dataEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var err diag.Diagnostics + var environment client.Environment projectId := d.Get("project_id").(string) @@ -138,6 +139,7 @@ func dataEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta inter } else { name := d.Get("name").(string) excludeArchived := d.Get("exclude_archived") + environment, err = getEnvironmentByName(meta, name, projectId, excludeArchived.(bool)) if err != nil { return err diff --git a/env0/data_git_token.go b/env0/data_git_token.go index 62eb5da3..58dde20a 100644 --- a/env0/data_git_token.go +++ b/env0/data_git_token.go @@ -31,6 +31,7 @@ func dataGitToken() *schema.Resource { func dataGitTokenRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { var gitToken *client.GitToken + var err error id, ok := d.GetOk("id") diff --git a/env0/data_kubernetes_credentials_test.go b/env0/data_kubernetes_credentials_test.go index ebd2160e..50cfa472 100644 --- a/env0/data_kubernetes_credentials_test.go +++ b/env0/data_kubernetes_credentials_test.go @@ -125,5 +125,4 @@ func TestKubernetesCredentialsDataSource(t *testing.T) { ) }) } - } diff --git a/env0/data_module_testing_project_test.go b/env0/data_module_testing_project_test.go index 468dd4f1..04f48871 100644 --- a/env0/data_module_testing_project_test.go +++ b/env0/data_module_testing_project_test.go @@ -61,5 +61,4 @@ func TestModuleTestingProjectDataSource(t *testing.T) { }, ) }) - } diff --git a/env0/data_notifications.go b/env0/data_notifications.go index 71b7132a..e43b9a67 100644 --- a/env0/data_notifications.go +++ b/env0/data_notifications.go @@ -28,6 +28,7 @@ func dataNotifications() *schema.Resource { func dataNotificationsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) + notifications, err := apiClient.Notifications() if err != nil { return diag.Errorf("could not get notifications: %v", err) diff --git a/env0/data_oidc_credentials_test.go b/env0/data_oidc_credentials_test.go index 4764a798..e39204b1 100644 --- a/env0/data_oidc_credentials_test.go +++ b/env0/data_oidc_credentials_test.go @@ -125,5 +125,4 @@ func TestOidcCredentialDataSource(t *testing.T) { ) }) } - } diff --git a/env0/data_project_policy.go b/env0/data_project_policy.go index a8cd6bcf..97898c8a 100644 --- a/env0/data_project_policy.go +++ b/env0/data_project_policy.go @@ -115,9 +115,11 @@ func dataPolicyRead(ctx context.Context, d *schema.ResourceData, meta interface{ func getPolicyByProjectId(projectId string, meta interface{}) (client.Policy, diag.Diagnostics) { apiClient := meta.(client.ApiClientInterface) + policy, err := apiClient.Policy(projectId) if err != nil { return client.Policy{}, diag.Errorf("Could not query policy: %v", err) } + return policy, nil } diff --git a/env0/data_projects.go b/env0/data_projects.go index 3c0271e4..480e34ac 100644 --- a/env0/data_projects.go +++ b/env0/data_projects.go @@ -68,6 +68,7 @@ func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interfac } filteredProjects := []client.Project{} + for _, project := range projects { if includeArchivedProjects || !project.IsArchived { filteredProjects = append(filteredProjects, project) diff --git a/env0/data_source_code_variables_test.go b/env0/data_source_code_variables_test.go index c5f52327..5e05faca 100644 --- a/env0/data_source_code_variables_test.go +++ b/env0/data_source_code_variables_test.go @@ -102,5 +102,4 @@ func TestSourceCodeVariablesDataSource(t *testing.T) { }, ) }) - } diff --git a/env0/data_sshkey.go b/env0/data_sshkey.go index 83830b55..31d3a33e 100644 --- a/env0/data_sshkey.go +++ b/env0/data_sshkey.go @@ -80,6 +80,7 @@ func getSshKeyByName(name interface{}, meta interface{}) (*client.SshKey, error) if len(sshKeysByName) > 1 { return nil, backoff.Permanent(fmt.Errorf("found multiple ssh keys with name: %s. Use id instead or make sure ssh key names are unique %v", name, sshKeysByName)) } + if len(sshKeysByName) == 0 { return nil, fmt.Errorf("ssh key with name %v not found", name) } @@ -97,6 +98,7 @@ func getSshKeyById(id interface{}, meta interface{}) (*client.SshKey, error) { } var sshKey *client.SshKey + for _, candidate := range sshKeys { if candidate.Id == id.(string) { sshKey = &candidate diff --git a/env0/data_team.go b/env0/data_team.go index 2a8248eb..e575e174 100644 --- a/env0/data_team.go +++ b/env0/data_team.go @@ -49,6 +49,7 @@ func dataTeamRead(ctx context.Context, d *schema.ResourceData, meta interface{}) if !ok { return diag.Errorf("Either 'name' or 'id' must be specified") } + team, err = getTeamByName(name.(string), meta) if err != nil { return err diff --git a/env0/data_template.go b/env0/data_template.go index 41ff3aa3..2818eff1 100644 --- a/env0/data_template.go +++ b/env0/data_template.go @@ -205,5 +205,6 @@ func getTemplateById(id interface{}, meta interface{}) (client.Template, diag.Di if err != nil { return client.Template{}, diag.Errorf("Could not query template: %v", err) } + return template, nil } diff --git a/env0/data_variable_set.go b/env0/data_variable_set.go index 67a750da..a5dd2bbd 100644 --- a/env0/data_variable_set.go +++ b/env0/data_variable_set.go @@ -56,6 +56,7 @@ func dataVariableSetRead(ctx context.Context, d *schema.ResourceData, meta inter switch resource.Scope { case "ORGANIZATION": var err error + scopeId, err = apiClient.OrganizationId() if err != nil { return diag.Errorf("could not get organization id: %v", err) @@ -64,6 +65,7 @@ func dataVariableSetRead(ctx context.Context, d *schema.ResourceData, meta inter if resource.ProjectId == "" { return diag.Errorf("'project_id' is required") } + scopeId = resource.ProjectId } @@ -75,6 +77,7 @@ func dataVariableSetRead(ctx context.Context, d *schema.ResourceData, meta inter for _, variableSet := range variableSets { if variableSet.Name == resource.Name { d.SetId(variableSet.Id) + return nil } } diff --git a/env0/data_variable_set_test.go b/env0/data_variable_set_test.go index 21e13971..1b8afb42 100644 --- a/env0/data_variable_set_test.go +++ b/env0/data_variable_set_test.go @@ -54,6 +54,7 @@ func TestVariableSetDataSource(t *testing.T) { if organizationId != "" { mock.EXPECT().OrganizationId().AnyTimes().Return(organizationId, nil) } + mock.EXPECT().ConfigurationSets(scope, scopeId).AnyTimes().Return(returnValue, nil) } } diff --git a/env0/data_workflow_triggers.go b/env0/data_workflow_triggers.go index 500b113a..15be0459 100644 --- a/env0/data_workflow_triggers.go +++ b/env0/data_workflow_triggers.go @@ -47,7 +47,9 @@ func dataWorkflowTriggersRead(ctx context.Context, d *schema.ResourceData, meta } d.SetId(environmentId) - var triggerIds []string + + triggerIds := []string{} + for _, value := range triggers { triggerIds = append(triggerIds, value.Id) } diff --git a/env0/errors.go b/env0/errors.go index 2f7c28b2..66e164e4 100644 --- a/env0/errors.go +++ b/env0/errors.go @@ -28,6 +28,7 @@ func ResourceGetFailure(ctx context.Context, resourceName string, d *schema.Reso if driftDetected(err) { tflog.Warn(ctx, "Drift Detected: Terraform will remove id from state", map[string]interface{}{"id": d.Id()}) d.SetId("") + return nil } diff --git a/env0/resource_api_key.go b/env0/resource_api_key.go index 41b41350..60393946 100644 --- a/env0/resource_api_key.go +++ b/env0/resource_api_key.go @@ -119,11 +119,13 @@ func getApiKeyById(id string, meta interface{}) (*client.ApiKey, error) { if err != nil { return nil, err } + for _, apiKey := range apiKeys { if apiKey.Id == id { return &apiKey, nil } } + return nil, nil } @@ -174,7 +176,7 @@ func resourceApiKeyImport(ctx context.Context, d *schema.ResourceData, meta inte } if err := writeResourceData(apiKey, d); err != nil { - return nil, fmt.Errorf("schema resource data serialization failed: %v", err) + return nil, fmt.Errorf("schema resource data serialization failed: %w", err) } return []*schema.ResourceData{d}, nil diff --git a/env0/resource_api_key_test.go b/env0/resource_api_key_test.go index 1b213de8..f9ce0520 100644 --- a/env0/resource_api_key_test.go +++ b/env0/resource_api_key_test.go @@ -258,5 +258,4 @@ func TestUnitApiKeyResource(t *testing.T) { mock.EXPECT().ApiKeyDelete(updatedApiKey.Id).Times(1) }) }) - } diff --git a/env0/resource_approval_policy.go b/env0/resource_approval_policy.go index 88fb3e89..71f4e606 100644 --- a/env0/resource_approval_policy.go +++ b/env0/resource_approval_policy.go @@ -135,7 +135,7 @@ func resourceApprovalPolicyImport(ctx context.Context, d *schema.ResourceData, m } if err := writeResourceData(approvalPolicy, d); err != nil { - return nil, fmt.Errorf("schema resource data serialization failed: %v", err) + return nil, fmt.Errorf("schema resource data serialization failed: %w", err) } return []*schema.ResourceData{d}, nil diff --git a/env0/resource_approval_policy_assignment.go b/env0/resource_approval_policy_assignment.go index f989b3f0..01c75cf5 100644 --- a/env0/resource_approval_policy_assignment.go +++ b/env0/resource_approval_policy_assignment.go @@ -88,6 +88,7 @@ func resourceApprovalPolicyAssignmentRead(ctx context.Context, d *schema.Resourc for _, approvalPolicyByScope := range approvalPolicyByScopeArr { if approvalPolicyByScope.ApprovalPolicy.Id == assignment.BlueprintId { found = true + break } } diff --git a/env0/resource_approval_policy_test.go b/env0/resource_approval_policy_test.go index 522c5594..43393523 100644 --- a/env0/resource_approval_policy_test.go +++ b/env0/resource_approval_policy_test.go @@ -34,6 +34,7 @@ func TestUnitApprovalPolicyResource(t *testing.T) { } var template client.Template + require.NoError(t, copier.Copy(&template, &approvalPolicy)) template.Type = string(ApprovalPolicy) @@ -54,6 +55,7 @@ func TestUnitApprovalPolicyResource(t *testing.T) { } var updatedTemplate client.Template + require.NoError(t, copier.Copy(&updatedTemplate, &updatedApprovalPolicy)) updatedTemplate.Type = string(ApprovalPolicy) diff --git a/env0/resource_azure_credentials_test.go b/env0/resource_azure_credentials_test.go index dba16995..cdf70e82 100644 --- a/env0/resource_azure_credentials_test.go +++ b/env0/resource_azure_credentials_test.go @@ -13,7 +13,6 @@ import ( ) func TestUnitAzureCredentialsResource(t *testing.T) { - resourceType := "env0_azure_credentials" resourceName := "test" resourceNameImport := resourceType + "." + resourceName diff --git a/env0/resource_cloud_credentials_project_assignment.go b/env0/resource_cloud_credentials_project_assignment.go index e2605563..abc0c124 100644 --- a/env0/resource_cloud_credentials_project_assignment.go +++ b/env0/resource_cloud_credentials_project_assignment.go @@ -43,11 +43,14 @@ func resourceCloudCredentialsProjectAssignmentCreate(ctx context.Context, d *sch apiClient := meta.(client.ApiClientInterface) credentialId, projectId := getCredentialIdAndProjectId(d) + result, err := apiClient.AssignCloudCredentialsToProject(projectId, credentialId) if err != nil { return diag.Errorf("could not assign cloud credentials to project: %v", err) } + d.SetId(getResourceId(result.CredentialId, result.ProjectId)) + return nil } @@ -55,11 +58,14 @@ func resourceCloudCredentialsProjectAssignmentRead(ctx context.Context, d *schem apiClient := meta.(client.ApiClientInterface) credentialId, projectId := getCredentialIdAndProjectId(d) + credentialsList, err := apiClient.CloudCredentialIdsInProject(projectId) if err != nil { return diag.Errorf("could not get cloud_credentials: %v", err) } + found := false + for _, candidate := range credentialsList { if candidate == credentialId { found = true diff --git a/env0/resource_cloud_credentials_project_assignment_test.go b/env0/resource_cloud_credentials_project_assignment_test.go index 907b3034..59d0645e 100644 --- a/env0/resource_cloud_credentials_project_assignment_test.go +++ b/env0/resource_cloud_credentials_project_assignment_test.go @@ -23,10 +23,12 @@ func TestUnitResourceCloudCredentialsProjectAssignmentResource(t *testing.T) { CredentialId: "cred-it", ProjectId: "proj-it-update", } + stepConfig := resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ "credential_id": assignment.CredentialId, "project_id": assignment.ProjectId, }) + t.Run("Create", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ diff --git a/env0/resource_configuration_variable.go b/env0/resource_configuration_variable.go index 6122c9f1..63e51dc6 100644 --- a/env0/resource_configuration_variable.go +++ b/env0/resource_configuration_variable.go @@ -152,7 +152,7 @@ func getConfigurationVariableCreateParams(d *schema.ResourceData) (*client.Confi scope, scopeId := whichScope(d) params := client.ConfigurationVariableCreateParams{Scope: scope, ScopeId: scopeId} if err := readResourceData(¶ms, d); err != nil { - return nil, fmt.Errorf("schema resource data deserialization failed: %v", err) + return nil, fmt.Errorf("schema resource data deserialization failed: %w", err) } if err := validateNilValue(params.IsReadOnly, params.IsRequired, params.Value); err != nil { @@ -191,6 +191,7 @@ func getEnum(d *schema.ResourceData, selectedValue string) ([]string, error) { if specified, ok := d.GetOk("enum"); ok { enumValues = specified.([]interface{}) valueExists := false + for i, enumValue := range enumValues { if enumValue == nil { return nil, fmt.Errorf("an empty enum value is not allowed (at index %d)", i) @@ -267,8 +268,10 @@ func resourceConfigurationVariableDelete(ctx context.Context, d *schema.Resource func resourceConfigurationVariableImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { var configurationParams ConfigurationVariableParams inputData := d.Id() + // soft delete isn't part of the configuration variable, so we need to set it d.Set("soft_delete", false) + err := json.Unmarshal([]byte(inputData), &configurationParams) // We need this conversion since getConfigurationVariable query by the scope and in our BE we use blueprint as the scope name instead of template if string(configurationParams.Scope) == "TEMPLATE" { diff --git a/env0/resource_cost_credentials.go b/env0/resource_cost_credentials.go index c1d84fdc..6cd6e053 100644 --- a/env0/resource_cost_credentials.go +++ b/env0/resource_cost_credentials.go @@ -69,7 +69,7 @@ func resourceCostCredentials(providerName string) *schema.Resource { }, } default: - panic(fmt.Sprintf("unhandled provider name: %s", providerName)) + panic("unhandled provider name: " + providerName) } } @@ -94,7 +94,7 @@ func resourceCostCredentials(providerName string) *schema.Resource { } value = &payload.(*client.GoogleCostCredentialsCreatePayload).Value default: - panic(fmt.Sprintf("unhandled provider name: %s", providerName)) + panic("unhandled provider name: " + providerName) } if err := readResourceData(value, d); err != nil { diff --git a/env0/resource_cost_credentials_project_assignment.go b/env0/resource_cost_credentials_project_assignment.go index 9a491eb5..44608fe6 100644 --- a/env0/resource_cost_credentials_project_assignment.go +++ b/env0/resource_cost_credentials_project_assignment.go @@ -39,7 +39,9 @@ func resourceCostCredentialsProjectAssignmentCreate(ctx context.Context, d *sche if err != nil { return diag.Errorf("could not assign cost credentials to project: %v", err) } + d.SetId(getResourceId(result.CredentialsId, result.ProjectId)) + return nil } @@ -47,16 +49,20 @@ func resourceCostdCredentialsProjectAssignmentRead(ctx context.Context, d *schem apiClient := meta.(client.ApiClientInterface) credentialId, projectId := getCredentialIdAndProjectId(d) + credentialsList, err := apiClient.CostCredentialIdsInProject(projectId) if err != nil { return diag.Errorf("could not get cost credentials: %v", err) } + found := false + for _, candidate := range credentialsList { if candidate.CredentialsId == credentialId { found = true } } + if !found && !d.IsNewResource() { d.SetId("") return nil diff --git a/env0/resource_cost_credentials_project_assignment_test.go b/env0/resource_cost_credentials_project_assignment_test.go index 6c1c1b46..7ba71ec0 100644 --- a/env0/resource_cost_credentials_project_assignment_test.go +++ b/env0/resource_cost_credentials_project_assignment_test.go @@ -35,6 +35,7 @@ func TestUnitResourceCostCredentialsProjectAssignmentResource(t *testing.T) { "credential_id": assignment.CredentialsId, "project_id": assignment.ProjectId, }) + t.Run("Create", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ diff --git a/env0/resource_cost_credentials_test.go b/env0/resource_cost_credentials_test.go index 3f7036e1..fe3b5ee1 100644 --- a/env0/resource_cost_credentials_test.go +++ b/env0/resource_cost_credentials_test.go @@ -12,7 +12,6 @@ import ( ) func TestUnitAwsCostCredentialsResource(t *testing.T) { - resourceType := "env0_aws_cost_credentials" resourceName := "test" accessor := resourceAccessor(resourceType, resourceName) @@ -154,11 +153,9 @@ func TestUnitAwsCostCredentialsResource(t *testing.T) { }, func(mock *client.MockApiClientInterface) { }) }) - } func TestUnitAzureCostCredentialsResource(t *testing.T) { - resourceType := "env0_azure_cost_credentials" resourceName := "test" accessor := resourceAccessor(resourceType, resourceName) @@ -289,17 +286,15 @@ func TestUnitAzureCostCredentialsResource(t *testing.T) { } for _, testCase := range missingArgumentsTestCases { tc := testCase + t.Run("validate specific argument", func(t *testing.T) { runUnitTest(t, tc, func(mock *client.MockApiClientInterface) {}) }) - } }) - } func TestUnitGoogleCostCredentialsResource(t *testing.T) { - resourceType := "env0_gcp_cost_credentials" resourceName := "test" accessor := resourceAccessor(resourceType, resourceName) diff --git a/env0/resource_environment.go b/env0/resource_environment.go index 2743f557..5f82ceb5 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -4,8 +4,11 @@ import ( "context" "errors" "fmt" + "os" "regexp" + "slices" "strings" + "time" "github.com/env0/terraform-provider-env0/client" "github.com/env0/terraform-provider-env0/client/http" @@ -81,7 +84,7 @@ func resourceEnvironment() *schema.Resource { "type": { Type: schema.TypeString, Description: "variable type (allowed values are: terraform, environment)", - Default: "environment", + Default: client.ENVIRONMENT, Optional: true, ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) { value := val.(string) @@ -369,6 +372,12 @@ func resourceEnvironment() *schema.Resource { Description: "variable set id", }, }, + "wait_for_destroy": { + Type: schema.TypeBool, + Description: "(Important note: this option is experimental, please report any issues found). During destroy, waits for the environment status to be 'INACTIVE'. Times out after 30 minutes.", + Default: false, + Optional: true, + }, }, CustomizeDiff: customdiff.ValidateChange("template_id", func(ctx context.Context, oldValue, newValue, meta interface{}) error { if oldValue != "" && oldValue != newValue { @@ -382,7 +391,7 @@ func resourceEnvironment() *schema.Resource { func setEnvironmentSchema(ctx context.Context, d *schema.ResourceData, environment client.Environment, configurationVariables client.ConfigurationChanges, variableSetsIds []string) error { if err := writeResourceData(&environment, d); err != nil { - return fmt.Errorf("schema resource data serialization failed: %v", err) + return fmt.Errorf("schema resource data serialization failed: %w", err) } //lint:ignore SA1019 reason: https://github.com/hashicorp/terraform-plugin-sdk/issues/817 @@ -530,7 +539,7 @@ func validateTemplateProjectAssignment(d *schema.ResourceData, apiClient client. template, err := apiClient.Template(templateId) if err != nil { - return fmt.Errorf("could not get template: %v", err) + return fmt.Errorf("could not get template: %w", err) } if projectId != template.ProjectId && !stringInSlice(projectId, template.ProjectIds) { @@ -559,16 +568,20 @@ func resourceEnvironmentCreate(ctx context.Context, d *schema.ResourceData, meta if err := validateTemplateProjectAssignment(d, apiClient); err != nil { return diag.Errorf("%v\n", err) } + environment, err = apiClient.EnvironmentCreate(environmentPayload) } else { templatePayload, createTemPayloadErr := templateCreatePayloadFromParameters("without_template_settings.0", d) + if createTemPayloadErr != nil { return createTemPayloadErr } + payload := client.EnvironmentCreateWithoutTemplate{ EnvironmentCreate: environmentPayload, TemplateCreate: templatePayload, } + // Note: the blueprint id field of the environment is returned only during creation of a template without environment. // Afterward, it will be omitted from future response. // setEnvironmentSchema() (several lines below) sets the blueprint id in the resource (under "without_template_settings.0.id"). @@ -876,7 +889,7 @@ func resourceEnvironmentDelete(ctx context.Context, d *schema.ResourceData, meta return diag.Errorf(`must enable "force_destroy" safeguard in order to destroy`) } - _, err := apiClient.EnvironmentDestroy(d.Id()) + res, err := apiClient.EnvironmentDestroy(d.Id()) if err != nil { if frerr, ok := err.(*http.FailedResponseError); ok && frerr.BadRequest() { tflog.Warn(ctx, "Could not delete environment. Already deleted?", map[string]interface{}{"id": d.Id(), "error": frerr.Error()}) @@ -884,9 +897,65 @@ func resourceEnvironmentDelete(ctx context.Context, d *schema.ResourceData, meta } return diag.Errorf("could not delete environment: %v", err) } + + if d.Get("wait_for_destroy").(bool) { + if err := waitForEnvironmentDestroy(ctx, apiClient, res.Id); err != nil { + return diag.FromErr(err) + } + } + return nil } +func waitForEnvironmentDestroy(ctx context.Context, apiClient client.ApiClientInterface, deploymentId string) error { + waitInteval := time.Second * 10 + timeout := time.Minute * 30 + + if os.Getenv("TF_ACC") == "1" { // For acceptance tests reducing interval to 1 second and timeout to 10 seconds. + waitInteval = time.Second + timeout = time.Second * 10 + } + + ticker := time.NewTicker(waitInteval) // When invoked - check the status. + timer := time.NewTimer(timeout) // When invoked - timeout. + results := make(chan error) + + go func() { + for { + deployment, err := apiClient.EnvironmentDeploymentLog(deploymentId) + if err != nil { + results <- fmt.Errorf("failed to get environment deployment '%s': %w", deploymentId, err) + return + } + + if slices.Contains([]string{"TIMEOUT", "FAILURE", "CANCELLED", "INTERNAL_FAILURE", "ABORTING", "ABORTED", "SKIPPED", "NEVER_DEPLOYED"}, deployment.Status) { + results <- fmt.Errorf("failed to wait for environment destroy to complete, deployment status is: %s", deployment.Status) + return + } + + if deployment.Status == "SUCCESS" { + results <- nil + return + } + + tflog.Info(ctx, "current 'destroy' deployment status", map[string]interface{}{"deploymentId": deploymentId, "status": deployment.Status}) + if deployment.Status == "WAITING_FOR_USER" { + tflog.Warn(ctx, "waiting for user approval (Env0 UI) to proceed with 'destroy' deployment") + } + + select { + case <-timer.C: + results <- fmt.Errorf("timeout! last 'destroy' deployment status was '%s'", deployment.Status) + return + case <-ticker.C: + continue + } + } + }() + + return <-results +} + func getCreatePayload(d *schema.ResourceData, apiClient client.ApiClientInterface) (client.EnvironmentCreate, diag.Diagnostics) { var payload client.EnvironmentCreate @@ -1148,13 +1217,16 @@ func getDeployPayload(d *schema.ResourceData, apiClient client.ApiClientInterfac if configuration, ok := d.GetOk("configuration"); ok && isRedeploy { configurationChanges := getConfigurationVariablesFromSchema(configuration.([]interface{})) scope := client.ScopeEnvironment + if _, ok := d.GetOk("sub_environment_configuration"); ok { scope = client.ScopeWorkflow } + configurationChanges, err = getUpdateConfigurationVariables(configurationChanges, d.Get("id").(string), scope, apiClient) if err != nil { return client.DeployRequest{}, err } + payload.ConfigurationChanges = &configurationChanges } @@ -1190,6 +1262,7 @@ func getUpdateConfigurationVariables(configurationChanges client.ConfigurationCh if err != nil { return client.ConfigurationChanges{}, fmt.Errorf("could not get environment configuration variables: %w", err) } + configurationChanges = linkToExistConfigurationVariables(configurationChanges, existVariables) configurationChanges = deleteUnusedConfigurationVariables(configurationChanges, existVariables) @@ -1279,6 +1352,7 @@ func resourceEnvironmentImport(ctx context.Context, d *schema.ResourceData, meta var getErr diag.Diagnostics var environment client.Environment + _, err := uuid.Parse(id) if err == nil { tflog.Info(ctx, "Resolving environment by id", map[string]interface{}{"id": id}) @@ -1304,12 +1378,12 @@ func resourceEnvironmentImport(ctx context.Context, d *schema.ResourceData, meta environmentConfigurationVariables, err := apiClient.ConfigurationVariablesByScope(scope, environment.Id) if err != nil { - return nil, fmt.Errorf("could not get environment configuration variables: %v", err) + return nil, fmt.Errorf("could not get environment configuration variables: %w", err) } environmentVariableSetIds, err := getEnvironmentVariableSetIdsFromApi(d, apiClient) if err != nil { - return nil, fmt.Errorf("could not get environment variable sets: %v", err) + return nil, fmt.Errorf("could not get environment variable sets: %w", err) } d.Set("deployment_id", environment.LatestDeploymentLogId) @@ -1348,6 +1422,7 @@ func resourceEnvironmentImport(ctx context.Context, d *schema.ResourceData, meta } d.Set("force_destroy", false) + d.Set("wait_for_destroy", false) d.Set("removal_strategy", "destroy") d.Set("vcs_pr_comments_enabled", environment.VcsCommandsAlias != "" || environment.VcsPrCommentsEnabled) diff --git a/env0/resource_environment_output_configuration_variable_test.go b/env0/resource_environment_output_configuration_variable_test.go index 1137e172..da1561aa 100644 --- a/env0/resource_environment_output_configuration_variable_test.go +++ b/env0/resource_environment_output_configuration_variable_test.go @@ -53,7 +53,6 @@ func TestUnitEnvironmentOutputConfigurationVariableResource(t *testing.T) { updatedConfigurationVariable.Value = updatedValueStr t.Run("create and update", func(t *testing.T) { - testCase := resource.TestCase{ Steps: []resource.TestStep{ { diff --git a/env0/resource_environment_scheduling_test.go b/env0/resource_environment_scheduling_test.go index 98e1e596..7739061a 100644 --- a/env0/resource_environment_scheduling_test.go +++ b/env0/resource_environment_scheduling_test.go @@ -1,7 +1,6 @@ package env0 import ( - "fmt" "regexp" "testing" @@ -47,7 +46,7 @@ func TestUnitEnvironmentSchedulingResource(t *testing.T) { }) for _, key := range cronExprKeys { - t.Run(fmt.Sprintf("Failure due to invalid cron expression for %s", key), func(t *testing.T) { + t.Run("Failure due to invalid cron expression for "+key, func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ { diff --git a/env0/resource_environment_test.go b/env0/resource_environment_test.go index 47a8a262..3064bd85 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -804,6 +804,182 @@ func TestUnitEnvironmentResource(t *testing.T) { }) }) + t.Run("wait for destroy", func(t *testing.T) { + templateId := "template-id" + + environment := client.Environment{ + Id: uuid.New().String(), + Name: "name", + ProjectId: "project-id", + LatestDeploymentLog: client.DeploymentLog{ + BlueprintId: templateId, + }, + } + + environmentCreate := client.EnvironmentCreate{ + Name: environment.Name, + ProjectId: environment.ProjectId, + + DeployRequest: &client.DeployRequest{ + BlueprintId: templateId, + }, + } + + deploymentLog := client.DeploymentLog{ + Id: "id12345_deployment", + } + + destroyResponse := &client.EnvironmentDestroyResponse{ + Id: deploymentLog.Id, + } + + deploymentWithStatus := func(status string) *client.DeploymentLog { + newDeployment := deploymentLog + newDeployment.Status = status + return &newDeployment + } + + config := resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": templateId, + "wait_for_destroy": true, + "force_destroy": true, + }) + + check := resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environment.Id), + resource.TestCheckResourceAttr(accessor, "name", environment.Name), + resource.TestCheckResourceAttr(accessor, "project_id", environment.ProjectId), + resource.TestCheckResourceAttr(accessor, "template_id", templateId), + ) + + t.Run("becomes inactive", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(environmentCreate).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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("QUEUED"), nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("IN_PROGRESS"), nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("SUCCESS"), nil), + ) + }) + }) + + t.Run("destroy fails", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + Config: config, + Destroy: true, + ExpectError: regexp.MustCompile("failed to wait for environment destroy to complete, deployment status is: CANCELLED"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(environmentCreate).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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, 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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("CANCELLED"), nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("SUCCESS"), nil), + ) + }) + }) + + t.Run("get deployment failed", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + Config: config, + Destroy: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("failed to get environment deployment '%s': error", deploymentLog.Id)), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(environmentCreate).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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, 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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(nil, errors.New("error")), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("SUCCESS"), nil), + ) + }) + }) + + t.Run("timeout", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + Config: config, + Destroy: true, + ExpectError: regexp.MustCompile("timeout! last 'destroy' deployment status was 'IN_PROGRESS'"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(environmentCreate).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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, 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().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("QUEUED"), nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).AnyTimes().Return(deploymentWithStatus("IN_PROGRESS"), nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(destroyResponse, nil), + mock.EXPECT().EnvironmentDeploymentLog(deploymentLog.Id).Times(1).Return(deploymentWithStatus("SUCCESS"), nil), + ) + }) + }) + }) + t.Run("Mark as archived", func(t *testing.T) { environment := client.Environment{ Id: uuid.New().String(), @@ -1257,6 +1433,7 @@ func TestUnitEnvironmentResource(t *testing.T) { configurationVariables.IsReadOnly = boolPtr(false) configurationVariables.IsRequired = boolPtr(false) configurationVariables.Value = configurationVariables.Schema.Enum[0] + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(templateInSlice, nil) mock.EXPECT().EnvironmentCreate(client.EnvironmentCreate{ Name: environment.Name, @@ -1272,6 +1449,7 @@ func TestUnitEnvironmentResource(t *testing.T) { varTrue := true configurationVariables.ToDelete = &varTrue + mock.EXPECT().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(4).Return(nil, nil) gomock.InOrder( mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, updatedEnvironment.Id).Times(3).Return(client.ConfigurationChanges{configurationVariables}, nil), // read after create -> on update @@ -2392,7 +2570,7 @@ func TestUnitEnvironmentResource(t *testing.T) { mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil) mock.EXPECT().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil) mock.EXPECT().Environment(gomock.Any()).Times(2).Return(environment, nil) - mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(environment, http.NewMockFailedResponseError(400)) + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(nil, http.NewMockFailedResponseError(400)) }) }) @@ -2675,7 +2853,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { } runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { - gomock.InOrder( // Step1 // Create @@ -2750,7 +2927,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { } runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { - gomock.InOrder( // Create mock.EXPECT().EnvironmentCreateWithoutTemplate(createPayload).Times(1).Return(environmentWithBluePrint, nil), diff --git a/env0/resource_gcp_credentials_test.go b/env0/resource_gcp_credentials_test.go index fe8a35eb..bd572729 100644 --- a/env0/resource_gcp_credentials_test.go +++ b/env0/resource_gcp_credentials_test.go @@ -13,7 +13,6 @@ import ( ) func TestUnitGcpCredentialsResource(t *testing.T) { - resourceType := "env0_gcp_credentials" resourceName := "test" resourceNameImport := resourceType + "." + resourceName diff --git a/env0/resource_notification.go b/env0/resource_notification.go index 9b946e1a..0cfe94ef 100644 --- a/env0/resource_notification.go +++ b/env0/resource_notification.go @@ -63,11 +63,13 @@ func getNotificationById(id string, meta interface{}) (*client.Notification, err if err != nil { return nil, err } + for _, notification := range notifications { if notification.Id == id { return ¬ification, nil } } + return nil, nil } diff --git a/env0/resource_notification_project_assignment.go b/env0/resource_notification_project_assignment.go index d439fe05..e2bded16 100644 --- a/env0/resource_notification_project_assignment.go +++ b/env0/resource_notification_project_assignment.go @@ -77,6 +77,7 @@ func resourceNotificationProjectAssignmentCreateOrUpdate(ctx context.Context, d if err != nil { return diag.Errorf("could not create or update notification project assignment: %v", err) } + d.SetId(assignment.Id) return nil diff --git a/env0/resource_template.go b/env0/resource_template.go index 17b0fe27..8cae780d 100644 --- a/env0/resource_template.go +++ b/env0/resource_template.go @@ -482,6 +482,7 @@ func templateReadRetryOnHelper(prefix string, d *schema.ResourceData, retryType value["retries_on_"+retryType] = 0 value["retry_on_"+retryType+"_only_when_matches_regex"] = "" } + d.Set(prefix, []interface{}{value}) } else { if retryOn != nil { diff --git a/env0/resource_template_test.go b/env0/resource_template_test.go index 3f7832fb..28441bd5 100644 --- a/env0/resource_template_test.go +++ b/env0/resource_template_test.go @@ -761,6 +761,7 @@ func TestUnitTemplateResource(t *testing.T) { }) }) } + t.Run("Basic template", func(t *testing.T) { template := client.Template{ Id: "id0", @@ -956,6 +957,7 @@ func TestUnitTemplateResource(t *testing.T) { for _, testCase := range testCases { tc := testCase + t.Run("Invalid retry times field", func(t *testing.T) { runUnitTest(t, tc, func(mockFunc *client.MockApiClientInterface) {}) }) @@ -982,6 +984,7 @@ func TestUnitTemplateResource(t *testing.T) { for _, testCase := range testCases { tc := testCase + t.Run("Invalid retry regex field", func(t *testing.T) { runUnitTest(t, tc, func(mockFunc *client.MockApiClientInterface) {}) }) diff --git a/env0/resource_user_organization_assignment.go b/env0/resource_user_organization_assignment.go index 0c86f051..74a048d9 100644 --- a/env0/resource_user_organization_assignment.go +++ b/env0/resource_user_organization_assignment.go @@ -70,7 +70,6 @@ func resourceUserOrganizationAssignmentRead(ctx context.Context, d *schema.Resou tflog.Warn(ctx, "Drift Detected: Terraform will remove id from state", map[string]interface{}{"id": d.Id()}) d.SetId("") return nil - } if isBuiltinOrganizationRole(user.Role) { diff --git a/env0/resource_user_team_assignment.go b/env0/resource_user_team_assignment.go index c86390cb..b2013f57 100644 --- a/env0/resource_user_team_assignment.go +++ b/env0/resource_user_team_assignment.go @@ -91,6 +91,7 @@ func resourceUserTeamAssignmentCreate(ctx context.Context, d *schema.ResourceDat if user.UserId == newAssignment.UserId { return diag.Errorf("assignment for user id %v and team id %v already exist", newAssignment.UserId, newAssignment.TeamId) } + userIds = append(userIds, user.UserId) } @@ -166,6 +167,7 @@ func resourceUserTeamAssignmentDelete(ctx context.Context, d *schema.ResourceDat if user.UserId == assignment.UserId { continue } + userIds = append(userIds, user.UserId) } diff --git a/env0/resource_variable_set.go b/env0/resource_variable_set.go index 9930784a..b3aa889d 100644 --- a/env0/resource_variable_set.go +++ b/env0/resource_variable_set.go @@ -305,6 +305,7 @@ func mergeVariables(schema []client.ConfigurationVariable, api []client.Configur // Sensitive - to avoid drift use the value from the schema avariable.Value = svariable.Value } + res.currentVariables = append(res.currentVariables, avariable) break diff --git a/env0/resource_workflow_triggers.go b/env0/resource_workflow_triggers.go index 236c9b73..d0c035d2 100644 --- a/env0/resource_workflow_triggers.go +++ b/env0/resource_workflow_triggers.go @@ -47,7 +47,7 @@ func resourceWorkflowTriggersRead(ctx context.Context, d *schema.ResourceData, m return diag.Errorf("could not get workflow triggers: %v", err) } - var triggerIds []string + triggerIds := []string{} for _, value := range triggers { triggerIds = append(triggerIds, value.Id) } @@ -62,7 +62,7 @@ func resourceWorkflowTriggersCreateOrUpdate(ctx context.Context, d *schema.Resou environmentId := d.Get("environment_id").(string) rawDownstreamIds := d.Get("downstream_environment_ids").([]interface{}) - var requestDownstreamIds []string + requestDownstreamIds := []string{} for _, rawId := range rawDownstreamIds { requestDownstreamIds = append(requestDownstreamIds, rawId.(string)) @@ -75,7 +75,7 @@ func resourceWorkflowTriggersCreateOrUpdate(ctx context.Context, d *schema.Resou return diag.Errorf("could not Create or Update workflow triggers: %v", err) } - var downstreamIds []string + downstreamIds := []string{} for _, trigger := range triggers { downstreamIds = append(downstreamIds, trigger.Id) } diff --git a/tests/integration/012_environment/main.tf b/tests/integration/012_environment/main.tf index f0604ef1..8b783ec4 100644 --- a/tests/integration/012_environment/main.tf +++ b/tests/integration/012_environment/main.tf @@ -49,6 +49,7 @@ resource "env0_environment" "auto_glob_envrironment" { approve_plan_automatically = true deploy_on_push = true force_destroy = true + wait_for_destroy = true } resource "env0_environment" "example" {