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

Add support for managing health assessment (drift detection) settings on workspaces and organizations #550

Merged
merged 16 commits into from Sep 26, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,10 @@
## Unreleased
* r/tfe_workspace: Changes in `agent_pool_id` and `execution_mode` attributes are now detected and applied. ([#607](https://github.com/hashicorp/terraform-provider-tfe/pull/607))
* Added attributes for health assessments (drift detection) - available only in Terraform Cloud ([550](https://github.com/hashicorp/terraform-provider-tfe/pull/550)):
* r/tfe_workspace: Added attribute, `assessments_enabled`, for health assessments (drift detection) setting
* d/tfe_workspace: Added attribute, `assessments_enabled`, for health assessments (drift detection) setting
* r/tfe_organization: Added attribute, `assessments_enforced`, for health assessments (drift detection) setting
* d/tfe_organization: Added attribute, `assessments_enforced`, for health assessments (drift detection) setting

FEATURES:
* r/tfe_workspace_run_task, d/tfe_workspace_run_task: Add `stage` attribute to workspace run tasks. ([#555](https://github.com/hashicorp/terraform-provider-tfe/pull/555))
Expand Down
6 changes: 6 additions & 0 deletions tfe/data_source_organization.go
Expand Up @@ -52,6 +52,11 @@ func dataSourceTFEOrganization() *schema.Resource {
Type: schema.TypeBool,
Computed: true,
},

"assessments_enforced": {
Type: schema.TypeBool,
Computed: true,
},
},
}
}
Expand Down Expand Up @@ -79,6 +84,7 @@ func dataSourceTFEOrganizationRead(d *schema.ResourceData, meta interface{}) err
d.Set("owners_team_saml_role_id", org.OwnersTeamSAMLRoleID)
d.Set("two_factor_conformant", org.TwoFactorConformant)
d.Set("send_passing_statuses_for_untriggered_speculative_plans", org.SendPassingStatusesForUntriggeredSpeculativePlans)
d.Set("assessments_enforced", org.AssessmentsEnforced)

return nil
}
6 changes: 6 additions & 0 deletions tfe/data_source_workspace.go
Expand Up @@ -54,6 +54,11 @@ func dataSourceTFEWorkspace() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
},

"assessments_enabled": {
Type: schema.TypeBool,
Computed: true,
},

"operations": {
Type: schema.TypeBool,
Computed: true,
Expand Down Expand Up @@ -183,6 +188,7 @@ func dataSourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error
d.Set("allow_destroy_plan", workspace.AllowDestroyPlan)
d.Set("auto_apply", workspace.AutoApply)
d.Set("description", workspace.Description)
d.Set("assessments_enabled", workspace.AssessmentsEnabled)
d.Set("file_triggers_enabled", workspace.FileTriggersEnabled)
d.Set("operations", workspace.Operations)
d.Set("policy_check_failures", workspace.PolicyCheckFailures)
Expand Down
3 changes: 3 additions & 0 deletions tfe/data_source_workspace_test.go
Expand Up @@ -91,6 +91,8 @@ func TestAccTFEWorkspaceDataSource_basic(t *testing.T) {
"data.tfe_workspace.foobar", "runs_count", "0"),
resource.TestCheckResourceAttr(
"data.tfe_workspace.foobar", "speculative_enabled", "true"),
resource.TestCheckResourceAttr(
"data.tfe_workspace.foobar", "assessments_enabled", "false"),
resource.TestCheckResourceAttr(
"data.tfe_workspace.foobar", "structured_run_output_enabled", "true"),
resource.TestCheckResourceAttr(
Expand Down Expand Up @@ -174,6 +176,7 @@ resource "tfe_workspace" "foobar" {
file_triggers_enabled = true
queue_all_runs = false
speculative_enabled = true
assessments_enabled = false
tag_names = ["modules", "shared"]
terraform_version = "0.11.1"
trigger_prefixes = ["/modules", "/shared"]
Expand Down
13 changes: 13 additions & 0 deletions tfe/resource_tfe_organization.go
Expand Up @@ -73,6 +73,11 @@ func resourceTFEOrganization() *schema.Resource {
Optional: true,
Computed: true,
},

"assessments_enforced": {
Type: schema.TypeBool,
Optional: true,
},
},
}
}
Expand Down Expand Up @@ -123,6 +128,9 @@ func resourceTFEOrganizationRead(d *schema.ResourceData, meta interface{}) error
d.Set("owners_team_saml_role_id", org.OwnersTeamSAMLRoleID)
d.Set("cost_estimation_enabled", org.CostEstimationEnabled)
d.Set("send_passing_statuses_for_untriggered_speculative_plans", org.SendPassingStatusesForUntriggeredSpeculativePlans)
// TFE (onprem) does not currently have this feature and this value won't be returned in those cases.
// org.AssessmentsEnforced will default to false
d.Set("assessments_enforced", org.AssessmentsEnforced)

return nil
}
Expand Down Expand Up @@ -166,6 +174,11 @@ func resourceTFEOrganizationUpdate(d *schema.ResourceData, meta interface{}) err
options.SendPassingStatusesForUntriggeredSpeculativePlans = tfe.Bool(sendPassingStatusesForUntriggeredSpeculativePlans.(bool))
}

// If cost_estimation_enabled is supplied, set it using the options struct.
if assessmentsEnforced, ok := d.GetOkExists("assessments_enforced"); ok {
options.AssessmentsEnforced = tfe.Bool(assessmentsEnforced.(bool))
}

log.Printf("[DEBUG] Update configuration of organization: %s", d.Id())
org, err := tfeClient.Organizations.Update(ctx, d.Id(), options)
if err != nil {
Expand Down
18 changes: 14 additions & 4 deletions tfe/resource_tfe_organization_test.go
Expand Up @@ -72,6 +72,8 @@ func TestAccTFEOrganization_full(t *testing.T) {
"tfe_organization.foobar", "cost_estimation_enabled", "false"),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "send_passing_statuses_for_untriggered_speculative_plans", "false"),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "assessments_enforced", "false"),
),
},
},
Expand All @@ -91,9 +93,11 @@ func TestAccTFEOrganization_update_costEstimation(t *testing.T) {

// First update
costEstimationEnabled1 := true
assessmentsEnforced1 := true

// Second update
costEstimationEnabled2 := false
assessmentsEnforced2 := false
updatedName := org.Name + "_foobar"

resource.Test(t, resource.TestCase{
Expand All @@ -102,7 +106,7 @@ func TestAccTFEOrganization_update_costEstimation(t *testing.T) {
CheckDestroy: testAccCheckTFEOrganizationDestroy,
Steps: []resource.TestStep{
{
Config: testAccTFEOrganization_update(org.Name, org.Email, costEstimationEnabled1),
Config: testAccTFEOrganization_update(org.Name, org.Email, costEstimationEnabled1, assessmentsEnforced1),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEOrganizationExists(
"tfe_organization.foobar", org),
Expand All @@ -123,11 +127,13 @@ func TestAccTFEOrganization_update_costEstimation(t *testing.T) {
"tfe_organization.foobar", "cost_estimation_enabled", strconv.FormatBool(costEstimationEnabled1)),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "send_passing_statuses_for_untriggered_speculative_plans", "false"),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "assessments_enforced", strconv.FormatBool(assessmentsEnforced1)),
),
},

{
Config: testAccTFEOrganization_update(updatedName, org.Email, costEstimationEnabled2),
Config: testAccTFEOrganization_update(updatedName, org.Email, costEstimationEnabled2, assessmentsEnforced2),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEOrganizationExists(
"tfe_organization.foobar", org),
Expand All @@ -146,6 +152,8 @@ func TestAccTFEOrganization_update_costEstimation(t *testing.T) {
"tfe_organization.foobar", "owners_team_saml_role_id", "owners"),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "cost_estimation_enabled", strconv.FormatBool(costEstimationEnabled2)),
resource.TestCheckResourceAttr(
"tfe_organization.foobar", "assessments_enforced", strconv.FormatBool(assessmentsEnforced2)),
),
},
},
Expand Down Expand Up @@ -370,10 +378,11 @@ resource "tfe_organization" "foobar" {
collaborator_auth_policy = "password"
owners_team_saml_role_id = "owners"
cost_estimation_enabled = false
assessments_enforced = false
}`, rInt)
}

func testAccTFEOrganization_update(orgName string, orgEmail string, costEstimationEnabled bool) string {
func testAccTFEOrganization_update(orgName string, orgEmail string, costEstimationEnabled bool, assessmentsEnforced bool) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "%s"
Expand All @@ -382,5 +391,6 @@ resource "tfe_organization" "foobar" {
session_remember_minutes = 3600
owners_team_saml_role_id = "owners"
cost_estimation_enabled = %t
}`, orgName, orgEmail, costEstimationEnabled)
assessments_enforced = %t
}`, orgName, orgEmail, costEstimationEnabled, assessmentsEnforced)
}
17 changes: 17 additions & 0 deletions tfe/resource_tfe_workspace.go
Expand Up @@ -123,6 +123,11 @@ func resourceTFEWorkspace() *schema.Resource {
Elem: &schema.Schema{Type: schema.TypeString},
},

"assessments_enabled": {
Type: schema.TypeBool,
Optional: true,
},

"operations": {
Type: schema.TypeBool,
Optional: true,
Expand Down Expand Up @@ -241,6 +246,7 @@ func resourceTFEWorkspaceCreate(d *schema.ResourceData, meta interface{}) error
AllowDestroyPlan: tfe.Bool(d.Get("allow_destroy_plan").(bool)),
AutoApply: tfe.Bool(d.Get("auto_apply").(bool)),
Description: tfe.String(d.Get("description").(string)),
AssessmentsEnabled: tfe.Bool(d.Get("assessments_enabled").(bool)),
FileTriggersEnabled: tfe.Bool(d.Get("file_triggers_enabled").(bool)),
QueueAllRuns: tfe.Bool(d.Get("queue_all_runs").(bool)),
SpeculativeEnabled: tfe.Bool(d.Get("speculative_enabled").(bool)),
Expand Down Expand Up @@ -365,6 +371,11 @@ func resourceTFEWorkspaceRead(d *schema.ResourceData, meta interface{}) error {
// Update the config.
d.Set("name", workspace.Name)
d.Set("allow_destroy_plan", workspace.AllowDestroyPlan)

// TFE (onprem) does not currently have this feature and this value won't be returned in those cases.
// workspace.AssessmentsEnabled will default to false
d.Set("assessments_enabled", workspace.AssessmentsEnabled)

d.Set("auto_apply", workspace.AutoApply)
d.Set("description", workspace.Description)
d.Set("file_triggers_enabled", workspace.FileTriggersEnabled)
Expand Down Expand Up @@ -453,6 +464,12 @@ func resourceTFEWorkspaceUpdate(d *schema.ResourceData, meta interface{}) error
WorkingDirectory: tfe.String(d.Get("working_directory").(string)),
}

if d.HasChange("assessments_enabled") {
if v, ok := d.GetOkExists("assessments_enabled"); ok {
options.AssessmentsEnabled = tfe.Bool(v.(bool))
}
}

if d.HasChange("agent_pool_id") {
if v, ok := d.GetOk("agent_pool_id"); ok && v.(string) != "" {
options.AgentPoolID = tfe.String(v.(string))
Expand Down
5 changes: 5 additions & 0 deletions tfe/resource_tfe_workspace_migrate.go
Expand Up @@ -20,6 +20,11 @@ func resourceTfeWorkspaceResourceV0() *schema.Resource {
ForceNew: true,
},

"assessments_enabled": {
Type: schema.TypeBool,
Optional: true,
},

"auto_apply": {
Type: schema.TypeBool,
Optional: true,
Expand Down
59 changes: 59 additions & 0 deletions tfe/resource_tfe_workspace_test.go
Expand Up @@ -47,6 +47,8 @@ func TestAccTFEWorkspace_basic(t *testing.T) {
"tfe_workspace.foobar", "queue_all_runs", "true"),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "speculative_enabled", "true"),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "assessments_enabled", "false"),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "structured_run_output_enabled", "true"),
resource.TestCheckResourceAttr(
Expand Down Expand Up @@ -2154,6 +2156,44 @@ func testAccCheckTFEWorkspaceDestroy(s *terraform.State) error {
return nil
}

func TestAccTFEWorkspace_basicAssessmentsEnabled(t *testing.T) {
skipIfEnterprise(t)

workspace := &tfe.Workspace{}
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckTFEWorkspaceDestroy,
Steps: []resource.TestStep{
{
Config: testAccTFEWorkspace_basic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEWorkspaceExists(
"tfe_workspace.foobar", workspace),
testAccCheckTFEWorkspaceAttributes(workspace),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "name", "workspace-test"),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "assessments_enabled", "false"),
),
},
{
Config: testAccTFEWorkspace_updateAssessmentsEnabled(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEWorkspaceExists(
"tfe_workspace.foobar", workspace),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "name", "workspace-updated"),
resource.TestCheckResourceAttr(
"tfe_workspace.foobar", "assessments_enabled", "true"),
),
},
},
})
}

func testAccTFEWorkspace_basic(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
Expand Down Expand Up @@ -2381,6 +2421,25 @@ resource "tfe_workspace" "foobar" {
}`, rInt)
}

func testAccTFEWorkspace_updateAssessmentsEnabled(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
name = "tst-terraform-%d"
email = "admin@company.com"
}

resource "tfe_workspace" "foobar" {
name = "workspace-updated"
organization = tfe_organization.foobar.id
description = "My favorite workspace!"
assessments_enabled = true
allow_destroy_plan = false
auto_apply = true
tag_names = ["fav", "test"]
terraform_version = "0.15.4"
}`, rInt)
}

func testAccTFEWorkspace_updateAddWorkingDirectory(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "foobar" {
Expand Down
1 change: 1 addition & 0 deletions website/docs/d/organization.html.markdown
Expand Up @@ -30,6 +30,7 @@ In addition to all arguments above, the following attributes are exported:
* `name` - Name of the organization.
* `email` - Admin email address.
* `external_id` - An identifier for the organization.
* `assessments_enforced` - (Available only in Terraform Cloud) Whether to force health assessments (drift detection) on all eligible workspaces or allow workspaces to set thier own preferences.
* `collaborator_auth_policy` - Authentication policy (`password` or `two_factor_mandatory`). Defaults to `password`.
* `cost_estimation_enabled` - Whether or not the cost estimation feature is enabled for all workspaces in the organization. Defaults to true. In a Terraform Cloud organization which does not have Teams & Governance features, this value is always false and cannot be changed. In Terraform Enterprise, Cost Estimation must also be enabled in Site Administration.
* `owners_team_saml_role_id` - The name of the "owners" team.
Expand Down
1 change: 1 addition & 0 deletions website/docs/d/workspace.html.markdown
Expand Up @@ -35,6 +35,7 @@ In addition to all arguments above, the following attributes are exported:
* `id` - The workspace ID.
* `allow_destroy_plan` - Indicates whether destroy plans can be queued on the workspace.
* `auto_apply` - Indicates whether to automatically apply changes when a Terraform plan is successful.
`assessments_enabled` - Indicates whether health assessments such as drift detection are enabled for the workspace.
rexredinger marked this conversation as resolved.
Show resolved Hide resolved
* `file_triggers_enabled` - Indicates whether runs are triggered based on the changed files in a VCS push (if `true`) or always triggered on every push (if `false`).
* `global_remote_state` - (Optional) Whether the workspace should allow all workspaces in the organization to access its state data during runs. If false, then only specifically approved workspaces can access its state (determined by the `remote_state_consumer_ids` argument).
* `remote_state_consumer_ids` - (Optional) A set of workspace IDs that will be set as the remote state consumers for the given workspace. Cannot be used if `global_remote_state` is set to `true`.
Expand Down
1 change: 1 addition & 0 deletions website/docs/r/organization.html.markdown
Expand Up @@ -36,6 +36,7 @@ The following arguments are supported:
* `owners_team_saml_role_id` - (Optional) The name of the "owners" team.
* `cost_estimation_enabled` - (Optional) Whether or not the cost estimation feature is enabled for all workspaces in the organization. Defaults to true. In a Terraform Cloud organization which does not have Teams & Governance features, this value is always false and cannot be changed. In Terraform Enterprise, Cost Estimation must also be enabled in Site Administration.
* `send_passing_statuses_for_untriggered_speculative_plans` - (Optional) Whether or not to send VCS status updates for untriggered speculative plans. This can be useful if large numbers of untriggered workspaces are exhausting request limits for connected version control service providers like GitHub. Defaults to false. In Terraform Enterprise, this setting has no effect and cannot be changed but is also available in Site Administration.
* `assessments_enforced` - (Optional) (Available only in Terraform Cloud) Whether to force health assessments (drift detection) on all eligible workspaces or allow workspaces to set thier own preferences.

## Attributes Reference

Expand Down
1 change: 1 addition & 0 deletions website/docs/r/workspace.html.markdown
Expand Up @@ -69,6 +69,7 @@ The following arguments are supported:
execution modes are valid. When set to `local`, the workspace will be used
for state storage only. This value _must not_ be provided if `operations`
is provided.
* `assessments_enabled` - (Optional) Whether to regularly run health assessments such as drift detection on the workspace. Defaults to `false`.
* `file_triggers_enabled` - (Optional) Whether to filter runs based on the changed files
in a VCS push. Defaults to `false`. If enabled, the working directory and
trigger prefixes describe a set of paths which must contain changes for a
Expand Down