Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Resource: azurerm_policy_definition #1010

Merged
merged 10 commits into from
Mar 27, 2018
11 changes: 11 additions & 0 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2016-04-01/redis"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-06-01/subscriptions"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-09-01/locks"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-12-01/policy"
"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2017-05-10/resources"
"github.com/Azure/azure-sdk-for-go/services/scheduler/mgmt/2016-03-01/scheduler"
"github.com/Azure/azure-sdk-for-go/services/search/mgmt/2015-08-19/search"
Expand Down Expand Up @@ -193,6 +194,9 @@ type ArmClient struct {
// Web
appServicePlansClient web.AppServicePlansClient
appServicesClient web.AppsClient

// Policy
policyDefinitionsClient policy.DefinitionsClient
}

func (c *ArmClient) configureClient(client *autorest.Client, auth autorest.Authorizer) {
Expand Down Expand Up @@ -372,6 +376,7 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) {
client.registerStorageClients(endpoint, c.SubscriptionID, auth)
client.registerTrafficManagerClients(endpoint, c.SubscriptionID, auth)
client.registerWebClients(endpoint, c.SubscriptionID, auth)
client.registerPolicyClients(endpoint, c.SubscriptionID, auth)

return &client, nil
}
Expand Down Expand Up @@ -864,6 +869,12 @@ func (c *ArmClient) registerWebClients(endpoint, subscriptionId string, auth aut
c.appServicesClient = appsClient
}

func (c *ArmClient) registerPolicyClients(endpoint, subscriptionId string, auth autorest.Authorizer) {
policyDefinitionsClient := policy.NewDefinitionsClientWithBaseURI(endpoint, subscriptionId)
c.configureClient(&policyDefinitionsClient.Client, auth)
c.policyDefinitionsClient = policyDefinitionsClient
}

var (
storageKeyCacheMu sync.RWMutex
storageKeyCache = make(map[string]string)
Expand Down
1 change: 1 addition & 0 deletions azurerm/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_network_security_group": resourceArmNetworkSecurityGroup(),
"azurerm_network_security_rule": resourceArmNetworkSecurityRule(),
"azurerm_network_watcher": resourceArmNetworkWatcher(),
"azurerm_policy_definition": resourceArmPolicyDefinition(),
"azurerm_postgresql_configuration": resourceArmPostgreSQLConfiguration(),
"azurerm_postgresql_database": resourceArmPostgreSQLDatabase(),
"azurerm_postgresql_firewall_rule": resourceArmPostgreSQLFirewallRule(),
Expand Down
218 changes: 218 additions & 0 deletions azurerm/resource_arm_policy_definition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package azurerm

import (
"fmt"
"log"

"net/url"
"path"

"github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-12-01/policy"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/structure"
"github.com/hashicorp/terraform/helper/validation"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceArmPolicyDefinition() *schema.Resource {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we add some documentation for this resource in the website folder?

return &schema.Resource{
Create: resourceArmPolicyDefinitionCreateUpdate,
Update: resourceArmPolicyDefinitionCreateUpdate,
Read: resourceArmPolicyDefinitionRead,
Delete: resourceArmPolicyDefinitionDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

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

"policy_type": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
string(policy.TypeBuiltIn),
string(policy.TypeCustom),
string(policy.TypeNotSpecified),
}, true)},

"mode": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: validation.StringInSlice([]string{
string(policy.All),
string(policy.Indexed),
string(policy.NotSpecified),
}, true),
},

"display_name": {
Type: schema.TypeString,
Required: true,
},

"description": {
Type: schema.TypeString,
Optional: true,
},

"policy_rule": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.ValidateJsonString,
DiffSuppressFunc: structure.SuppressJsonDiff,
},

"meta_data": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.ValidateJsonString,
DiffSuppressFunc: structure.SuppressJsonDiff,
},

"parameters": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.ValidateJsonString,
DiffSuppressFunc: structure.SuppressJsonDiff,
},
},
}
}

func resourceArmPolicyDefinitionCreateUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).policyDefinitionsClient
ctx := meta.(*ArmClient).StopContext

name := d.Get("name").(string)
policyType := d.Get("policy_type").(string)
mode := d.Get("mode").(string)
displayName := d.Get("display_name").(string)
description := d.Get("description").(string)

properties := policy.DefinitionProperties{
PolicyType: policy.Type(policyType),
Mode: policy.Mode(mode),
DisplayName: utils.String(displayName),
Description: utils.String(description),
}

if policyRuleString := d.Get("policy_rule").(string); policyRuleString != "" {
policyRule, err := structure.ExpandJsonFromString(policyRuleString)
if err != nil {
return fmt.Errorf("unable to parse policy_rule: %s", err)
}
properties.PolicyRule = &policyRule
}

if metaDataString := d.Get("meta_data").(string); metaDataString != "" {
metaData, err := structure.ExpandJsonFromString(metaDataString)
if err != nil {
return fmt.Errorf("unable to parse meta_data: %s", err)
}
properties.Metadata = &metaData
}

if parametersString := d.Get("parameters").(string); parametersString != "" {
parameters, err := structure.ExpandJsonFromString(parametersString)
if err != nil {
return fmt.Errorf("unable to parse parameters: %s", err)
}
properties.Parameters = &parameters
}

definition := policy.Definition{
Name: utils.String(name),
DefinitionProperties: &properties,
}

_, err := client.CreateOrUpdate(ctx, name, definition)
if err != nil {
return err
}

resp, err := client.Get(ctx, name)
if err != nil {
return err
}

d.SetId(*resp.ID)

return resourceArmPolicyDefinitionRead(d, meta)
}

func resourceArmPolicyDefinitionRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).policyDefinitionsClient
ctx := meta.(*ArmClient).StopContext

name, err := parsePolicyDefinitionNameFromId(d.Id())
if err != nil {
return err
}

resp, err := client.Get(ctx, name)
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[INFO] Error reading Policy Definition %q - removing from state", d.Id())
d.SetId("")
return nil
}

return fmt.Errorf("Error reading Policy Definition %+v", err)
}

d.Set("id", resp.ID)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to set id here - since it's already set in the Create method above

d.Set("name", resp.Name)
if props := resp.DefinitionProperties; props != nil {
d.Set("policy_type", props.PolicyType)
d.Set("mode", props.Mode)
d.Set("display_name", props.DisplayName)
d.Set("description", props.Description)
d.Set("policy_rule", props.PolicyRule)
d.Set("meta_data", props.Metadata)
d.Set("parameters", props.Parameters)
}

return nil
}

func resourceArmPolicyDefinitionDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*ArmClient).policyDefinitionsClient
ctx := meta.(*ArmClient).StopContext

name, err := parsePolicyDefinitionNameFromId(d.Id())
if err != nil {
return err
}

resp, err := client.Delete(ctx, name)

if err != nil {
if utils.ResponseWasNotFound(resp) {
return nil
}

return fmt.Errorf("Error deleting Policy Definition %q: %+v", name, err)
}

return nil
}

func parsePolicyDefinitionNameFromId(id string) (string, error) {
idURL, err := url.ParseRequestURI(id)
if err != nil {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the ID should be in the format:

/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/policyDefinitions/{policyDefinitionName}

based on what I can see in the SDK - I think we should be able to use the built in functions here:

We can access this via:

id, err := parseAzureResourceID(d.Id())
if err != nil {
  return nil
}

return id.Path["policyDefinitions"]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When we use parseAzureResourceID we get an error that a resource group isn't in the id string.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in which case can we return just the last segment of this? given we can guarantee there's only 6 segments in the URI (once it's split based on /) we should be able to make this:

components := strings.Split(path, "/")

if len(components) != 6 {
  return nil, fmt.Errorf("Azure Policy Definition Id should have 6 segments, got %d: '%s'", len(components), path)
}

return components[5]

return "", err
}
name := path.Base(idURL.Path)
if name == "." || name == "/" {
return "", fmt.Errorf("Couldn't parse Policy Definition name")
}

return name, nil
}
117 changes: 117 additions & 0 deletions azurerm/resource_arm_policy_definition_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package azurerm

import (
"fmt"
"net/http"
"testing"

"github.com/hashicorp/terraform/helper/acctest"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/terraform"
)

func TestAccAzureRMPolicyDefinition_basic(t *testing.T) {
resourceName := "azurerm_policy_definition.test"

ri := acctest.RandInt()
config := testAzureRMPolicyDefinition(ri)

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMPolicyDefinitionDestroy,
Steps: []resource.TestStep{
{
Config: config,
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMPolicyDefinitionExists(resourceName)),
},
},
})
}

func testCheckAzureRMPolicyDefinitionExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("not found: %s", name)
}

policyName := rs.Primary.Attributes["name"]

client := testAccProvider.Meta().(*ArmClient).policyDefinitionsClient
ctx := testAccProvider.Meta().(*ArmClient).StopContext

resp, err := client.Get(ctx, policyName)
if err != nil {
return fmt.Errorf("Bad: Get on policyDefinitionsClient: %s", err)
}

if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("policy does not exist: %s", name)
}

return nil
}
}

func testCheckAzureRMPolicyDefinitionDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ArmClient).policyDefinitionsClient
ctx := testAccProvider.Meta().(*ArmClient).StopContext

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

name := rs.Primary.Attributes["name"]

resp, err := client.Get(ctx, name)

if err != nil {
return nil
}

if resp.StatusCode != http.StatusNotFound {
return fmt.Errorf("policy still exists:%s", *resp.Name)
}
}

return nil
}

func testAzureRMPolicyDefinition(ri int) string {
return fmt.Sprintf(`
resource "azurerm_policy_definition" "test" {
name = "acctestpol-%d"
policy_type = "Custom"
mode = "All"
display_name = "acctestpol-%d"
policy_rule =<<POLICY_RULE
{
"if": {
"not": {
"field": "location",
"in": "[parameters('allowedLocations')]"
}
},
"then": {
"effect": "audit"
}
}
POLICY_RULE

parameters =<<PARAMETERS
{
"allowedLocations": {
"type": "Array",
"metadata": {
"description": "The list of allowed locations for resources.",
"displayName": "Allowed locations",
"strongType": "location"
}
}
}
PARAMETERS
}`, ri, ri)
}
Loading