Skip to content

Commit 3ca6281

Browse files
authored
Merge pull request #972 from hashicorp/Netra2104/TF-6273-add-project-policy-set-resource
TF-6273 Add a new project_policy_set resource
2 parents 03776af + f4c9169 commit 3ca6281

File tree

5 files changed

+407
-0
lines changed

5 files changed

+407
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
FEATURES:
44
* **New Resource**: `r/tfe_saml_settings` manages SAML Settings, by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970)
55
* `d/tfe_saml_settings`: Add PrivateKey (sensitive), SignatureSigningMethod, and SignatureDigestMethod attributes, by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970)
6+
* **New Resource**: `r/tfe_project_policy_set` is a new resource to attach/detach an existing `project` to an existing `policy set`, by @Netra2104 [972](https://github.com/hashicorp/terraform-provider-tfe/pull/972)
67

78
NOTES:
89
* The provider is now using go-tfe [v1.30.0](https://github.com/hashicorp/go-tfe/releases/tag/v1.30.0), by @karvounis-form3 [970](https://github.com/hashicorp/terraform-provider-tfe/pull/970)

tfe/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ func Provider() *schema.Provider {
149149
"tfe_policy_set": resourceTFEPolicySet(),
150150
"tfe_policy_set_parameter": resourceTFEPolicySetParameter(),
151151
"tfe_project": resourceTFEProject(),
152+
"tfe_project_policy_set": resourceTFEProjectPolicySet(),
152153
"tfe_project_variable_set": resourceTFEProjectVariableSet(),
153154
"tfe_registry_module": resourceTFERegistryModule(),
154155
"tfe_no_code_module": resourceTFENoCodeModule(),
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfe
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"log"
11+
"strings"
12+
13+
tfe "github.com/hashicorp/go-tfe"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
15+
)
16+
17+
func resourceTFEProjectPolicySet() *schema.Resource {
18+
return &schema.Resource{
19+
Create: resourceTFEProjectPolicySetCreate,
20+
Read: resourceTFEProjectPolicySetRead,
21+
Delete: resourceTFEProjectPolicySetDelete,
22+
Importer: &schema.ResourceImporter{
23+
StateContext: resourceTFEProjectPolicySetImporter,
24+
},
25+
26+
Schema: map[string]*schema.Schema{
27+
"policy_set_id": {
28+
Type: schema.TypeString,
29+
Required: true,
30+
ForceNew: true,
31+
},
32+
33+
"project_id": {
34+
Type: schema.TypeString,
35+
Required: true,
36+
ForceNew: true,
37+
},
38+
},
39+
}
40+
}
41+
42+
func resourceTFEProjectPolicySetCreate(d *schema.ResourceData, meta interface{}) error {
43+
config := meta.(ConfiguredClient)
44+
45+
policySetID := d.Get("policy_set_id").(string)
46+
projectID := d.Get("project_id").(string)
47+
48+
policySetAddProjectsOptions := tfe.PolicySetAddProjectsOptions{}
49+
policySetAddProjectsOptions.Projects = append(policySetAddProjectsOptions.Projects, &tfe.Project{ID: projectID})
50+
51+
err := config.Client.PolicySets.AddProjects(ctx, policySetID, policySetAddProjectsOptions)
52+
if err != nil {
53+
return fmt.Errorf(
54+
"error attaching policy set id %s to project %s: %w", policySetID, projectID, err)
55+
}
56+
57+
d.SetId(fmt.Sprintf("%s_%s", projectID, policySetID))
58+
59+
return resourceTFEProjectPolicySetRead(d, meta)
60+
}
61+
62+
func resourceTFEProjectPolicySetRead(d *schema.ResourceData, meta interface{}) error {
63+
config := meta.(ConfiguredClient)
64+
65+
policySetID := d.Get("policy_set_id").(string)
66+
projectID := d.Get("project_id").(string)
67+
68+
log.Printf("[DEBUG] Read configuration of project policy set: %s", policySetID)
69+
policySet, err := config.Client.PolicySets.ReadWithOptions(ctx, policySetID, &tfe.PolicySetReadOptions{
70+
Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetProjects},
71+
})
72+
if err != nil {
73+
if errors.Is(err, tfe.ErrResourceNotFound) {
74+
log.Printf("[DEBUG] Policy set %s no longer exists", policySetID)
75+
d.SetId("")
76+
return nil
77+
}
78+
return fmt.Errorf("error reading configuration of policy set %s: %w", policySetID, err)
79+
}
80+
81+
isProjectAttached := false
82+
for _, project := range policySet.Projects {
83+
if project.ID == projectID {
84+
isProjectAttached = true
85+
d.Set("project_id", projectID)
86+
break
87+
}
88+
}
89+
90+
if !isProjectAttached {
91+
log.Printf("[DEBUG] Project %s not attached to policy set %s. Removing from state.", projectID, policySetID)
92+
d.SetId("")
93+
return nil
94+
}
95+
96+
d.Set("policy_set_id", policySetID)
97+
return nil
98+
}
99+
100+
func resourceTFEProjectPolicySetDelete(d *schema.ResourceData, meta interface{}) error {
101+
config := meta.(ConfiguredClient)
102+
103+
policySetID := d.Get("policy_set_id").(string)
104+
projectID := d.Get("project_id").(string)
105+
106+
log.Printf("[DEBUG] Detaching project (%s) from policy set (%s)", projectID, policySetID)
107+
policySetRemoveProjectsOptions := tfe.PolicySetRemoveProjectsOptions{}
108+
policySetRemoveProjectsOptions.Projects = append(policySetRemoveProjectsOptions.Projects, &tfe.Project{ID: projectID})
109+
110+
err := config.Client.PolicySets.RemoveProjects(ctx, policySetID, policySetRemoveProjectsOptions)
111+
if err != nil {
112+
return fmt.Errorf(
113+
"error detaching project %s from policy set %s: %w", projectID, policySetID, err)
114+
}
115+
116+
return nil
117+
}
118+
119+
func resourceTFEProjectPolicySetImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
120+
// The format of the import ID is <ORGANIZATION/PROJECT ID/POLICYSET NAME>
121+
splitID := strings.SplitN(d.Id(), "/", 3)
122+
if len(splitID) != 3 {
123+
return nil, fmt.Errorf(
124+
"invalid project policy set input format: %s (expected <ORGANIZATION>/<PROJECT ID>/<POLICYSET NAME>)",
125+
splitID,
126+
)
127+
}
128+
129+
organization, projectID, policySetName := splitID[0], splitID[1], splitID[2]
130+
131+
config := meta.(ConfiguredClient)
132+
133+
// Ensure the named project exists before fetching all the policy sets in the org
134+
_, err := config.Client.Projects.Read(ctx, projectID)
135+
if err != nil {
136+
return nil, fmt.Errorf("error reading configuration of project %s in organization %s: %w", projectID, organization, err)
137+
}
138+
139+
options := &tfe.PolicySetListOptions{Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetProjects}}
140+
for {
141+
list, err := config.Client.PolicySets.List(ctx, organization, options)
142+
if err != nil {
143+
return nil, fmt.Errorf("error retrieving organization's list of policy sets: %w", err)
144+
}
145+
for _, policySet := range list.Items {
146+
if policySet.Name != policySetName {
147+
continue
148+
}
149+
150+
for _, project := range policySet.Projects {
151+
if project.ID != projectID {
152+
continue
153+
}
154+
155+
d.Set("project_id", project.ID)
156+
d.Set("policy_set_id", policySet.ID)
157+
d.SetId(fmt.Sprintf("%s_%s", project.ID, policySet.ID))
158+
159+
return []*schema.ResourceData{d}, nil
160+
}
161+
}
162+
163+
// Exit the loop when we've seen all pages.
164+
if list.CurrentPage >= list.TotalPages {
165+
break
166+
}
167+
168+
// Update the page number to get the next page.
169+
options.PageNumber = list.NextPage
170+
}
171+
172+
return nil, fmt.Errorf("project %s has not been assigned to policy set %s", projectID, policySetName)
173+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package tfe
5+
6+
import (
7+
"fmt"
8+
"math/rand"
9+
"regexp"
10+
"testing"
11+
"time"
12+
13+
tfe "github.com/hashicorp/go-tfe"
14+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
15+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
16+
)
17+
18+
func TestAccTFEProjectPolicySet_basic(t *testing.T) {
19+
skipUnlessBeta(t)
20+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
21+
22+
tfeClient, err := getClientUsingEnv()
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
27+
org, orgCleanup := createOrganization(t, tfeClient, tfe.OrganizationCreateOptions{
28+
Name: tfe.String(fmt.Sprintf("tst-terraform-%d", rInt)),
29+
Email: tfe.String(fmt.Sprintf("%[email protected]", randomString(t))),
30+
})
31+
t.Cleanup(orgCleanup)
32+
33+
// Make a project
34+
project := createProject(t, tfeClient, org.Name, tfe.ProjectCreateOptions{
35+
Name: randomString(t),
36+
})
37+
38+
resource.Test(t, resource.TestCase{
39+
PreCheck: func() { testAccPreCheck(t) },
40+
Providers: testAccProviders,
41+
CheckDestroy: testAccCheckTFEProjectPolicySetDestroy,
42+
Steps: []resource.TestStep{
43+
{
44+
Config: testAccTFEProjectPolicySet_basic(org.Name, project.ID),
45+
Check: resource.ComposeTestCheckFunc(
46+
testAccCheckTFEProjectPolicySetExists(
47+
"tfe_project_policy_set.test"),
48+
),
49+
},
50+
{
51+
ResourceName: "tfe_project_policy_set.test",
52+
ImportState: true,
53+
ImportStateId: fmt.Sprintf("%s/%s/policy_set_test", org.Name, project.ID),
54+
ImportStateVerify: true,
55+
},
56+
},
57+
})
58+
}
59+
60+
func TestAccTFEProjectPolicySet_incorrectImportSyntax(t *testing.T) {
61+
skipUnlessBeta(t)
62+
rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int()
63+
64+
tfeClient, err := getClientUsingEnv()
65+
if err != nil {
66+
t.Fatal(err)
67+
}
68+
69+
org, orgCleanup := createOrganization(t, tfeClient, tfe.OrganizationCreateOptions{
70+
Name: tfe.String(fmt.Sprintf("tst-terraform-%d", rInt)),
71+
Email: tfe.String(fmt.Sprintf("%[email protected]", randomString(t))),
72+
})
73+
t.Cleanup(orgCleanup)
74+
75+
// Make a project
76+
project := createProject(t, tfeClient, org.Name, tfe.ProjectCreateOptions{
77+
Name: randomString(t),
78+
})
79+
80+
resource.Test(t, resource.TestCase{
81+
PreCheck: func() { testAccPreCheck(t) },
82+
Providers: testAccProviders,
83+
Steps: []resource.TestStep{
84+
{
85+
Config: testAccTFEProjectPolicySet_basic(org.Name, project.ID),
86+
},
87+
{
88+
ResourceName: "tfe_project_policy_set.test",
89+
ImportState: true,
90+
ImportStateId: fmt.Sprintf("%s/tst-terraform-%d", org.Name, rInt),
91+
ExpectError: regexp.MustCompile(`Error: invalid project policy set input format`),
92+
},
93+
},
94+
})
95+
}
96+
97+
func testAccCheckTFEProjectPolicySetExists(n string) resource.TestCheckFunc {
98+
return func(s *terraform.State) error {
99+
config := testAccProvider.Meta().(ConfiguredClient)
100+
101+
rs, ok := s.RootModule().Resources[n]
102+
if !ok {
103+
return fmt.Errorf("not found: %s", n)
104+
}
105+
106+
id := rs.Primary.ID
107+
if id == "" {
108+
return fmt.Errorf("no ID is set")
109+
}
110+
111+
policySetID := rs.Primary.Attributes["policy_set_id"]
112+
if policySetID == "" {
113+
return fmt.Errorf("no policy set id set")
114+
}
115+
116+
projectID := rs.Primary.Attributes["project_id"]
117+
if projectID == "" {
118+
return fmt.Errorf("no project id set")
119+
}
120+
121+
policySet, err := config.Client.PolicySets.ReadWithOptions(ctx, policySetID, &tfe.PolicySetReadOptions{
122+
Include: []tfe.PolicySetIncludeOpt{tfe.PolicySetProjects},
123+
})
124+
if err != nil {
125+
return fmt.Errorf("error reading policy set %s: %w", policySetID, err)
126+
}
127+
for _, project := range policySet.Projects {
128+
if project.ID == projectID {
129+
return nil
130+
}
131+
}
132+
133+
return fmt.Errorf("project (%s) is not attached to policy set (%s).", projectID, policySetID)
134+
}
135+
}
136+
137+
func testAccCheckTFEProjectPolicySetDestroy(s *terraform.State) error {
138+
config := testAccProvider.Meta().(ConfiguredClient)
139+
140+
for _, rs := range s.RootModule().Resources {
141+
if rs.Type != "tfe_policy_set" {
142+
continue
143+
}
144+
145+
if rs.Primary.ID == "" {
146+
return fmt.Errorf("no instance ID is set")
147+
}
148+
149+
_, err := config.Client.PolicySets.Read(ctx, rs.Primary.ID)
150+
if err == nil {
151+
return fmt.Errorf("policy set %s still exists", rs.Primary.ID)
152+
}
153+
}
154+
155+
return nil
156+
}
157+
158+
func testAccTFEProjectPolicySet_base(orgName string) string {
159+
return fmt.Sprintf(`
160+
resource "tfe_policy_set" "test" {
161+
name = "policy_set_test"
162+
description = "a test policy set"
163+
global = false
164+
organization = "%s"
165+
}
166+
`, orgName)
167+
}
168+
169+
func testAccTFEProjectPolicySet_basic(orgName string, prjID string) string {
170+
return testAccTFEProjectPolicySet_base(orgName) + fmt.Sprintf(`
171+
resource "tfe_project_policy_set" "test" {
172+
policy_set_id = tfe_policy_set.test.id
173+
project_id = "%s"
174+
}
175+
`, prjID)
176+
}

0 commit comments

Comments
 (0)