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_configuration_variable.go b/env0/data_configuration_variable.go index d1f7d0ad..f4acb41a 100644 --- a/env0/data_configuration_variable.go +++ b/env0/data_configuration_variable.go @@ -185,6 +185,7 @@ func getConfigurationVariable(params ConfigurationVariableParams, meta interface if err != nil { return client.ConfigurationVariable{}, diag.Errorf("Could not query variable: %v", err) } + return variable, nil } 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 5e385832..28fd1b80 100644 --- a/env0/data_environment.go +++ b/env0/data_environment.go @@ -104,6 +104,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) @@ -117,6 +118,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_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.go b/env0/data_project.go index 2d580ac5..48ae7f99 100644 --- a/env0/data_project.go +++ b/env0/data_project.go @@ -79,6 +79,7 @@ func dataProjectRead(ctx context.Context, d *schema.ResourceData, meta interface if !ok { return diag.Errorf("either 'name' or 'id' must be specified") } + project, err = getProjectByName(name.(string), d.Get("parent_project_id").(string), d.Get("parent_project_name").(string), meta) if err != nil { return diag.Errorf("%v", err) @@ -93,7 +94,8 @@ func dataProjectRead(ctx context.Context, d *schema.ResourceData, meta interface } func filterByParentProjectId(parentId string, projects []client.Project) []client.Project { - filteredProjects := make([]client.Project, 0) + filteredProjects := []client.Project{} + for _, project := range projects { if len(project.ParentProjectId) == 0 { continue @@ -134,12 +136,14 @@ func getProjectByName(name string, parentId string, parentName string, meta inte return client.Project{}, fmt.Errorf("could not query project by name: %w", err) } - projectsByName := make([]client.Project, 0) + projectsByName := []client.Project{} + for _, candidate := range projects { if candidate.Name == name && !candidate.IsArchived { projectsByName = append(projectsByName, candidate) } } + if len(parentId) > 0 { // Use parentId filter to reduce the results. projectsByName = filterByParentProjectId(parentId, projectsByName) @@ -164,12 +168,15 @@ func getProjectByName(name string, parentId string, parentName string, meta inte func getProjectById(id string, meta interface{}) (client.Project, error) { apiClient := meta.(client.ApiClientInterface) + project, err := apiClient.Project(id) if err != nil { if frerr, ok := err.(*http.FailedResponseError); ok && frerr.NotFound() { return client.Project{}, fmt.Errorf("could not find a project with id: %s", id) } + return client.Project{}, fmt.Errorf("could not query project: %w", err) } + return project, nil } 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/resource_agent_project_assignment.go b/env0/resource_agent_project_assignment.go index d464fba4..47468119 100644 --- a/env0/resource_agent_project_assignment.go +++ b/env0/resource_agent_project_assignment.go @@ -107,6 +107,7 @@ func resourceAgentProjectAssignmentRead(ctx context.Context, d *schema.ResourceD AgentId: agentId.(string), ProjectId: projectId, } + break } } @@ -180,6 +181,7 @@ func resourceAgentProjectAssignmentImport(ctx context.Context, d *schema.Resourc AgentId: agentId, ProjectId: projectId, } + break } } @@ -189,7 +191,7 @@ func resourceAgentProjectAssignmentImport(ctx context.Context, d *schema.Resourc } if err := writeResourceData(assignment, 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_agent_project_assignment_test.go b/env0/resource_agent_project_assignment_test.go index be41d7e8..a6ee2c5b 100644 --- a/env0/resource_agent_project_assignment_test.go +++ b/env0/resource_agent_project_assignment_test.go @@ -11,7 +11,6 @@ import ( ) func TestUnitAgentProjectAssignmentResource(t *testing.T) { - // helper functions that receives a variadic list of key value items and returns a ProjectsAgentsAssignments instance. GenerateProjectsAgentsAssignmentsMap := func(items ...string) map[string]interface{} { 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_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 458a2f42..381f8f83 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -4,7 +4,9 @@ import ( "context" "errors" "fmt" + "os" "regexp" + "time" "github.com/env0/terraform-provider-env0/client" "github.com/env0/terraform-provider-env0/client/http" @@ -368,6 +370,12 @@ func resourceEnvironment() *schema.Resource { Description: "variable set id", }, }, + "wait_for_destroy": { + Type: schema.TypeBool, + Description: "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 { @@ -381,7 +389,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 @@ -587,7 +595,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) { @@ -929,7 +937,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()) + environment, 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()}) @@ -937,9 +945,61 @@ func resourceEnvironmentDelete(ctx context.Context, d *schema.ResourceData, meta } return diag.Errorf("could not delete environment: %v", err) } + + if environment.Status != "INACTIVE" && d.Get("wait_for_destroy").(bool) { + if err := waitForEnvironmentDestroy(apiClient, &environment); err != nil { + return diag.FromErr(err) + } + } + return nil } +func waitForEnvironmentDestroy(apiClient client.ApiClientInterface, environment *client.Environment) error { + waitInteval := time.Second * 10 + timeout := time.Minute * 30 + if os.Getenv("TF_ACC") == "1" { // For acceptance tests. + waitInteval = time.Second + timeout = time.Second * 10 + } + + ticker := time.NewTicker(waitInteval) // When invoked - check if environment is inactive. + timer := time.NewTimer(timeout) // When invoked - time out + results := make(chan error) + + go func() { + for { + select { + case <-timer.C: + results <- fmt.Errorf("timed out! last environment status was '%s'", environment.Status) + return + case <-ticker.C: + latestEnvironment, err := apiClient.Environment(environment.Id) + if err != nil { + results <- fmt.Errorf("failed to get environment status: %w", err) + return + } + + environment = &latestEnvironment + + if environment.Status == "INACTIVE" { + results <- nil + return + } + + if environment.Status == "DESTROY_IN_PROGRESS" { + continue + } + + results <- fmt.Errorf("environment destroy failed with status '%s'", environment.Status) + return + } + } + }() + + return <-results +} + func getCreatePayload(d *schema.ResourceData, apiClient client.ApiClientInterface) (client.EnvironmentCreate, diag.Diagnostics) { var payload client.EnvironmentCreate @@ -1325,9 +1385,11 @@ func getConfigurationVariableFromSchema(variable map[string]interface{}) client. if variable["schema_type"] != "" && len(variable["schema_enum"].([]interface{})) > 0 { enumOfAny := variable["schema_enum"].([]interface{}) enum := make([]string, len(enumOfAny)) + for i := range enum { enum[i] = enumOfAny[i].(string) } + configurationSchema.Type = variable["schema_type"].(string) configurationSchema.Enum = enum } @@ -1450,6 +1512,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.go b/env0/resource_environment_output_configuration_variable.go index ae61fca1..5a26c996 100644 --- a/env0/resource_environment_output_configuration_variable.go +++ b/env0/resource_environment_output_configuration_variable.go @@ -121,11 +121,11 @@ func deserializeEnvironmentOutputConfigurationVariableValue(valueStr string) (*E } if value.OutputName == "" { - return nil, fmt.Errorf("after unmarshal 'outputName' is empty") + return nil, errors.New("after unmarshal 'outputName' is empty") } if value.EnvironmentId == "" && value.SubEnvironmentAlias == "" { - return nil, fmt.Errorf("after unmarshal both 'environmentId' and 'subEnvironmentAlias' are empty") + return nil, errors.New("after unmarshal both 'environmentId' and 'subEnvironmentAlias' are empty") } return &value, nil 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 2d7650f5..cc2ffa4f 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -804,6 +804,171 @@ 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, + }, + } + + environmentWithStatus := func(status string) client.Environment { + newEnvironment := environment + newEnvironment.Status = status + return newEnvironment + } + + 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(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(environmentWithStatus("INACTIVE"), 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("environment destroy failed with status 'FAILED'"), + }, + }, + } + + 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(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(environmentWithStatus("FAILED"), nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(environmentWithStatus("INACTIVE"), nil), + ) + }) + }) + + t.Run("get environment failed", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: config, + Check: check, + }, + { + Config: config, + Destroy: true, + ExpectError: regexp.MustCompile("failed to get environment status: error"), + }, + }, + } + + 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(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(client.Environment{}, errors.New("error")), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(environmentWithStatus("INACTIVE"), 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("timed out! last environment status was 'DESTROY_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(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().Environment(environment.Id).AnyTimes().Return(environmentWithStatus("DESTROY_IN_PROGRESS"), nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1).Return(environmentWithStatus("INACTIVE"), nil), + ) + }) + }) + }) + t.Run("Mark as archived", func(t *testing.T) { environment := client.Environment{ Id: uuid.New().String(), @@ -1257,6 +1422,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 +1438,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 @@ -2656,7 +2823,6 @@ func TestUnitEnvironmentWithoutTemplateResource(t *testing.T) { } runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { - gomock.InOrder( // Step1 // Create @@ -2731,7 +2897,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 c604c607..b3aa889d 100644 --- a/env0/resource_variable_set.go +++ b/env0/resource_variable_set.go @@ -300,11 +300,14 @@ func mergeVariables(schema []client.ConfigurationVariable, api []client.Configur for _, avariable := range api { if svariable.Name == avariable.Name && *svariable.Type == *avariable.Type { found = true + if avariable.IsSensitive != nil && *avariable.IsSensitive { // 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) }