Skip to content

Commit

Permalink
add ability to attach a workspace to an existing policy set
Browse files Browse the repository at this point in the history
  • Loading branch information
Uk1288 committed Aug 22, 2022
1 parent ca44122 commit b38c264
Show file tree
Hide file tree
Showing 8 changed files with 383 additions and 3 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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))
* r/tfe_workspace_policy_set: Add ability to attach an existing `workspace` to an existing `policy set`. ([#589](https://github.com/hashicorp/terraform-provider-tfe/pull/589))

## v0.36.0 (August 16th, 2022)

Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ require (
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.1 // indirect
github.com/hashicorp/go-slug v0.10.0
github.com/hashicorp/go-tfe v1.7.0
github.com/hashicorp/go-tfe v1.8.0
github.com/hashicorp/go-version v1.6.0
github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce
github.com/hashicorp/hcl/v2 v2.10.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -231,8 +231,8 @@ github.com/hashicorp/go-slug v0.10.0 h1:mh4DDkBJTh9BuEjY/cv8PTo7k9OjT4PcW8PgZnJ4
github.com/hashicorp/go-slug v0.10.0/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4=
github.com/hashicorp/go-tfe v1.6.0 h1:lRfyTVLBP1njo2wShE9FimALzVZBfOqMGNuBdsor38w=
github.com/hashicorp/go-tfe v1.6.0/go.mod h1:E8a90lC4kjU5Lc2c0D+SnWhUuyuoCIVm4Ewzv3jCD3A=
github.com/hashicorp/go-tfe v1.7.0 h1:GELRhS5dizF6giwjZBqUC/xPaSuNYB+hWRtUnf6i8K8=
github.com/hashicorp/go-tfe v1.7.0/go.mod h1:E8a90lC4kjU5Lc2c0D+SnWhUuyuoCIVm4Ewzv3jCD3A=
github.com/hashicorp/go-tfe v1.8.0 h1:VnVdHHPoyAByqJuHN0aTOxY2Svojn5fzh/RnVlqDRqE=
github.com/hashicorp/go-tfe v1.8.0/go.mod h1:uSWi2sPw7tLrqNIiASid9j3SprbbkPSJ/2s3X0mMemg=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
Expand Down
1 change: 1 addition & 0 deletions tfe/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ func Provider() *schema.Provider {
"tfe_variable": resourceTFEVariable(),
"tfe_variable_set": resourceTFEVariableSet(),
"tfe_workspace_variable_set": resourceTFEWorkspaceVariableSet(),
"tfe_workspace_policy_set": resourceTFEWorkspacePolicySet(),
},

ConfigureFunc: providerConfigure,
Expand Down
1 change: 1 addition & 0 deletions tfe/resource_tfe_policy_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ func resourceTFEPolicySet() *schema.Resource {
"workspace_ids": {
Type: schema.TypeSet,
Optional: true,
Computed: true,
Elem: &schema.Schema{Type: schema.TypeString},
ConflictsWith: []string{"global"},
},
Expand Down
169 changes: 169 additions & 0 deletions tfe/resource_tfe_workspace_policy_set.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package tfe

import (
"errors"
"fmt"
"log"
"strings"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceTFEWorkspacePolicySet() *schema.Resource {
return &schema.Resource{
Create: resourceTFEWorkspacePolicySetCreate,
Read: resourceTFEWorkspacePolicySetRead,
Delete: resourceTFEWorkspacePolicySetDelete,
Importer: &schema.ResourceImporter{
State: resourceTFEWorkspacePolicySetImporter,
},

Schema: map[string]*schema.Schema{
"policy_set_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"workspace_id": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}

func resourceTFEWorkspacePolicySetCreate(d *schema.ResourceData, meta interface{}) error {
tfeClient := meta.(*tfe.Client)

policySetID := d.Get("policy_set_id").(string)
workspaceID := d.Get("workspace_id").(string)

policySetAddWorkspacesOptions := tfe.PolicySetAddWorkspacesOptions{}
policySetAddWorkspacesOptions.Workspaces = append(policySetAddWorkspacesOptions.Workspaces, &tfe.Workspace{ID: workspaceID})

err := tfeClient.PolicySets.AddWorkspaces(ctx, policySetID, policySetAddWorkspacesOptions)
if err != nil {
return fmt.Errorf(
"Error attaching policy set id %s to workspace %s: %w", policySetID, workspaceID, err)
}

d.SetId(fmt.Sprintf("%s_%s", workspaceID, policySetID))

return resourceTFEWorkspacePolicySetRead(d, meta)
}

func resourceTFEWorkspacePolicySetRead(d *schema.ResourceData, meta interface{}) error {
tfeClient := meta.(*tfe.Client)

policySetID := d.Get("policy_set_id").(string)
workspaceID := d.Get("workspace_id").(string)

log.Printf("[DEBUG] Read configuration of workspace policy set: %s", policySetID)
policySet, err := tfeClient.PolicySets.ReadWithOptions(ctx, policySetID, &tfe.PolicySetReadOptions{
Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetWorkspaces},
})
if err != nil {
if errors.Is(err, tfe.ErrResourceNotFound) {
log.Printf("[DEBUG] Policy set %s no longer exists", policySetID)
d.SetId("")
return nil
}
return fmt.Errorf("Error reading configuration of policy set %s: %w", policySetID, err)
}

isWorkspaceAttached := false
for _, workspace := range policySet.Workspaces {
if workspace.ID == workspaceID {
isWorkspaceAttached = true
d.Set("workspace_id", workspaceID)
break
}
}

if !isWorkspaceAttached {
log.Printf("[DEBUG] Workspace %s not attached to policy set %s. Removing from state.", workspaceID, policySetID)
d.SetId("")
return nil
}

d.Set("policy_set_id", policySetID)
return nil
}

func resourceTFEWorkspacePolicySetDelete(d *schema.ResourceData, meta interface{}) error {
tfeClient := meta.(*tfe.Client)

policySetID := d.Get("policy_set_id").(string)
workspaceID := d.Get("workspace_id").(string)

log.Printf("[DEBUG] Detaching workspace (%s) from policy set (%s)", workspaceID, policySetID)
policySetRemoveWorkspacesOptions := tfe.PolicySetRemoveWorkspacesOptions{}
policySetRemoveWorkspacesOptions.Workspaces = append(policySetRemoveWorkspacesOptions.Workspaces, &tfe.Workspace{ID: workspaceID})

err := tfeClient.PolicySets.RemoveWorkspaces(ctx, policySetID, policySetRemoveWorkspacesOptions)
if err != nil {
return fmt.Errorf(
"Error detaching workspace %s from policy set %s: %w", workspaceID, policySetID, err)
}

return nil
}

func resourceTFEWorkspacePolicySetImporter(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
// The format of the import ID is <ORGANIZATION/WORKSPACE NAME/POLICYSET NAME>
splitID := strings.SplitN(d.Id(), "/", 3)
if len(splitID) != 3 {
return nil, fmt.Errorf(
"invalid workspace policy set input format: %s (expected <ORGANIZATION>/<WORKSPACE NAME>/<POLICYSET NAME>)",
splitID,
)
}

organization, wsName, pSName := splitID[0], splitID[1], splitID[2]

tfeClient := meta.(*tfe.Client)

// Ensure the named workspace exists before fetching all the policy sets in the org
_, err := tfeClient.Workspaces.Read(ctx, organization, wsName)
if err != nil {
return nil, fmt.Errorf("error reading configuration of workspace %s in organization %s: %w", wsName, organization, err)
}

options := &tfe.PolicySetListOptions{Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetWorkspaces}}
for {
list, err := tfeClient.PolicySets.List(ctx, organization, options)
if err != nil {
return nil, fmt.Errorf("Error retrieving policy sets: %w", err)
}
for _, policySet := range list.Items {
if policySet.Name != pSName {
continue
}

for _, ws := range policySet.Workspaces {
if ws.Name != wsName {
continue
}

d.Set("workspace_id", ws.ID)
d.Set("policy_set_id", policySet.ID)
d.SetId(fmt.Sprintf("%s_%s", ws.ID, policySet.ID))

return []*schema.ResourceData{d}, nil
}
}

// Exit the loop when we've seen all pages.
if list.CurrentPage >= list.TotalPages {
break
}

// Update the page number to get the next page.
options.PageNumber = list.NextPage
}

return nil, fmt.Errorf("workspace %s has not been assigned to policy set %s", wsName, pSName)
}
149 changes: 149 additions & 0 deletions tfe/resource_tfe_workspace_policy_set_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package tfe

import (
"fmt"
"math/rand"
"regexp"
"testing"
"time"

tfe "github.com/hashicorp/go-tfe"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

func TestAccTFEWorkspacePolicySet_basic(t *testing.T) {
skipIfFreeOnly(t)

rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckTFEWorkspacePolicySetDestroy,
Steps: []resource.TestStep{
{
Config: testAccTFEWorkspacePolicySet_basic(rInt),
Check: resource.ComposeTestCheckFunc(
testAccCheckTFEWorkspacePolicySetExists(
"tfe_workspace_policy_set.test"),
),
},
{
ResourceName: "tfe_workspace_policy_set.test",
ImportState: true,
ImportStateId: fmt.Sprintf("tst-terraform-%d/tst-terraform-%d/tst-policy-set-%d", rInt, rInt, rInt),
ImportStateVerify: true,
},
},
})
}

func TestAccTFEWorkspacePolicySet_incorrectImportSyntax(t *testing.T) {
skipIfFreeOnly(t)

rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
Steps: []resource.TestStep{
{
Config: testAccTFEWorkspacePolicySet_basic(rInt),
},
{
ResourceName: "tfe_workspace_policy_set.test",
ImportState: true,
ImportStateId: fmt.Sprintf("tst-terraform-%d/tst-terraform-%d", rInt, rInt),
ExpectError: regexp.MustCompile(`Error: invalid workspace policy set input format`),
},
},
})
}

func testAccCheckTFEWorkspacePolicySetExists(n string) resource.TestCheckFunc {
return func(s *terraform.State) error {
tfeClient := testAccProvider.Meta().(*tfe.Client)

rs, ok := s.RootModule().Resources[n]
if !ok {
return fmt.Errorf("Not found: %s", n)
}

id := rs.Primary.ID
if id == "" {
return fmt.Errorf("No ID is set")
}

policySetID := rs.Primary.Attributes["policy_set_id"]
if policySetID == "" {
return fmt.Errorf("No policy set id set")
}

workspaceID := rs.Primary.Attributes["workspace_id"]
if workspaceID == "" {
return fmt.Errorf("No workspace id set")
}

policySet, err := tfeClient.PolicySets.ReadWithOptions(ctx, policySetID, &tfe.PolicySetReadOptions{
Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetWorkspaces},
})
if err != nil {
return fmt.Errorf("error reading polciy set %s: %w", policySetID, err)
}
for _, workspace := range policySet.Workspaces {
if workspace.ID == workspaceID {
return nil
}
}

return fmt.Errorf("Workspace (%s) is not attached to policy set (%s).", workspaceID, policySetID)
}
}

func testAccCheckTFEWorkspacePolicySetDestroy(s *terraform.State) error {
tfeClient := testAccProvider.Meta().(*tfe.Client)

for _, rs := range s.RootModule().Resources {
if rs.Type != "tfe_policy_set" {
continue
}

if rs.Primary.ID == "" {
return fmt.Errorf("No instance ID is set")
}

_, err := tfeClient.PolicySets.Read(ctx, rs.Primary.ID)
if err == nil {
return fmt.Errorf("Policy Set %s still exists", rs.Primary.ID)
}
}

return nil
}

func testAccTFEWorkspacePolicySet_basic(rInt int) string {
return fmt.Sprintf(`
resource "tfe_organization" "test" {
name = "tst-terraform-%d"
email = "admin@company.com"
}
resource "tfe_workspace" "test" {
name = "tst-terraform-%d"
organization = tfe_organization.test.id
auto_apply = true
tag_names = ["test"]
}
resource "tfe_policy_set" "test" {
name = "tst-policy-set-%d"
description = "Policy Set"
organization = tfe_organization.test.id
}
resource "tfe_workspace_policy_set" "test" {
policy_set_id = tfe_policy_set.test.id
workspace_id = tfe_workspace.test.id
}`, rInt, rInt, rInt)
}
Loading

0 comments on commit b38c264

Please sign in to comment.