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 Drift Remediation #1019

Merged
merged 4 commits into from
Mar 26, 2025
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
10 changes: 9 additions & 1 deletion client/environment_drift_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ var _ = Describe("EnvironmentDriftDetection", func() {
environmentId := "env"
path := "/scheduling/drift-detection/environments/" + environmentId
mockError := errors.New("I don't like milk")
schedulingExpression := EnvironmentSchedulingExpression{Cron: "0 * * * *", Enabled: true}
schedulingExpression := EnvironmentSchedulingExpression{
Cron: "0 * * * *",
Enabled: true,
AutoDriftRemediation: "CODE_TO_CLOUD",
}
var driftResponse EnvironmentSchedulingExpression

Describe("Get", func() {
Expand Down Expand Up @@ -63,6 +67,10 @@ var _ = Describe("EnvironmentDriftDetection", func() {
It("Should return the drift scheduling response", func() {
Expect(driftResponse).To(Equal(schedulingExpression))
})

It("Should include auto drift remediation in response", func() {
Expect(driftResponse.AutoDriftRemediation).To(Equal("CODE_TO_CLOUD"))
})
})

Describe("Fail", func() {
Expand Down
5 changes: 3 additions & 2 deletions client/environment_scheduling.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import (
)

type EnvironmentSchedulingExpression struct {
Cron string `json:"cron,omitempty"`
Enabled bool `json:"enabled"`
Cron string `json:"cron,omitempty"`
Enabled bool `json:"enabled"`
AutoDriftRemediation string `json:"autoDriftRemediation,omitempty"`
}

func (e *EnvironmentSchedulingExpression) ReadResourceData(fieldName string, d *schema.ResourceData) error {
Expand Down
2 changes: 2 additions & 0 deletions client/project_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Policy struct {
ForceRemoteBackend bool `json:"forceRemoteBackend"`
DriftDetectionCron string `json:"driftDetectionCron"`
DriftDetectionEnabled bool `json:"driftDetectionEnabled"`
AutoDriftRemediation string `json:"autoDriftRemediation"`
OutputsAsInputsEnabled bool `json:"outputsAsInputsEnabled"`
}

Expand All @@ -38,6 +39,7 @@ type PolicyUpdatePayload struct {
ForceRemoteBackend bool `json:"forceRemoteBackend"`
DriftDetectionCron string `json:"driftDetectionCron"`
DriftDetectionEnabled bool `json:"driftDetectionEnabled"`
AutoDriftRemediation string `json:"autoDriftRemediation,omitempty"`
VcsPrCommentsEnabledDefault bool `json:"vcsPrCommentsEnabledDefault"`
OutputsAsInputsEnabled bool `json:"outputsAsInputsEnabled"`
}
Expand Down
20 changes: 18 additions & 2 deletions client/project_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ const (

var _ = Describe("Policy", func() {
mockPolicy := Policy{
Id: policyId,
Id: policyId,
ProjectId: "project0",
DriftDetectionCron: "0 * * * *",
AutoDriftRemediation: "CODE_TO_CLOUD",
}

Describe("Policy", func() {
Expand Down Expand Up @@ -47,6 +50,10 @@ var _ = Describe("Policy", func() {
It("Should not return an error", func() {
Expect(err).Should(BeNil())
})

It("Should return policy with auto drift remediation", func() {
Expect(policy.AutoDriftRemediation).Should(Equal("CODE_TO_CLOUD"))
})
})

Describe("Failure", func() {
Expand All @@ -63,7 +70,12 @@ var _ = Describe("Policy", func() {
})

Describe("PolicyUpdate", func() {
updatePolicyPayload := PolicyUpdatePayload{ProjectId: "project0"}
updatePolicyPayload := PolicyUpdatePayload{
ProjectId: "project0",
DriftDetectionCron: "0 * * * *",
DriftDetectionEnabled: true,
AutoDriftRemediation: "CODE_TO_CLOUD",
}
Describe("Success", func() {
var updatedPolicy Policy
var err error
Expand All @@ -89,6 +101,10 @@ var _ = Describe("Policy", func() {
It("Should return team received from API", func() {
Expect(updatedPolicy).To(Equal(mockPolicy))
})

It("Should return policy with updated auto drift remediation", func() {
Expect(updatedPolicy.AutoDriftRemediation).To(Equal("CODE_TO_CLOUD"))
})
})

Describe("Failure", func() {
Expand Down
5 changes: 5 additions & 0 deletions env0/data_project_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ func dataPolicy() *schema.Resource {
Description: "force env0 remote backend",
Computed: true,
},
"auto_drift_remediation": {
Type: schema.TypeString,
Description: "Auto drift remediation setting (DISABLED or CODE_TO_CLOUD)",
Computed: true,
},
},
}
}
Expand Down
3 changes: 3 additions & 0 deletions env0/data_project_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func TestPolicyDataSource(t *testing.T) {
MaxTtl: stringPtr("3h"),
DefaultTtl: stringPtr("1h"),
ForceRemoteBackend: true,
DriftDetectionEnabled: true,
AutoDriftRemediation: "CODE_TO_CLOUD",
}

resourceType := "env0_project_policy"
Expand Down Expand Up @@ -50,6 +52,7 @@ func TestPolicyDataSource(t *testing.T) {
resource.TestCheckResourceAttr(accessor, "max_ttl", *policy.MaxTtl),
resource.TestCheckResourceAttr(accessor, "default_ttl", *policy.DefaultTtl),
resource.TestCheckResourceAttr(accessor, "force_remote_backend", strconv.FormatBool(policy.ForceRemoteBackend)),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", policy.AutoDriftRemediation),
),
},
},
Expand Down
22 changes: 21 additions & 1 deletion env0/resource_drift_detection.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

const (
DriftRemediationDisabled = "DISABLED"
DriftRemediationCodeToCloud = "CODE_TO_CLOUD"
)

func resourceDriftDetection() *schema.Resource {
return &schema.Resource{
CreateContext: resourceEnvironmentDriftCreateOrUpdate,
Expand All @@ -31,6 +36,16 @@ func resourceDriftDetection() *schema.Resource {
Required: true,
ValidateDiagFunc: ValidateCronExpression,
},
"auto_drift_remediation": {
Type: schema.TypeString,
Description: "Auto drift remediation setting (DISABLED or CODE_TO_CLOUD). Defaults to DISABLED",
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

note: default is "DISABLED.

Optional: true,
Default: DriftRemediationDisabled,
ValidateDiagFunc: NewStringInValidator([]string{
DriftRemediationDisabled,
DriftRemediationCodeToCloud,
}),
},
},
}
}
Expand Down Expand Up @@ -65,8 +80,13 @@ func resourceEnvironmentDriftCreateOrUpdate(ctx context.Context, d *schema.Resou

environmentId := d.Get("environment_id").(string)
cron := d.Get("cron").(string)
autoDriftRemediation := d.Get("auto_drift_remediation").(string)

payload := client.EnvironmentSchedulingExpression{Cron: cron, Enabled: true}
payload := client.EnvironmentSchedulingExpression{
Cron: cron,
Enabled: true,
AutoDriftRemediation: autoDriftRemediation,
}

if _, err := apiClient.EnvironmentUpdateDriftDetection(environmentId, payload); err != nil {
return diag.Errorf("could not create or update environment drift detection: %v", err)
Expand Down
31 changes: 23 additions & 8 deletions env0/resource_drift_detection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,31 @@ func TestUnitEnvironmentDriftDetectionResource(t *testing.T) {
resourceType := "env0_environment_drift_detection"
resourceName := "test"
accessor := resourceAccessor(resourceType, resourceName)
drift := client.EnvironmentSchedulingExpression{Cron: "2 * * * *", Enabled: true}
updateDrift := client.EnvironmentSchedulingExpression{Cron: "2 2 * * *", Enabled: true}
drift := client.EnvironmentSchedulingExpression{
Cron: "2 * * * *",
Enabled: true,
AutoDriftRemediation: "CODE_TO_CLOUD",
}
updateDrift := client.EnvironmentSchedulingExpression{
Cron: "2 2 * * *",
Enabled: true,
AutoDriftRemediation: "DISABLED",
}
autoDriftRemediation := "CODE_TO_CLOUD"

t.Run("Success", func(t *testing.T) {
testCase := resource.TestCase{
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
"environment_id": environmentId,
"cron": drift.Cron,
"environment_id": environmentId,
"cron": drift.Cron,
"auto_drift_remediation": autoDriftRemediation,
}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "environment_id", environmentId),
resource.TestCheckResourceAttr(accessor, "cron", drift.Cron),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", autoDriftRemediation),
),
},
},
Expand All @@ -43,22 +54,26 @@ func TestUnitEnvironmentDriftDetectionResource(t *testing.T) {
Steps: []resource.TestStep{
{
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
"environment_id": environmentId,
"cron": drift.Cron,
"environment_id": environmentId,
"cron": drift.Cron,
"auto_drift_remediation": autoDriftRemediation,
}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "environment_id", environmentId),
resource.TestCheckResourceAttr(accessor, "cron", drift.Cron),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", autoDriftRemediation),
),
},
{
Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{
"environment_id": environmentId,
"cron": updateDrift.Cron,
"environment_id": environmentId,
"cron": updateDrift.Cron,
"auto_drift_remediation": "DISABLED",
}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "environment_id", environmentId),
resource.TestCheckResourceAttr(accessor, "cron", updateDrift.Cron),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", "DISABLED"),
),
},
},
Expand Down
10 changes: 10 additions & 0 deletions env0/resource_project_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ func resourceProjectPolicy() *schema.Resource {
Optional: true,
ValidateDiagFunc: ValidateCronExpression,
},
"auto_drift_remediation": {
Type: schema.TypeString,
Description: "Auto drift remediation setting (DISABLED or CODE_TO_CLOUD). Defaults to DISABLED",
Optional: true,
Default: DriftRemediationDisabled,
ValidateDiagFunc: NewStringInValidator([]string{
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

note: default is "DISABLED.

DriftRemediationDisabled,
DriftRemediationCodeToCloud,
}),
},
"vcs_pr_comments_enabled_default": {
Type: schema.TypeBool,
Description: "if 'true' all environments created in this project will be created with an 'enabled' running VCS PR plan/apply commands using PR comments. Default is 'false'",
Expand Down
14 changes: 14 additions & 0 deletions env0/resource_project_policy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
ForceRemoteBackend: true,
DriftDetectionCron: "0 4 * * *",
DriftDetectionEnabled: true,
AutoDriftRemediation: "CODE_TO_CLOUD",
VcsPrCommentsEnabledDefault: true,
OutputsAsInputsEnabled: true,
}
Expand All @@ -48,6 +49,9 @@ func TestUnitProjectPolicyResource(t *testing.T) {
MaxTtl: nil,
DefaultTtl: stringPtr("7-h"),
ForceRemoteBackend: false,
DriftDetectionCron: "",
DriftDetectionEnabled: false,
AutoDriftRemediation: "DISABLED",
}

resetPolicy := client.Policy{
Expand All @@ -72,6 +76,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
"continuous_deployment_default": policy.ContinuousDeploymentDefault,
"force_remote_backend": policy.ForceRemoteBackend,
"drift_detection_cron": policy.DriftDetectionCron,
"auto_drift_remediation": policy.AutoDriftRemediation,
"vcs_pr_comments_enabled_default": policy.VcsPrCommentsEnabledDefault,
"outputs_as_inputs_enabled": policy.OutputsAsInputsEnabled,
}),
Expand All @@ -90,6 +95,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
resource.TestCheckResourceAttr(accessor, "default_ttl", "inherit"),
resource.TestCheckResourceAttr(accessor, "force_remote_backend", strconv.FormatBool(policy.ForceRemoteBackend)),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", policy.DriftDetectionCron),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", policy.AutoDriftRemediation),
resource.TestCheckResourceAttr(accessor, "vcs_pr_comments_enabled_default", strconv.FormatBool(policy.VcsPrCommentsEnabledDefault)),
resource.TestCheckResourceAttr(accessor, "outputs_as_inputs_enabled", strconv.FormatBool(policy.OutputsAsInputsEnabled)),
),
Expand All @@ -104,6 +110,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
"skip_redundant_deployments": updatedPolicy.SkipRedundantDeployments,
"max_ttl": "Infinite",
"default_ttl": *updatedPolicy.DefaultTtl,
"auto_drift_remediation": updatedPolicy.AutoDriftRemediation,
}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "project_id", updatedPolicy.ProjectId),
Expand All @@ -118,6 +125,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
resource.TestCheckResourceAttr(accessor, "default_ttl", *updatedPolicy.DefaultTtl),
resource.TestCheckResourceAttr(accessor, "force_remote_backend", strconv.FormatBool(updatedPolicy.ForceRemoteBackend)),
resource.TestCheckResourceAttr(accessor, "drift_detection_cron", updatedPolicy.DriftDetectionCron),
resource.TestCheckResourceAttr(accessor, "auto_drift_remediation", updatedPolicy.AutoDriftRemediation),
resource.TestCheckResourceAttr(accessor, "vcs_pr_comments_enabled_default", strconv.FormatBool(updatedPolicy.VcsPrCommentsEnabledDefault)),
resource.TestCheckResourceAttr(accessor, "outputs_as_inputs_enabled", strconv.FormatBool(updatedPolicy.OutputsAsInputsEnabled)),
),
Expand All @@ -142,6 +150,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
ForceRemoteBackend: true,
DriftDetectionEnabled: true,
DriftDetectionCron: policy.DriftDetectionCron,
AutoDriftRemediation: policy.AutoDriftRemediation,
VcsPrCommentsEnabledDefault: policy.VcsPrCommentsEnabledDefault,
OutputsAsInputsEnabled: policy.OutputsAsInputsEnabled,
}).Times(1).Return(policy, nil),
Expand All @@ -159,6 +168,9 @@ func TestUnitProjectPolicyResource(t *testing.T) {
MaxTtl: "",
DefaultTtl: *updatedPolicy.DefaultTtl,
ForceRemoteBackend: updatedPolicy.ForceRemoteBackend,
DriftDetectionEnabled: updatedPolicy.DriftDetectionEnabled,
DriftDetectionCron: updatedPolicy.DriftDetectionCron,
AutoDriftRemediation: updatedPolicy.AutoDriftRemediation,
}).Times(1).Return(updatedPolicy, nil),
mock.EXPECT().Policy(gomock.Any()).Times(1).Return(updatedPolicy, nil), // 1 after update.
// Delete
Expand Down Expand Up @@ -189,6 +201,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
DisableDestroyEnvironments: true,
SkipRedundantDeployments: true,
UpdatedBy: "updater0",
AutoDriftRemediation: "DISABLED",
}

testCaseForDefault := resource.TestCase{
Expand Down Expand Up @@ -233,6 +246,7 @@ func TestUnitProjectPolicyResource(t *testing.T) {
SkipRedundantDeployments: policy.SkipRedundantDeployments,
DefaultTtl: "inherit",
MaxTtl: "inherit",
AutoDriftRemediation: "DISABLED",
}).Times(1).Return(policy, nil)

mock.EXPECT().Policy(gomock.Any()).Times(1).Return(expectedPolicy, nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ resource "env0_environment" "env" {
}

resource "env0_environment_drift_detection" "drift" {
environment_id = env0_environment.env.id
cron = "0 4 * * *"
environment_id = env0_environment.env.id
cron = "0 4 * * *" # Run drift detection daily at 4 AM
auto_drift_remediation = "CODE_TO_CLOUD" # Optional, defaults to "DISABLED"
}

2 changes: 2 additions & 0 deletions examples/resources/env0_project_policy/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ resource "env0_project_policy" "example" {
skip_apply_when_plan_is_empty = true
disable_destroy_environments = true
skip_redundant_deployments = true
drift_detection_cron = "0 4 * * *" # Run drift detection daily at 4 AM
auto_drift_remediation = "CODE_TO_CLOUD" # Optional, defaults to "DISABLED"
}
6 changes: 5 additions & 1 deletion tests/integration/011_policy/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ resource "env0_project_policy" "test_policy" {
disable_destroy_environments = false
skip_redundant_deployments = false
drift_detection_cron = var.second_run ? "0 4 * * *" : "0 3 * * *"

auto_drift_remediation = var.second_run ? "DISABLED" : "CODE_TO_CLOUD"
}

resource "env0_project_policy" "test_policy_2" {
Expand All @@ -32,6 +32,8 @@ resource "env0_project_policy" "test_policy_2" {
skip_redundant_deployments = true
vcs_pr_comments_enabled_default = true
outputs_as_inputs_enabled = true
drift_detection_cron = "0 4 * * *"
auto_drift_remediation = "CODE_TO_CLOUD"
}

resource "env0_project_policy" "test_policy_ttl" {
Expand All @@ -45,6 +47,8 @@ resource "env0_project_policy" "test_policy_ttl" {
skip_redundant_deployments = true
max_ttl = "4-d"
default_ttl = "14-h"
drift_detection_cron = "0 4 * * *"
auto_drift_remediation = "DISABLED"
}

resource "env0_project_policy" "test_policy_infinite" {
Expand Down
Loading