Skip to content

Commit

Permalink
Feat: support 'Enable Remote Apply' flag on environment settings (#763)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerHeber authored Dec 4, 2023
1 parent 2ba84fb commit 88ff9b4
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 4 deletions.
2 changes: 2 additions & 0 deletions client/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions env0/resource_custom_role.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, ", "))
Expand Down
26 changes: 22 additions & 4 deletions env0/resource_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
Expand All @@ -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
}

Expand Down Expand Up @@ -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
}

Expand Down
128 changes: 128 additions & 0 deletions env0/resource_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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{
Expand Down

0 comments on commit 88ff9b4

Please sign in to comment.