From 88ff9b46523617aa2ae98a7b1821aa4b90c72ad7 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Mon, 4 Dec 2023 06:53:33 -0600 Subject: [PATCH] Feat: support 'Enable Remote Apply' flag on environment settings (#763) --- client/environment.go | 2 + env0/resource_custom_role.go | 1 + env0/resource_environment.go | 26 +++++- env0/resource_environment_test.go | 128 ++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 4 deletions(-) diff --git a/client/environment.go b/client/environment.go index 1b78e9d1..6c0bd56f 100644 --- a/client/environment.go +++ b/client/environment.go @@ -118,6 +118,7 @@ type Environment struct { BlueprintId string `json:"blueprintId" tfschema:"-"` IsRemoteBackend *bool `json:"isRemoteBackend" tfschema:"-"` IsArchived *bool `json:"isArchived" tfschema:"-"` + IsRemoteApplyEnabled bool `json:"isRemoteApplyEnabled"` } type EnvironmentCreate struct { @@ -186,6 +187,7 @@ type EnvironmentUpdate struct { AutoDeployOnPathChangesOnly *bool `json:"autoDeployOnPathChangesOnly,omitempty" tfschema:"-"` IsRemoteBackend *bool `json:"isRemoteBackend,omitempty" tfschema:"-"` IsArchived *bool `json:"isArchived,omitempty" tfschema:"-"` + IsRemoteApplyEnabled bool `json:"isRemoteApplyEnabled"` } type EnvironmentDeployResponse struct { diff --git a/env0/resource_custom_role.go b/env0/resource_custom_role.go index a68a8355..865b9b1a 100644 --- a/env0/resource_custom_role.go +++ b/env0/resource_custom_role.go @@ -45,6 +45,7 @@ func resourceCustomRole() *schema.Resource { "VIEW_PROVIDERS", "VIEW_ENVIRONMENT", "ASSIGN_ROLE_ON_ENVIRONMENT", + "EDIT_ALLOW_REMOTE_APPLY", } allowedCustomRoleTypesStr := fmt.Sprintf("(allowed values: %s)", strings.Join(allowedCustomRoleTypes, ", ")) diff --git a/env0/resource_environment.go b/env0/resource_environment.go index 499b9e79..9ef1c45b 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -323,6 +323,12 @@ func resourceEnvironment() *schema.Resource { Optional: true, ValidateDiagFunc: ValidateCronExpression, }, + "is_remote_apply_enabled": { + Type: schema.TypeBool, + Description: "enables remote apply when set to true (defaults to false). Can only be enabled when is_remote_backend and approve_plan_automatically are enabled. Can only enabled for an existing environment", + Optional: true, + Default: false, + }, }, CustomizeDiff: customdiff.ForceNewIf("template_id", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool { @@ -519,6 +525,11 @@ func validateTemplateProjectAssignment(d *schema.ResourceData, apiClient client. func resourceEnvironmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) + isRemoteApplyEnabled := d.Get("is_remote_apply_enabled").(bool) + if isRemoteApplyEnabled { + return diag.Errorf("is_remote_apply_enabled cannot be set when creating a new environment. Set this value after the environment is created") + } + environmentPayload, createEnvPayloadErr := getCreatePayload(d, apiClient) if createEnvPayloadErr != nil { return createEnvPayloadErr @@ -642,7 +653,7 @@ func shouldDeploy(d *schema.ResourceData) bool { } func shouldUpdate(d *schema.ResourceData) bool { - return d.HasChanges("name", "approve_plan_automatically", "deploy_on_push", "run_plan_on_pull_requests", "auto_deploy_by_custom_glob", "auto_deploy_on_path_changes_only", "terragrunt_working_directory", "vcs_commands_alias", "is_remote_backend", "is_inactive") + return d.HasChanges("name", "approve_plan_automatically", "deploy_on_push", "run_plan_on_pull_requests", "auto_deploy_by_custom_glob", "auto_deploy_on_path_changes_only", "terragrunt_working_directory", "vcs_commands_alias", "is_remote_backend", "is_inactive", "is_remote_apply_enabled") } func shouldUpdateTTL(d *schema.ResourceData) bool { @@ -798,7 +809,7 @@ func getCreatePayload(d *schema.ResourceData, apiClient client.ApiClientInterfac payload.IsRemoteBackend = boolPtr(val.(bool)) } - if err := assertDeploymentTriggers(d); err != nil { + if err := assertEnvironment(d); err != nil { return client.EnvironmentCreate{}, err } @@ -844,7 +855,7 @@ func getCreatePayload(d *schema.ResourceData, apiClient client.ApiClientInterfac return payload, nil } -func assertDeploymentTriggers(d *schema.ResourceData) diag.Diagnostics { +func assertEnvironment(d *schema.ResourceData) diag.Diagnostics { continuousDeployment := d.Get("deploy_on_push").(bool) pullRequestPlanDeployments := d.Get("run_plan_on_pull_requests").(bool) autoDeployOnPathChangesOnly := d.Get("auto_deploy_on_path_changes_only").(bool) @@ -859,6 +870,13 @@ func assertDeploymentTriggers(d *schema.ResourceData) diag.Diagnostics { } } + isRemoteApplyEnabled := d.Get("is_remote_apply_enabled").(bool) + isRemotedBackend := d.Get("is_remote_backend").(bool) + approvePlanAutomatically := d.Get("approve_plan_automatically").(bool) + if isRemoteApplyEnabled && (!isRemotedBackend || !approvePlanAutomatically) { + return diag.Errorf("cannot set is_remote_apply_enabled when approve_plan_automatically or is_remote_backend are disabled") + } + return nil } @@ -893,7 +911,7 @@ func getUpdatePayload(d *schema.ResourceData) (client.EnvironmentUpdate, diag.Di payload.IsArchived = boolPtr(d.Get("is_inactive").(bool)) } - if err := assertDeploymentTriggers(d); err != nil { + if err := assertEnvironment(d); err != nil { return client.EnvironmentUpdate{}, err } diff --git a/env0/resource_environment_test.go b/env0/resource_environment_test.go index 2eb677c6..19831cfb 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -185,6 +185,118 @@ func TestUnitEnvironmentResource(t *testing.T) { }) }) + t.Run("remote apply is enabled", func(t *testing.T) { + templateId := "template-id" + + environment := client.Environment{ + Id: uuid.New().String(), + Name: "name", + ProjectId: "project-id", + LatestDeploymentLog: client.DeploymentLog{ + BlueprintId: templateId, + }, + IsRemoteBackend: boolPtr(true), + RequiresApproval: boolPtr(false), + } + + updatedEnvironment := client.Environment{ + Id: environment.Id, + Name: "name", + ProjectId: "project-id", + LatestDeploymentLog: client.DeploymentLog{ + BlueprintId: templateId, + }, + IsRemoteApplyEnabled: true, + } + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": templateId, + "is_remote_backend": *environment.IsRemoteBackend, + "approve_plan_automatically": !*environment.RequiresApproval, + "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), + resource.TestCheckResourceAttr(accessor, "is_remote_backend", "true"), + resource.TestCheckResourceAttr(accessor, "approve_plan_automatically", "true"), + resource.TestCheckResourceAttr(accessor, "is_remote_apply_enabled", "false"), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": templateId, + "is_remote_backend": *environment.IsRemoteBackend, + "approve_plan_automatically": !*environment.RequiresApproval, + "force_destroy": true, + "is_remote_apply_enabled": 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), + resource.TestCheckResourceAttr(accessor, "is_remote_backend", "true"), + resource.TestCheckResourceAttr(accessor, "approve_plan_automatically", "true"), + resource.TestCheckResourceAttr(accessor, "is_remote_apply_enabled", "true"), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": templateId, + "is_remote_backend": !*environment.IsRemoteBackend, + "approve_plan_automatically": !*environment.RequiresApproval, + "force_destroy": true, + "is_remote_apply_enabled": true, + }), + ExpectError: regexp.MustCompile("cannot set is_remote_apply_enabled when approve_plan_automatically or is_remote_backend are disabled"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(client.EnvironmentCreate{ + Name: environment.Name, + ProjectId: environment.ProjectId, + + DeployRequest: &client.DeployRequest{ + BlueprintId: templateId, + }, + IsRemoteBackend: environment.IsRemoteBackend, + RequiresApproval: environment.RequiresApproval, + }).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().Environment(environment.Id).Times(1).Return(environment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().EnvironmentUpdate(updatedEnvironment.Id, client.EnvironmentUpdate{ + Name: updatedEnvironment.Name, + IsRemoteBackend: updatedEnvironment.IsRemoteBackend, + RequiresApproval: updatedEnvironment.RequiresApproval, + IsRemoteApplyEnabled: updatedEnvironment.IsRemoteApplyEnabled, + }).Times(1).Return(updatedEnvironment, nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(updatedEnvironment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().Environment(environment.Id).Times(1).Return(updatedEnvironment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1), + ) + }) + }) + t.Run("Import By Id", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ @@ -1391,6 +1503,22 @@ func TestUnitEnvironmentResource(t *testing.T) { } testValidationFailures := func() { + t.Run("create environment with is_remote_apply_enabled set to 'true'", func(t *testing.T) { + runUnitTest(t, resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": environment.LatestDeploymentLog.BlueprintId, + "is_remote_apply_enabled": true, + }), + ExpectError: regexp.MustCompile("is_remote_apply_enabled cannot be set when creating a new environment"), + }, + }, + }, func(mockFunc *client.MockApiClientInterface) {}) + }) + t.Run("Failure in validation while glob is enabled and pathChanges no", func(t *testing.T) { autoDeployWithCustomGlobEnabled := resource.TestCase{ Steps: []resource.TestStep{