Skip to content

Commit

Permalink
Merge pull request #690 from hashicorp/mr/TF-1451-policies
Browse files Browse the repository at this point in the history
Add OPA support to the provider for Policies
  • Loading branch information
mrinalirao authored Nov 21, 2022
2 parents b13737b + 972d5a6 commit ec6b82a
Show file tree
Hide file tree
Showing 6 changed files with 872 additions and 4 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## Unreleased

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))

## v0.39.0 (November 18, 2022)

Expand Down
1 change: 1 addition & 0 deletions tfe/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ func Provider() *schema.Provider {
"tfe_organization_module_sharing": resourceTFEOrganizationModuleSharing(),
"tfe_organization_run_task": resourceTFEOrganizationRunTask(),
"tfe_organization_token": resourceTFEOrganizationToken(),
"tfe_policy": resourceTFEPolicy(),
"tfe_policy_set": resourceTFEPolicySet(),
"tfe_policy_set_parameter": resourceTFEPolicySetParameter(),
"tfe_registry_module": resourceTFERegistryModule(),
Expand Down
334 changes: 334 additions & 0 deletions tfe/resource_tfe_policy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
package tfe

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

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

func resourceTFEPolicy() *schema.Resource {
return &schema.Resource{
Create: resourceTFEPolicyCreate,
Read: resourceTFEPolicyRead,
Update: resourceTFEPolicyUpdate,
Delete: resourceTFEPolicyDelete,

Importer: &schema.ResourceImporter{
StateContext: resourceTFEPolicyImporter,
},

Schema: map[string]*schema.Schema{
"name": {
Description: "The name of the policy",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"description": {
Description: "Text describing the policy's purpose",
Type: schema.TypeString,
Optional: true,
},

"organization": {
Description: "Name of the organization that this policy belongs to",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},

"kind": {
Description: "The policy-as-code framework for the policy. Valid values are sentinel and opa",
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Default: string(tfe.Sentinel),
ValidateFunc: validation.StringInSlice(
[]string{
string(tfe.OPA),
string(tfe.Sentinel),
}, false),
},

"query": {
Description: "The OPA query to run. Required for OPA policies",
Type: schema.TypeString,
Optional: true,
},

"policy": {
Description: "Text of a valid Sentinel or OPA policy",
Type: schema.TypeString,
Required: true,
},

"enforce_mode": {
Type: schema.TypeString,
Description: fmt.Sprintf(
"The enforcement configuration of the policy. For Sentinel, valid values are %s. For OPA, Valid values are `%s`", sentenceList(
sentinelPolicyEnforcementLevels(),
"`",
"`",
"and"),
sentenceList(
opaPolicyEnforcementLevels(),
"`",
"`",
"and")),
Optional: true,
Computed: true,
ValidateFunc: validation.StringInSlice(
[]string{
string(tfe.EnforcementAdvisory),
string(tfe.EnforcementHard),
string(tfe.EnforcementSoft),
string(tfe.EnforcementMandatory),
},
false,
),
},
},
}
}

func sentinelPolicyEnforcementLevels() []string {
return []string{
string(tfe.EnforcementHard),
string(tfe.EnforcementSoft),
string(tfe.EnforcementAdvisory),
}
}

func opaPolicyEnforcementLevels() []string {
return []string{
string(tfe.EnforcementMandatory),
string(tfe.EnforcementAdvisory),
}
}

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

// Get the name and organization.
name := d.Get("name").(string)
organization := d.Get("organization").(string)

var kind string
if vKind, ok := d.GetOk("kind"); ok {
kind = vKind.(string)
}

// Setup common policy options
options := &tfe.PolicyCreateOptions{
Name: tfe.String(name),
Kind: tfe.PolicyKind(kind),
}

if desc, ok := d.GetOk("description"); ok {
options.Description = tfe.String(desc.(string))
}

var err error
// Setup per-kind policy options
switch tfe.PolicyKind(kind) {
case tfe.Sentinel:
options = createSentinelPolicyOptions(options, d)
case tfe.OPA:
options, err = createOPAPolicyOptions(options, d)
default:
err = fmt.Errorf(
"Unsupported policy kind %s: has to be one of [%s, %s]", kind, string(tfe.Sentinel), string(tfe.OPA))
}
if err != nil {
return err
}
log.Printf("[DEBUG] Create %s policy %s for organization: %s", kind, name, organization)
policy, err := tfeClient.Policies.Create(ctx, organization, *options)
if err != nil {
return fmt.Errorf(
"Error creating %s policy %s for organization %s: %w", kind, name, organization, err)
}

d.SetId(policy.ID)

log.Printf("[DEBUG] Upload %s policy %s for organization: %s", kind, name, organization)
err = tfeClient.Policies.Upload(ctx, policy.ID, []byte(d.Get("policy").(string)))
if err != nil {
return fmt.Errorf(
"Error uploading %s policy %s for organization %s: %w", kind, name, organization, err)
}

return resourceTFEPolicyRead(d, meta)
}

func createOPAPolicyOptions(options *tfe.PolicyCreateOptions, d *schema.ResourceData) (*tfe.PolicyCreateOptions, error) {
name := d.Get("name").(string)
path := name + ".rego"
enforceOpts := &tfe.EnforcementOptions{
Path: tfe.String(path),
}

if v, ok := d.GetOk("enforce_mode"); !ok {
enforceOpts.Mode = tfe.EnforcementMode(getDefaultEnforcementMode(tfe.OPA))
} else {
enforceOpts.Mode = tfe.EnforcementMode(tfe.EnforcementLevel(v.(string)))
}

options.Enforce = []*tfe.EnforcementOptions{enforceOpts}

vQuery, ok := d.GetOk("query")
if !ok {
return options, fmt.Errorf("Missing query for OPA policy.")
}
options.Query = tfe.String(vQuery.(string))

return options, nil
}

func createSentinelPolicyOptions(options *tfe.PolicyCreateOptions, d *schema.ResourceData) *tfe.PolicyCreateOptions {
name := d.Get("name").(string)
path := name + ".sentinel"
enforceOpts := &tfe.EnforcementOptions{
Path: tfe.String(path),
}

if v, ok := d.GetOk("enforce_mode"); !ok {
enforceOpts.Mode = tfe.EnforcementMode(getDefaultEnforcementMode(tfe.Sentinel))
} else {
enforceOpts.Mode = tfe.EnforcementMode(tfe.EnforcementLevel(v.(string)))
}

options.Enforce = []*tfe.EnforcementOptions{enforceOpts}
return options
}

func getDefaultEnforcementMode(kind tfe.PolicyKind) tfe.EnforcementLevel {
switch kind {
case tfe.Sentinel:
return tfe.EnforcementSoft

case tfe.OPA:
return tfe.EnforcementAdvisory

default:
return ""
}
}

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

log.Printf("[DEBUG] Read policy: %s", d.Id())
policy, err := tfeClient.Policies.Read(ctx, d.Id())
if err != nil {
if errors.Is(err, tfe.ErrResourceNotFound) {
log.Printf("[DEBUG] Policy %s does no longer exist", d.Id())
d.SetId("")
return nil
}
return fmt.Errorf("Error reading Policy %s: %w", d.Id(), err)
}

// Update the config.
d.Set("name", policy.Name)
d.Set("description", policy.Description)
d.Set("kind", policy.Kind)

if len(policy.Enforce) == 1 {
d.Set("enforce_mode", string(policy.Enforce[0].Mode))
}

content, err := tfeClient.Policies.Download(ctx, policy.ID)
if err != nil {
return fmt.Errorf("Error downloading policy %s: %w", d.Id(), err)
}
d.Set("policy", string(content))

return nil
}

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

// nolint:nestif
if d.HasChange("description") || d.HasChange("enforce_mode") {
// Create a new options struct.
options := tfe.PolicyUpdateOptions{}

if desc, ok := d.GetOk("description"); ok {
options.Description = tfe.String(desc.(string))
}

path := d.Get("name").(string) + ".sentinel"
vKind, ok := d.GetOk("kind")
if ok {
if vKind == tfe.OPA {
path = d.Get("name").(string) + ".rego"
}
}
if d.HasChange("enforce_mode") {
options.Enforce = []*tfe.EnforcementOptions{
{
Path: tfe.String(path),
Mode: tfe.EnforcementMode(tfe.EnforcementLevel(d.Get("enforce_mode").(string))),
},
}
}

log.Printf("[DEBUG] Update configuration for %s policy: %s", vKind, d.Id())
_, err := tfeClient.Policies.Update(ctx, d.Id(), options)
if err != nil {
return fmt.Errorf(
"Error updating configuration for %s policy %s: %w", vKind, d.Id(), err)
}
}

if d.HasChange("policy") {
vKind := d.Get("kind").(string)
log.Printf("[DEBUG] Update %s policy: %s", vKind, d.Id())
err := tfeClient.Policies.Upload(ctx, d.Id(), []byte(d.Get("policy").(string)))
if err != nil {
return fmt.Errorf("Error updating %s policy %s: %w", vKind, d.Id(), err)
}
}

return resourceTFEPolicyRead(d, meta)
}

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

log.Printf("[DEBUG] Delete policy: %s", d.Id())
err := tfeClient.Policies.Delete(ctx, d.Id())
if err != nil {
if errors.Is(err, tfe.ErrResourceNotFound) {
return nil
}
return fmt.Errorf("Error deleting policy %s: %w", d.Id(), err)
}

return nil
}

func resourceTFEPolicyImporter(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
s := strings.SplitN(d.Id(), "/", 2)
if len(s) != 2 {
return nil, fmt.Errorf(
"invalid policy import format: %s (expected <ORGANIZATION>/<POLICY ID>)",
d.Id(),
)
}

// Set the fields that are part of the import ID.
d.Set("organization", s[0])
d.SetId(s[1])

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

0 comments on commit ec6b82a

Please sign in to comment.