Skip to content

Commit

Permalink
Feat: add support for drift detection creation for environment resour…
Browse files Browse the repository at this point in the history
…ce (#665)
  • Loading branch information
TomerHeber authored Jun 29, 2023
1 parent 423084d commit 3fc65e1
Show file tree
Hide file tree
Showing 5 changed files with 249 additions and 16 deletions.
36 changes: 21 additions & 15 deletions client/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ type DeploymentLog struct {
WorkflowFile *WorkflowFile `json:"workflowFile,omitempty" tfschema:"-"`
}

type DriftDetectionRequest struct {
Enabled bool `json:"enabled"`
Cron string `json:"cron"`
}

type Environment struct {
Id string `json:"id"`
Name string `json:"name"`
Expand All @@ -116,21 +121,22 @@ type Environment struct {
}

type EnvironmentCreate struct {
Name string `json:"name"`
ProjectId string `json:"projectId"`
DeployRequest *DeployRequest `json:"deployRequest" tfschema:"-"`
WorkspaceName string `json:"workspaceName,omitempty" tfschema:"workspace"`
RequiresApproval *bool `json:"requiresApproval,omitempty" tfschema:"-"`
ContinuousDeployment *bool `json:"continuousDeployment,omitempty" tfschema:"-"`
PullRequestPlanDeployments *bool `json:"pullRequestPlanDeployments,omitempty" tfschema:"-"`
AutoDeployOnPathChangesOnly *bool `json:"autoDeployOnPathChangesOnly,omitempty" tfchema:"-"`
AutoDeployByCustomGlob string `json:"autoDeployByCustomGlob,omitempty"`
ConfigurationChanges *ConfigurationChanges `json:"configurationChanges,omitempty" tfschema:"-"`
TTL *TTL `json:"ttl,omitempty" tfschema:"-"`
TerragruntWorkingDirectory string `json:"terragruntWorkingDirectory,omitempty"`
VcsCommandsAlias string `json:"vcsCommandsAlias"`
IsRemoteBackend *bool `json:"isRemoteBackend,omitempty" tfschema:"-"`
Type string `json:"type,omitempty"`
Name string `json:"name"`
ProjectId string `json:"projectId"`
DeployRequest *DeployRequest `json:"deployRequest" tfschema:"-"`
WorkspaceName string `json:"workspaceName,omitempty" tfschema:"workspace"`
RequiresApproval *bool `json:"requiresApproval,omitempty" tfschema:"-"`
ContinuousDeployment *bool `json:"continuousDeployment,omitempty" tfschema:"-"`
PullRequestPlanDeployments *bool `json:"pullRequestPlanDeployments,omitempty" tfschema:"-"`
AutoDeployOnPathChangesOnly *bool `json:"autoDeployOnPathChangesOnly,omitempty" tfchema:"-"`
AutoDeployByCustomGlob string `json:"autoDeployByCustomGlob,omitempty"`
ConfigurationChanges *ConfigurationChanges `json:"configurationChanges,omitempty" tfschema:"-"`
TTL *TTL `json:"ttl,omitempty" tfschema:"-"`
TerragruntWorkingDirectory string `json:"terragruntWorkingDirectory,omitempty"`
VcsCommandsAlias string `json:"vcsCommandsAlias"`
IsRemoteBackend *bool `json:"isRemoteBackend,omitempty" tfschema:"-"`
Type string `json:"type,omitempty"`
DriftDetectionRequest *DriftDetectionRequest `json:"driftDetectionRequest,omitempty" tfschema:"-"`
}

// When converted to JSON needs to be flattened. See custom MarshalJSON below.
Expand Down
2 changes: 2 additions & 0 deletions env0/resource_drift_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ func resourceDriftDetection() *schema.Resource {
UpdateContext: resourceEnvironmentDriftCreateOrUpdate,
DeleteContext: resourceEnvironmentDriftDelete,

Description: "note: instead of using this resource, setting drift detection can be configured directly through the environment resource",

Schema: map[string]*schema.Schema{
"environment_id": {
Type: schema.TypeString,
Expand Down
42 changes: 41 additions & 1 deletion env0/resource_environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ func resourceEnvironment() *schema.Resource {
},
},
},
"drift_detection_cron": {
Type: schema.TypeString,
Description: "cron expression for scheduled drift detection of the environment (cannot be used with resource_drift_detection resource)",
Optional: true,
ValidateDiagFunc: ValidateCronExpression,
},
},

CustomizeDiff: customdiff.ForceNewIf("template_id", func(ctx context.Context, d *schema.ResourceDiff, meta interface{}) bool {
Expand Down Expand Up @@ -598,12 +604,17 @@ func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, meta
}

if shouldUpdateTTL(d) {

if err := updateTTL(d, apiClient); err != nil {
return err
}
}

if shouldUpdateDriftDetection(d) {
if err := updateDriftDetection(d, apiClient); err != nil {
return err
}
}

if shouldUpdateTemplate(d) {
if err := updateTemplate(d, apiClient); err != nil {
return err
Expand Down Expand Up @@ -635,6 +646,10 @@ func shouldUpdateTTL(d *schema.ResourceData) bool {
return d.HasChange("ttl")
}

func shouldUpdateDriftDetection(d *schema.ResourceData) bool {
return d.HasChange("drift_detection_cron")
}

func updateTemplate(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Diagnostics {
payload, problem := templateCreatePayloadFromParameters("without_template_settings.0", d)
if problem != nil {
Expand All @@ -650,6 +665,24 @@ func updateTemplate(d *schema.ResourceData, apiClient client.ApiClientInterface)
return nil
}

func updateDriftDetection(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Diagnostics {
drift_detection_cron, ok := d.GetOk("drift_detection_cron")
if !ok || drift_detection_cron.(string) == "" {
if err := apiClient.EnvironmentStopDriftDetection(d.Id()); err != nil {
return diag.Errorf("could not stop drift detection: %v", err)
}
} else {
if _, err := apiClient.EnvironmentUpdateDriftDetection(d.Id(), client.EnvironmentSchedulingExpression{
Enabled: true,
Cron: drift_detection_cron.(string),
}); err != nil {
return diag.Errorf("could not update drift detection: %v", err)
}
}

return nil
}

func deploy(d *schema.ResourceData, apiClient client.ApiClientInterface) diag.Diagnostics {
deployPayload := getDeployPayload(d, apiClient, true)

Expand Down Expand Up @@ -772,6 +805,13 @@ func getCreatePayload(d *schema.ResourceData, apiClient client.ApiClientInterfac
payload.TTL = &ttlPayload
}

if drift_detection_cron, ok := d.GetOk("drift_detection_cron"); ok && drift_detection_cron.(string) != "" {
payload.DriftDetectionRequest = &client.DriftDetectionRequest{
Enabled: true,
Cron: drift_detection_cron.(string),
}
}

deployPayload := getDeployPayload(d, apiClient, false)

subEnvironments, err := getSubEnvironments(d)
Expand Down
184 changes: 184 additions & 0 deletions env0/resource_environment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ func TestUnitEnvironmentResource(t *testing.T) {
isRemoteBackendTrue := true
isRemoteBackendFalse := false

driftDetectionCron := "*/5 * * * *"
updatedDriftDetectionCron := "*/10 1 * * *"

environment := client.Environment{
Id: "id0",
Name: "my-environment",
Expand Down Expand Up @@ -82,6 +85,27 @@ func TestUnitEnvironmentResource(t *testing.T) {
return resourceConfigCreate(resourceType, resourceName, config)
}

createEnvironmentResourceConfigDriftDetection := func(environment client.Environment, cron string) string {
config := map[string]interface{}{
"name": environment.Name,
"project_id": environment.ProjectId,
"template_id": environment.LatestDeploymentLog.BlueprintId,
"workspace": environment.WorkspaceName,
"terragrunt_working_directory": environment.TerragruntWorkingDirectory,
"force_destroy": true,
"vcs_commands_alias": environment.VcsCommandsAlias,
"is_remote_backend": *(environment.IsRemoteBackend),
}

if environment.IsArchived != nil {
config["is_inactive"] = *(environment.IsArchived)
}

config["drift_detection_cron"] = cron

return resourceConfigCreate(resourceType, resourceName, config)
}

autoDeployByCustomGlobDefault := ""

testSuccess := func() {
Expand Down Expand Up @@ -158,6 +182,166 @@ func TestUnitEnvironmentResource(t *testing.T) {
})
})

t.Run("Success create and remove drift cron", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: createEnvironmentResourceConfigDriftDetection(environment, driftDetectionCron),
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, "workspace", environment.WorkspaceName),
resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", environment.TerragruntWorkingDirectory),
resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", environment.VcsCommandsAlias),
resource.TestCheckResourceAttr(accessor, "revision", environment.LatestDeploymentLog.BlueprintRevision),
resource.TestCheckResourceAttr(accessor, "is_remote_backend", "false"),
resource.TestCheckResourceAttr(accessor, "output", string(updatedEnvironment.LatestDeploymentLog.Output)),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", driftDetectionCron),
resource.TestCheckNoResourceAttr(accessor, "deploy_on_push"),
resource.TestCheckNoResourceAttr(accessor, "run_plan_on_pull_requests"),
),
},
{
Config: createEnvironmentResourceConfig(updatedEnvironment),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "id", updatedEnvironment.Id),
resource.TestCheckResourceAttr(accessor, "name", updatedEnvironment.Name),
resource.TestCheckResourceAttr(accessor, "project_id", updatedEnvironment.ProjectId),
resource.TestCheckResourceAttr(accessor, "template_id", templateId),
resource.TestCheckResourceAttr(accessor, "workspace", environment.WorkspaceName),
resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", updatedEnvironment.TerragruntWorkingDirectory),
resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", updatedEnvironment.VcsCommandsAlias),
resource.TestCheckResourceAttr(accessor, "revision", updatedEnvironment.LatestDeploymentLog.BlueprintRevision),
resource.TestCheckResourceAttr(accessor, "is_remote_backend", "true"),
resource.TestCheckResourceAttr(accessor, "output", string(updatedEnvironment.LatestDeploymentLog.Output)),
resource.TestCheckResourceAttr(accessor, "is_inactive", "true"),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", ""),
resource.TestCheckNoResourceAttr(accessor, "deploy_on_push"),
resource.TestCheckNoResourceAttr(accessor, "run_plan_on_pull_requests"),
),
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil)
mock.EXPECT().EnvironmentCreate(client.EnvironmentCreate{
Name: environment.Name,
ProjectId: environment.ProjectId,
WorkspaceName: environment.WorkspaceName,
AutoDeployByCustomGlob: autoDeployByCustomGlobDefault,
TerragruntWorkingDirectory: environment.TerragruntWorkingDirectory,
VcsCommandsAlias: environment.VcsCommandsAlias,
DeployRequest: &client.DeployRequest{
BlueprintId: templateId,
},
IsRemoteBackend: &isRemoteBackendFalse,
DriftDetectionRequest: &client.DriftDetectionRequest{
Enabled: true,
Cron: driftDetectionCron,
},
}).Times(1).Return(environment, nil)
mock.EXPECT().EnvironmentUpdate(updatedEnvironment.Id, client.EnvironmentUpdate{
Name: updatedEnvironment.Name,
AutoDeployByCustomGlob: autoDeployByCustomGlobDefault,
TerragruntWorkingDirectory: updatedEnvironment.TerragruntWorkingDirectory,
VcsCommandsAlias: updatedEnvironment.VcsCommandsAlias,
IsRemoteBackend: &isRemoteBackendTrue,
IsArchived: updatedEnvironment.IsArchived,
}).Times(1).Return(updatedEnvironment, nil)
mock.EXPECT().EnvironmentStopDriftDetection(environment.Id).Times(1).Return(nil)
mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, updatedEnvironment.Id).Times(3).Return(client.ConfigurationChanges{}, nil)
gomock.InOrder(
mock.EXPECT().Environment(gomock.Any()).Times(2).Return(environment, nil), // 1 after create, 1 before update
mock.EXPECT().Environment(gomock.Any()).Times(1).Return(updatedEnvironment, nil), // 1 after update
)

mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1)
})
})

t.Run("Success create and update drift cron", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: createEnvironmentResourceConfigDriftDetection(environment, driftDetectionCron),
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, "workspace", environment.WorkspaceName),
resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", environment.TerragruntWorkingDirectory),
resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", environment.VcsCommandsAlias),
resource.TestCheckResourceAttr(accessor, "revision", environment.LatestDeploymentLog.BlueprintRevision),
resource.TestCheckResourceAttr(accessor, "is_remote_backend", "false"),
resource.TestCheckResourceAttr(accessor, "output", string(updatedEnvironment.LatestDeploymentLog.Output)),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", driftDetectionCron),
resource.TestCheckNoResourceAttr(accessor, "deploy_on_push"),
resource.TestCheckNoResourceAttr(accessor, "run_plan_on_pull_requests"),
),
},
{
Config: createEnvironmentResourceConfigDriftDetection(updatedEnvironment, updatedDriftDetectionCron),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "id", updatedEnvironment.Id),
resource.TestCheckResourceAttr(accessor, "name", updatedEnvironment.Name),
resource.TestCheckResourceAttr(accessor, "project_id", updatedEnvironment.ProjectId),
resource.TestCheckResourceAttr(accessor, "template_id", templateId),
resource.TestCheckResourceAttr(accessor, "workspace", environment.WorkspaceName),
resource.TestCheckResourceAttr(accessor, "terragrunt_working_directory", updatedEnvironment.TerragruntWorkingDirectory),
resource.TestCheckResourceAttr(accessor, "vcs_commands_alias", updatedEnvironment.VcsCommandsAlias),
resource.TestCheckResourceAttr(accessor, "revision", updatedEnvironment.LatestDeploymentLog.BlueprintRevision),
resource.TestCheckResourceAttr(accessor, "is_remote_backend", "true"),
resource.TestCheckResourceAttr(accessor, "output", string(updatedEnvironment.LatestDeploymentLog.Output)),
resource.TestCheckResourceAttr(accessor, "is_inactive", "true"),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", updatedDriftDetectionCron),
resource.TestCheckNoResourceAttr(accessor, "deploy_on_push"),
resource.TestCheckNoResourceAttr(accessor, "run_plan_on_pull_requests"),
),
},
},
}

runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) {
mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil)
mock.EXPECT().EnvironmentCreate(client.EnvironmentCreate{
Name: environment.Name,
ProjectId: environment.ProjectId,
WorkspaceName: environment.WorkspaceName,
AutoDeployByCustomGlob: autoDeployByCustomGlobDefault,
TerragruntWorkingDirectory: environment.TerragruntWorkingDirectory,
VcsCommandsAlias: environment.VcsCommandsAlias,
DeployRequest: &client.DeployRequest{
BlueprintId: templateId,
},
IsRemoteBackend: &isRemoteBackendFalse,
DriftDetectionRequest: &client.DriftDetectionRequest{
Enabled: true,
Cron: driftDetectionCron,
},
}).Times(1).Return(environment, nil)
mock.EXPECT().EnvironmentUpdate(updatedEnvironment.Id, client.EnvironmentUpdate{
Name: updatedEnvironment.Name,
AutoDeployByCustomGlob: autoDeployByCustomGlobDefault,
TerragruntWorkingDirectory: updatedEnvironment.TerragruntWorkingDirectory,
VcsCommandsAlias: updatedEnvironment.VcsCommandsAlias,
IsRemoteBackend: &isRemoteBackendTrue,
IsArchived: updatedEnvironment.IsArchived,
}).Times(1).Return(updatedEnvironment, nil)
mock.EXPECT().EnvironmentUpdateDriftDetection(environment.Id, client.EnvironmentSchedulingExpression{Cron: updatedDriftDetectionCron, Enabled: true}).Times(1).Return(client.EnvironmentSchedulingExpression{}, nil)
mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, updatedEnvironment.Id).Times(3).Return(client.ConfigurationChanges{}, nil)
gomock.InOrder(
mock.EXPECT().Environment(gomock.Any()).Times(2).Return(environment, nil), // 1 after create, 1 before update
mock.EXPECT().Environment(gomock.Any()).Times(1).Return(updatedEnvironment, nil), // 1 after update
)

mock.EXPECT().EnvironmentDestroy(environment.Id).Times(1)
})
})

t.Run("Success in create and deploy with variables update", func(t *testing.T) {
environment := client.Environment{
Id: "id0",
Expand Down
1 change: 1 addition & 0 deletions tests/integration/012_environment/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ resource "env0_environment" "example" {
approve_plan_automatically = true
revision = "master"
vcs_commands_alias = "alias"
drift_detection_cron = var.second_run ? "*/5 * * * *" : "*/10 * * * *"
}

resource "env0_custom_role" "custom_role1" {
Expand Down

0 comments on commit 3fc65e1

Please sign in to comment.