diff --git a/CHANGELOG.md b/CHANGELOG.md index 441954306..1c47d6981 100644 --- a/CHANGELOG.md +++ b/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)) diff --git a/tfe/data_source_organization.go b/tfe/data_source_organization.go index f6d68d3e2..a8d65ea17 100644 --- a/tfe/data_source_organization.go +++ b/tfe/data_source_organization.go @@ -52,6 +52,11 @@ func dataSourceTFEOrganization() *schema.Resource { Type: schema.TypeBool, Computed: true, }, + + "assessments_enforced": { + Type: schema.TypeBool, + Computed: true, + }, }, } } @@ -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 } diff --git a/tfe/data_source_workspace.go b/tfe/data_source_workspace.go index 396cc5add..bd6f974fa 100644 --- a/tfe/data_source_workspace.go +++ b/tfe/data_source_workspace.go @@ -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, @@ -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) diff --git a/tfe/data_source_workspace_test.go b/tfe/data_source_workspace_test.go index f816fdeff..182397e7f 100644 --- a/tfe/data_source_workspace_test.go +++ b/tfe/data_source_workspace_test.go @@ -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( @@ -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"] diff --git a/tfe/resource_tfe_organization.go b/tfe/resource_tfe_organization.go index 3af957a7f..6c1c1299a 100644 --- a/tfe/resource_tfe_organization.go +++ b/tfe/resource_tfe_organization.go @@ -73,6 +73,11 @@ func resourceTFEOrganization() *schema.Resource { Optional: true, Computed: true, }, + + "assessments_enforced": { + Type: schema.TypeBool, + Optional: true, + }, }, } } @@ -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 } @@ -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 { diff --git a/tfe/resource_tfe_organization_test.go b/tfe/resource_tfe_organization_test.go index ccba0dfc3..dd3bedb09 100644 --- a/tfe/resource_tfe_organization_test.go +++ b/tfe/resource_tfe_organization_test.go @@ -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"), ), }, }, @@ -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{ @@ -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), @@ -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), @@ -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)), ), }, }, @@ -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" @@ -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) } diff --git a/tfe/resource_tfe_workspace.go b/tfe/resource_tfe_workspace.go index 5be30d65c..fda8b5f74 100644 --- a/tfe/resource_tfe_workspace.go +++ b/tfe/resource_tfe_workspace.go @@ -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, @@ -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)), @@ -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) @@ -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)) diff --git a/tfe/resource_tfe_workspace_migrate.go b/tfe/resource_tfe_workspace_migrate.go index e9e977c9e..509e087ee 100644 --- a/tfe/resource_tfe_workspace_migrate.go +++ b/tfe/resource_tfe_workspace_migrate.go @@ -20,6 +20,11 @@ func resourceTfeWorkspaceResourceV0() *schema.Resource { ForceNew: true, }, + "assessments_enabled": { + Type: schema.TypeBool, + Optional: true, + }, + "auto_apply": { Type: schema.TypeBool, Optional: true, diff --git a/tfe/resource_tfe_workspace_test.go b/tfe/resource_tfe_workspace_test.go index 2d749d5e1..5cf8c9f29 100644 --- a/tfe/resource_tfe_workspace_test.go +++ b/tfe/resource_tfe_workspace_test.go @@ -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( @@ -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" { @@ -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" { diff --git a/website/docs/d/organization.html.markdown b/website/docs/d/organization.html.markdown index 9cce994df..bc686c722 100644 --- a/website/docs/d/organization.html.markdown +++ b/website/docs/d/organization.html.markdown @@ -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. diff --git a/website/docs/d/workspace.html.markdown b/website/docs/d/workspace.html.markdown index 978e7381b..773629fc8 100644 --- a/website/docs/d/workspace.html.markdown +++ b/website/docs/d/workspace.html.markdown @@ -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` - (Available only in Terraform Cloud) Indicates whether health assessments such as drift detection are enabled for the workspace. * `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`. diff --git a/website/docs/r/organization.html.markdown b/website/docs/r/organization.html.markdown index 73466933c..2516355c7 100644 --- a/website/docs/r/organization.html.markdown +++ b/website/docs/r/organization.html.markdown @@ -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 diff --git a/website/docs/r/workspace.html.markdown b/website/docs/r/workspace.html.markdown index baf26fa92..a263f530e 100644 --- a/website/docs/r/workspace.html.markdown +++ b/website/docs/r/workspace.html.markdown @@ -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