Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: support 'Enable Remote Apply' flag on environment settings #763

Merged
merged 1 commit into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"`
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in golang, this defaults to false.
I don't need it to be a pointer because the schema defaults to 'false'.
(unfortunately, defaults were not used in the early days of the provider which caused a lot of challenges).

}

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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the new custom role.
the provider doesn't check for rbac. That's a backend feature.

}

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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is important...

},
},

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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_remote_apply_enabled cannot be set to true during environment creation.

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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_remote_apply_enabled may only be true when the others are true.

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{}{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

regular creation.

"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{}{
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated and set is_remote_apply_enabled to true.

"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,
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

making this false to test the error use-case.

"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(
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

alot of mocks... it's just how the provider works... /:

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) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

test error on creation when is_remote_apply_enabled is set to true.

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
Loading