From 70655d60ad265d2f367f973d6b43952425a53b99 Mon Sep 17 00:00:00 2001 From: Manu Chandrasekhar Date: Sun, 21 Sep 2025 00:06:56 -0400 Subject: [PATCH] feat: add projects and workspace exclusions support for policyset --- CHANGELOG.md | 3 + internal/provider/resource_tfe_policy_set.go | 115 ++++++++++++++++++ .../provider/resource_tfe_policy_set_test.go | 64 ++++++++++ website/docs/r/policy_set.html.markdown | 17 +++ 4 files changed, 199 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 90ff10089..05483c12a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## Unreleased +ENHANCEMENTS: +* `r/tfe_policy_set`: Add support for `workspace_exclusion_ids` and `project_ids` attributes ([#1684](https://github.com/hashicorp/terraform-provider-tfe/issues/1684)) + ## v0.69.0 BREAKING CHANGES: diff --git a/internal/provider/resource_tfe_policy_set.go b/internal/provider/resource_tfe_policy_set.go index 802dd080a..73b27cb28 100644 --- a/internal/provider/resource_tfe_policy_set.go +++ b/internal/provider/resource_tfe_policy_set.go @@ -157,6 +157,21 @@ func resourceTFEPolicySet() *schema.Resource { Elem: &schema.Schema{Type: schema.TypeString}, ConflictsWith: []string{"global"}, }, + + "workspace_exclusion_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + + "project_ids": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + ConflictsWith: []string{"global"}, + }, }, } } @@ -226,6 +241,14 @@ func resourceTFEPolicySetCreate(d *schema.ResourceData, meta interface{}) error options.Workspaces = append(options.Workspaces, &tfe.Workspace{ID: workspaceID.(string)}) } + for _, workspaceExclusionID := range d.Get("workspace_exclusion_ids").(*schema.Set).List() { + options.WorkspaceExclusions = append(options.WorkspaceExclusions, &tfe.Workspace{ID: workspaceExclusionID.(string)}) + } + + for _, projectID := range d.Get("project_ids").(*schema.Set).List() { + options.Projects = append(options.Projects, &tfe.Project{ID: projectID.(string)}) + } + log.Printf("[DEBUG] Create policy set %s for organization: %s", name, organization) policySet, err := config.Client.PolicySets.Create(ctx, organization, options) if err != nil { @@ -325,6 +348,20 @@ func resourceTFEPolicySetRead(d *schema.ResourceData, meta interface{}) error { } d.Set("workspace_ids", workspaceIDs) + // Update the workspace exclusions. + var workspaceExclusionIDs []interface{} + for _, workspaceExclusion := range policySet.WorkspaceExclusions { + workspaceExclusionIDs = append(workspaceExclusionIDs, workspaceExclusion.ID) + } + d.Set("workspace_exclusion_ids", workspaceExclusionIDs) + + // Update the projects. + var projectIDs []interface{} + for _, project := range policySet.Projects { + projectIDs = append(projectIDs, project.ID) + } + d.Set("project_ids", projectIDs) + return nil } @@ -485,6 +522,84 @@ func resourceTFEPolicySetUpdate(d *schema.ResourceData, meta interface{}) error } } + if d.HasChange("workspace_exclusion_ids") { + oldWorkspaceExclusionIDValues, newWorkspaceExclusionIDValues := d.GetChange("workspace_exclusion_ids") + newWorkspaceExclusionIDsSet := newWorkspaceExclusionIDValues.(*schema.Set) + oldWorkspaceExclusionIDsSet := oldWorkspaceExclusionIDValues.(*schema.Set) + + newWorkspaceExclusionIDs := newWorkspaceExclusionIDsSet.Difference(oldWorkspaceExclusionIDsSet) + oldWorkspaceExclusionIDs := oldWorkspaceExclusionIDsSet.Difference(newWorkspaceExclusionIDsSet) + + // First add the new workspace exclusions. + if newWorkspaceExclusionIDs.Len() > 0 { + options := tfe.PolicySetAddWorkspaceExclusionsOptions{} + + for _, workspaceExclusionID := range newWorkspaceExclusionIDs.List() { + options.WorkspaceExclusions = append(options.WorkspaceExclusions, &tfe.Workspace{ID: workspaceExclusionID.(string)}) + } + + log.Printf("[DEBUG] Add workspace exclusions to policy set: %s", d.Id()) + err := config.Client.PolicySets.AddWorkspaceExclusions(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error adding workspace exclusions to policy set %s: %w", d.Id(), err) + } + } + + // Then remove all the old workspace exclusions. + if oldWorkspaceExclusionIDs.Len() > 0 { + options := tfe.PolicySetRemoveWorkspaceExclusionsOptions{} + + for _, workspaceExclusionID := range oldWorkspaceExclusionIDs.List() { + options.WorkspaceExclusions = append(options.WorkspaceExclusions, &tfe.Workspace{ID: workspaceExclusionID.(string)}) + } + + log.Printf("[DEBUG] Remove workspace exclusions from policy set: %s", d.Id()) + err := config.Client.PolicySets.RemoveWorkspaceExclusions(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error removing workspace exclusions from policy set %s: %w", d.Id(), err) + } + } + } + + if d.HasChange("project_ids") { + oldProjectIDValues, newProjectIDValues := d.GetChange("project_ids") + newProjectIDsSet := newProjectIDValues.(*schema.Set) + oldProjectIDsSet := oldProjectIDValues.(*schema.Set) + + newProjectIDs := newProjectIDsSet.Difference(oldProjectIDsSet) + oldProjectIDs := oldProjectIDsSet.Difference(newProjectIDsSet) + + // First add the new projects. + if newProjectIDs.Len() > 0 { + options := tfe.PolicySetAddProjectsOptions{} + + for _, projectID := range newProjectIDs.List() { + options.Projects = append(options.Projects, &tfe.Project{ID: projectID.(string)}) + } + + log.Printf("[DEBUG] Add projects to policy set: %s", d.Id()) + err := config.Client.PolicySets.AddProjects(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error adding projects to policy set %s: %w", d.Id(), err) + } + } + + // Then remove all the old projects. + if oldProjectIDs.Len() > 0 { + options := tfe.PolicySetRemoveProjectsOptions{} + + for _, projectID := range oldProjectIDs.List() { + options.Projects = append(options.Projects, &tfe.Project{ID: projectID.(string)}) + } + + log.Printf("[DEBUG] Remove projects from policy set: %s", d.Id()) + err := config.Client.PolicySets.RemoveProjects(ctx, d.Id(), options) + if err != nil { + return fmt.Errorf("Error removing projects from policy set %s: %w", d.Id(), err) + } + } + } + return resourceTFEPolicySetRead(d, meta) } diff --git a/internal/provider/resource_tfe_policy_set_test.go b/internal/provider/resource_tfe_policy_set_test.go index ba79ff907..496139801 100644 --- a/internal/provider/resource_tfe_policy_set_test.go +++ b/internal/provider/resource_tfe_policy_set_test.go @@ -1331,6 +1331,70 @@ resource "tfe_policy_set" "foobar" { }`, sourcePath, organization) } +func TestAccTFEPolicySet_projectsAndWorkspaceExclusions(t *testing.T) { + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, orgCleanup := createBusinessOrganization(t, tfeClient) + t.Cleanup(orgCleanup) + + policySet := &tfe.PolicySet{} + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccMuxedProviders, + CheckDestroy: testAccCheckTFEPolicySetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEPolicySet_projectsAndWorkspaceExclusions(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEPolicySetExists("tfe_policy_set.foobar", policySet), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "name", "tst-terraform"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "project_ids.#", "1"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "workspace_exclusion_ids.#", "1"), + ), + }, + }, + }) +} + +func testAccTFEPolicySet_projectsAndWorkspaceExclusions(organization string) string { + return fmt.Sprintf(` +locals { + organization_name = "%s" +} + +resource "tfe_sentinel_policy" "foo" { + name = "policy-foo" + organization = local.organization_name + policy = "main = rule { true }" + enforce_mode = "hard-mandatory" +} + +resource "tfe_project" "foo" { + name = "project-foo" + organization = local.organization_name +} + +resource "tfe_workspace" "foo" { + name = "workspace-foo" + organization = local.organization_name +} + +resource "tfe_policy_set" "foobar" { + name = "tst-terraform" + organization = local.organization_name + policy_ids = [tfe_sentinel_policy.foo.id] + project_ids = [tfe_project.foo.id] + workspace_exclusion_ids = [tfe_workspace.foo.id] +}`, organization) +} + func testAccTFEPolicySet_versionsConflict(organization string, sourcePath string) string { return fmt.Sprintf(` data "tfe_slug" "policy" { diff --git a/website/docs/r/policy_set.html.markdown b/website/docs/r/policy_set.html.markdown index c27881951..a74549ff5 100644 --- a/website/docs/r/policy_set.html.markdown +++ b/website/docs/r/policy_set.html.markdown @@ -54,6 +54,21 @@ resource "tfe_policy_set" "test" { } ``` +Using projects and workspace exclusions: + +```hcl +resource "tfe_policy_set" "test" { + name = "my-policy-set" + description = "A brand new policy set" + organization = "my-org-name" + kind = "sentinel" + policy_tool_version = "0.24.1" + policy_ids = [tfe_sentinel_policy.test.id] + project_ids = [tfe_project.test.id] + workspace_exclusion_ids = [tfe_workspace.excluded.id] +} +``` + Manually uploaded policy set, in lieu of VCS: ```hcl @@ -101,6 +116,8 @@ The following arguments are supported: new resource if changed. This value _must not_ be provided if `policy_ids` are provided. * `workspace_ids` - (Optional) A list of workspace IDs. This value _must not_ be provided if `global` is provided. +* `workspace_exclusion_ids` - (Optional) A list of workspace IDs to exclude from this policy set. +* `project_ids` - (Optional) A list of project IDs that this policy set should be enforced on. * `slug` - (Optional) A reference to the `tfe_slug` data source that contains the `source_path` to where the local policies are located. This is used when policies are located locally, and can only be used when there is no VCS repo or