diff --git a/CHANGELOG.md b/CHANGELOG.md index ef24a0cb9..51b7782ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ FEATURES: * r/tfe_workspace: Add preemptive check for resources under management when `force_delete` attribute is false ([#699](https://github.com/hashicorp/terraform-provider-tfe/pull/699)) * r/tfe_policy: Add OPA support for policies. `tfe_policy` is a new resource that supports both Sentinel as well as OPA policies. `tfe_sentinel_policy` now includes a deprecation warning. ([#690](https://github.com/hashicorp/terraform-provider-tfe/pull/690)) +* r/tfe_policy_set: Add OPA support for policy sets. ([#691](https://github.com/hashicorp/terraform-provider-tfe/pull/691)) +* d/tfe_policy_set: Add optional `kind` and `overridable` fields for OPA policy sets ([#691](https://github.com/hashicorp/terraform-provider-tfe/pull/691)) ## v0.39.0 (November 18, 2022) diff --git a/go.mod b/go.mod index fc58bfbfb..a5922f831 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,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.1 - github.com/hashicorp/go-tfe v1.13.0 + github.com/hashicorp/go-tfe v1.14.0 github.com/hashicorp/go-version v1.6.0 github.com/hashicorp/hcl v0.0.0-20180404174102-ef8a98b0bbce github.com/hashicorp/hcl/v2 v2.15.0 // indirect diff --git a/go.sum b/go.sum index 042c010e8..e3d163c80 100644 --- a/go.sum +++ b/go.sum @@ -163,8 +163,8 @@ github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1 github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-slug v0.10.1 h1:05SCRWCBpCxOeP7stQHvMgOz0raCBCekaytu8Rg/RZ4= github.com/hashicorp/go-slug v0.10.1/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4= -github.com/hashicorp/go-tfe v1.13.0 h1:Z8pJrmN9BV5EncnFRmQmLjhP7pHMNXkC2VkCQXWxKjM= -github.com/hashicorp/go-tfe v1.13.0/go.mod h1:thYtIxtgBpDDNdf/2yYPdBJ94Fz5yT5XCNZvGtTGHAU= +github.com/hashicorp/go-tfe v1.14.0 h1:FZKKkwlyTxw8/OE3e7NiFQLcgGXTHra9ogGhMTotxh8= +github.com/hashicorp/go-tfe v1.14.0/go.mod h1:77snluBqtTTvMrY0w/mxQA5jlHQ8NT44AqQ8UdrPf0o= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= diff --git a/tfe/data_source_policy_set.go b/tfe/data_source_policy_set.go index 5c8625993..e827671e3 100644 --- a/tfe/data_source_policy_set.go +++ b/tfe/data_source_policy_set.go @@ -33,6 +33,18 @@ func dataSourceTFEPolicySet() *schema.Resource { Computed: true, }, + "kind": { + Description: "The policy-as-code framework for the policy. Valid values are sentinel and opa", + Type: schema.TypeString, + Optional: true, + }, + + "overridable": { + Description: "Whether users can override this policy when it fails during a run. Only valid for OPA policies", + Type: schema.TypeBool, + Optional: true, + }, + "policies_path": { Type: schema.TypeString, Computed: true, @@ -100,6 +112,7 @@ func dataSourceTFEPolicySetRead(d *schema.ResourceData, meta interface{}) error } for _, policySet := range policySetList.Items { + // nolint: nestif if policySet.Name == name { d.Set("name", policySet.Name) d.Set("description", policySet.Description) @@ -110,6 +123,14 @@ func dataSourceTFEPolicySetRead(d *schema.ResourceData, meta interface{}) error d.Set("organization", policySet.Organization.Name) } + if policySet.Kind != "" { + d.Set("kind", policySet.Kind) + } + + if policySet.Overridable != nil { + d.Set("overridable", policySet.Overridable) + } + var vcsRepo []interface{} if policySet.VCSRepo != nil { vcsRepo = append(vcsRepo, map[string]interface{}{ diff --git a/tfe/data_source_policy_set_test.go b/tfe/data_source_policy_set_test.go index cdc92eb5b..760cd9b65 100644 --- a/tfe/data_source_policy_set_test.go +++ b/tfe/data_source_policy_set_test.go @@ -50,6 +50,49 @@ func TestAccTFEPolicySetDataSource_basic(t *testing.T) { ) } +func TestAccTFEPolicySetDataSourceOPA_basic(t *testing.T) { + skipUnlessBeta(t) + tfeClient, err := getClientUsingEnv() + if err != nil { + t.Fatal(err) + } + + org, orgCleanup := createBusinessOrganization(t, tfeClient) + t.Cleanup(orgCleanup) + + rInt := rand.New(rand.NewSource(time.Now().UnixNano())).Int() + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccTFEPolicySetDataSourceConfigOPA_basic(org.Name, rInt), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttrSet("data.tfe_policy_set.bar", "id"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "name", fmt.Sprintf("tst-policy-set-%d", rInt)), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "description", "Policy Set"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "global", "false"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "organization", org.Name), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "kind", "opa"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "overridable", "true"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "workspace_ids.#", "1"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "vcs_repo.#", "0"), + ), + }, + }, + }, + ) +} + func TestAccTFEPolicySetDataSource_vcs(t *testing.T) { tfeClient, err := getClientUsingEnv() if err != nil { @@ -90,6 +133,8 @@ func TestAccTFEPolicySetDataSource_vcs(t *testing.T) { "data.tfe_policy_set.bar", "description", "Policy Set"), resource.TestCheckResourceAttr( "data.tfe_policy_set.bar", "global", "false"), + resource.TestCheckResourceAttr( + "data.tfe_policy_set.bar", "kind", "sentinel"), resource.TestCheckResourceAttr( "data.tfe_policy_set.bar", "organization", org.Name), resource.TestCheckResourceAttr( @@ -152,6 +197,33 @@ data "tfe_policy_set" "bar" { }`, organization, rInt, rInt) } +func testAccTFEPolicySetDataSourceConfigOPA_basic(organization string, rInt int) string { + return fmt.Sprintf(` +locals { + organization_name = "%s" +} + +resource "tfe_workspace" "foobar" { + name = "workspace-foo-%d" + organization = local.organization_name +} + +resource "tfe_policy_set" "foobar" { + name = "tst-policy-set-%d" + description = "Policy Set" + organization = local.organization_name + kind = "opa" + overridable = true + workspace_ids = [tfe_workspace.foobar.id] +} + +data "tfe_policy_set" "bar" { + name = tfe_policy_set.foobar.name + organization = local.organization_name + kind = "opa" +}`, organization, rInt, rInt) +} + func testAccTFEPolicySetDataSourceConfig_vcs(organization string, rInt int) string { return fmt.Sprintf(` locals { diff --git a/tfe/resource_tfe_policy_set.go b/tfe/resource_tfe_policy_set.go index 5cfe590d4..2bfb5260d 100644 --- a/tfe/resource_tfe_policy_set.go +++ b/tfe/resource_tfe_policy_set.go @@ -46,6 +46,24 @@ func resourceTFEPolicySet() *schema.Resource { ConflictsWith: []string{"workspace_ids"}, }, + "kind": { + Type: schema.TypeString, + Optional: true, + Default: string(tfe.Sentinel), + ForceNew: true, + ValidateFunc: validation.StringInSlice( + []string{ + string(tfe.OPA), + string(tfe.Sentinel), + }, false), + }, + + "overridable": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, + "policies_path": { Type: schema.TypeString, Optional: true, @@ -123,6 +141,14 @@ func resourceTFEPolicySetCreate(d *schema.ResourceData, meta interface{}) error } // Process all configured options. + if vKind, ok := d.GetOk("kind"); ok { + options.Kind = tfe.PolicyKind(vKind.(string)) + } + + if vOverridable, ok := d.GetOk("overridable"); ok { + options.Overridable = tfe.Bool(vOverridable.(bool)) + } + if desc, ok := d.GetOk("description"); ok { options.Description = tfe.String(desc.(string)) } @@ -199,6 +225,15 @@ func resourceTFEPolicySetRead(d *schema.ResourceData, meta interface{}) error { d.Set("organization", policySet.Organization.Name) } + // Note: Old API endpoints return an empty string, so use the default in the schema + if policySet.Kind != "" { + d.Set("kind", policySet.Kind) + } + + if policySet.Overridable != nil { + d.Set("overridable", policySet.Overridable) + } + // Set VCS policy set options. var vcsRepo []interface{} if policySet.VCSRepo != nil { @@ -271,7 +306,7 @@ func resourceTFEPolicySetUpdate(d *schema.ResourceData, meta interface{}) error } // Don't bother updating the policy set's attributes if they haven't changed - if d.HasChange("name") || d.HasChange("description") || d.HasChange("global") || d.HasChange("vcs_repo") { + if d.HasChange("name") || d.HasChange("description") || d.HasChange("global") || d.HasChange("vcs_repo") || d.HasChange("overridable") { // Create a new options struct. options := tfe.PolicySetUpdateOptions{ Name: tfe.String(name), @@ -282,6 +317,11 @@ func resourceTFEPolicySetUpdate(d *schema.ResourceData, meta interface{}) error options.Description = tfe.String(desc.(string)) } + if d.HasChange("overridable") { + o := d.Get("overridable").(bool) + options.Overridable = tfe.Bool(o) + } + if v, ok := d.GetOk("vcs_repo"); ok { vcsRepo := v.([]interface{})[0].(map[string]interface{}) diff --git a/tfe/resource_tfe_policy_set_test.go b/tfe/resource_tfe_policy_set_test.go index 08e6a66d0..b6a88cd3b 100644 --- a/tfe/resource_tfe_policy_set_test.go +++ b/tfe/resource_tfe_policy_set_test.go @@ -46,6 +46,99 @@ func TestAccTFEPolicySet_basic(t *testing.T) { }) } +func TestAccTFEPolicySetOPA_basic(t *testing.T) { + skipUnlessBeta(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) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEPolicySetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEPolicySetOPA_basic(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEPolicySetExists("tfe_policy_set.foobar", policySet), + testAccCheckTFEPolicySetAttributes(policySet), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "name", "tst-terraform"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "opa"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "overridable", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "description", "Policy Set"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "global", "false"), + ), + }, + }, + }) +} + +func TestAccTFEPolicySet_updateOverridable(t *testing.T) { + skipUnlessBeta(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) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckTFEPolicySetDestroy, + Steps: []resource.TestStep{ + { + Config: testAccTFEPolicySetOPA_basic(org.Name), + Check: resource.ComposeTestCheckFunc( + testAccCheckTFEPolicySetExists("tfe_policy_set.foobar", policySet), + testAccCheckTFEPolicySetAttributes(policySet), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "name", "tst-terraform"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "description", "Policy Set"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "opa"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "overridable", "true"), + ), + }, + + { + Config: testAccTFEPolicySetOPA_overridable(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", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "opa"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "workspace_ids.#", "1"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "overridable", "false"), + ), + }, + }, + }) +} + func TestAccTFEPolicySet_update(t *testing.T) { tfeClient, err := getClientUsingEnv() if err != nil { @@ -87,6 +180,8 @@ func TestAccTFEPolicySet_update(t *testing.T) { "tfe_policy_set.foobar", "name", "terraform-populated"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "policy_ids.#", "1"), resource.TestCheckResourceAttr( @@ -189,6 +284,8 @@ func TestAccTFEPolicySet_updatePopulated(t *testing.T) { "tfe_policy_set.foobar", "name", "terraform-populated-updated"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "policy_ids.#", "1"), resource.TestCheckResourceAttr( @@ -240,6 +337,8 @@ func TestAccTFEPolicySet_updateToGlobal(t *testing.T) { "tfe_policy_set.foobar", "name", "terraform-global"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "true"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "policy_ids.#", "1"), ), @@ -338,6 +437,8 @@ func TestAccTFEPolicySet_vcs(t *testing.T) { "tfe_policy_set.foobar", "description", "Policy Set"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "vcs_repo.0.identifier", GITHUB_POLICY_SET_IDENTIFIER), resource.TestCheckResourceAttr( @@ -393,6 +494,8 @@ func TestAccTFEPolicySet_updateVCSBranch(t *testing.T) { "tfe_policy_set.foobar", "description", "Policy Set"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "vcs_repo.0.identifier", GITHUB_POLICY_SET_IDENTIFIER), resource.TestCheckResourceAttr( @@ -415,6 +518,8 @@ func TestAccTFEPolicySet_updateVCSBranch(t *testing.T) { "tfe_policy_set.foobar", "description", "Policy Set"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "global", "false"), + resource.TestCheckResourceAttr( + "tfe_policy_set.foobar", "kind", "sentinel"), resource.TestCheckResourceAttr( "tfe_policy_set.foobar", "vcs_repo.0.identifier", GITHUB_POLICY_SET_IDENTIFIER), resource.TestCheckResourceAttr( @@ -628,6 +733,9 @@ func TestAccTFEPolicySetImport(t *testing.T) { ResourceName: "tfe_policy_set.foobar", ImportState: true, ImportStateVerify: true, + // Note: We ignore the optional fields below, since the old API endpoints send empty values + // and the results may vary depending on the API version + ImportStateVerifyIgnore: []string{"kind", "overridable"}, }, }, }) @@ -819,6 +927,17 @@ resource "tfe_policy_set" "foobar" { }`, organization, organization) } +func testAccTFEPolicySetOPA_basic(organization string) string { + return fmt.Sprintf(` +resource "tfe_policy_set" "foobar" { + name = "tst-terraform" + description = "Policy Set" + organization = "%s" + kind = "opa" + overridable = "true" +}`, organization) +} + func testAccTFEPolicySet_empty(organization string) string { return fmt.Sprintf(` resource "tfe_policy_set" "foobar" { @@ -853,6 +972,26 @@ resource "tfe_policy_set" "foobar" { }`, organization) } +func testAccTFEPolicySetOPA_overridable(organization string) string { + return fmt.Sprintf(` +locals { + organization_name = "%s" +} + +resource "tfe_workspace" "foo" { + name = "workspace-foo" + organization = local.organization_name +} + +resource "tfe_policy_set" "foobar" { + name = "tst-terraform" + organization = local.organization_name + workspace_ids = [tfe_workspace.foo.id] + overridable = "false" + kind = "opa" +}`, organization) +} + func testAccTFEPolicySet_updatePopulated(organization string) string { return fmt.Sprintf(` locals {