diff --git a/azurerm/resource_arm_policy_definition.go b/azurerm/resource_arm_policy_definition.go index 26fc1cda5409..78336f4035f1 100644 --- a/azurerm/resource_arm_policy_definition.go +++ b/azurerm/resource_arm_policy_definition.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "regexp" "strings" "time" @@ -11,6 +12,7 @@ import ( "strconv" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/policy" + "github.com/Azure/go-autorest/autorest" "github.com/hashicorp/terraform/helper/resource" "github.com/hashicorp/terraform/helper/schema" "github.com/hashicorp/terraform/helper/structure" @@ -25,7 +27,7 @@ func resourceArmPolicyDefinition() *schema.Resource { Read: resourceArmPolicyDefinitionRead, Delete: resourceArmPolicyDefinitionDelete, Importer: &schema.ResourceImporter{ - State: schema.ImportStatePassthrough, + State: resourceArmPolicyDefinitionImport, }, Schema: map[string]*schema.Schema{ @@ -56,6 +58,12 @@ func resourceArmPolicyDefinition() *schema.Resource { }, true), }, + "management_group_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + }, + "display_name": { Type: schema.TypeString, Required: true, @@ -99,6 +107,7 @@ func resourceArmPolicyDefinitionCreateUpdate(d *schema.ResourceData, meta interf mode := d.Get("mode").(string) displayName := d.Get("display_name").(string) description := d.Get("description").(string) + managementGroupID := d.Get("management_group_id").(string) properties := policy.DefinitionProperties{ PolicyType: policy.Type(policyType), @@ -136,7 +145,15 @@ func resourceArmPolicyDefinitionCreateUpdate(d *schema.ResourceData, meta interf DefinitionProperties: &properties, } - if _, err := client.CreateOrUpdate(ctx, name, definition); err != nil { + var err error + + if managementGroupID == "" { + _, err = client.CreateOrUpdate(ctx, name, definition) + } else { + _, err = client.CreateOrUpdateAtManagementGroup(ctx, name, definition, managementGroupID) + } + + if err != nil { return err } @@ -145,16 +162,16 @@ func resourceArmPolicyDefinitionCreateUpdate(d *schema.ResourceData, meta interf stateConf := &resource.StateChangeConf{ Pending: []string{"404"}, Target: []string{"200"}, - Refresh: policyDefinitionRefreshFunc(ctx, client, name), + Refresh: policyDefinitionRefreshFunc(ctx, client, name, managementGroupID), Timeout: 5 * time.Minute, MinTimeout: 10 * time.Second, ContinuousTargetOccurence: 10, } - if _, err := stateConf.WaitForState(); err != nil { + if _, err = stateConf.WaitForState(); err != nil { return fmt.Errorf("Error waiting for Policy Definition %q to become available: %s", name, err) } - resp, err := client.Get(ctx, name) + resp, err := getPolicyDefinition(ctx, client, name, managementGroupID) if err != nil { return err } @@ -173,7 +190,10 @@ func resourceArmPolicyDefinitionRead(d *schema.ResourceData, meta interface{}) e return err } - resp, err := client.Get(ctx, name) + managementGroupID := parseManagementGroupIdFromPolicyId(d.Id()) + + resp, err := getPolicyDefinition(ctx, client, name, managementGroupID) + if err != nil { if utils.ResponseWasNotFound(resp.Response) { log.Printf("[INFO] Error reading Policy Definition %q - removing from state", d.Id()) @@ -185,6 +205,7 @@ func resourceArmPolicyDefinitionRead(d *schema.ResourceData, meta interface{}) e } d.Set("name", resp.Name) + d.Set("management_group_id", managementGroupID) if props := resp.DefinitionProperties; props != nil { d.Set("policy_type", props.PolicyType) @@ -235,7 +256,14 @@ func resourceArmPolicyDefinitionDelete(d *schema.ResourceData, meta interface{}) return err } - resp, err := client.Delete(ctx, name) + managementGroupID := parseManagementGroupIdFromPolicyId(d.Id()) + + var resp autorest.Response + if managementGroupID == "" { + resp, err = client.Delete(ctx, name) + } else { + resp, err = client.DeleteAtManagementGroup(ctx, name, managementGroupID) + } if err != nil { if utils.ResponseWasNotFound(resp) { @@ -255,16 +283,24 @@ func parsePolicyDefinitionNameFromId(id string) (string, error) { return "", fmt.Errorf("Azure Policy Definition Id is empty or not formatted correctly: %s", id) } - if len(components) != 7 { - return "", fmt.Errorf("Azure Policy Definition Id should have 6 segments, got %d: '%s'", len(components)-1, id) + return components[len(components)-1], nil +} + +func parseManagementGroupIdFromPolicyId(id string) string { + r, _ := regexp.Compile("managementgroups/(.+)/providers/.*$") + + if r.MatchString(id) { + parms := r.FindAllStringSubmatch(id, -1)[0] + return parms[1] } - return components[6], nil + return "" } -func policyDefinitionRefreshFunc(ctx context.Context, client policy.DefinitionsClient, name string) resource.StateRefreshFunc { +func policyDefinitionRefreshFunc(ctx context.Context, client policy.DefinitionsClient, name string, managementGroupID string) resource.StateRefreshFunc { return func() (interface{}, string, error) { - res, err := client.Get(ctx, name) + res, err := getPolicyDefinition(ctx, client, name, managementGroupID) + if err != nil { return nil, strconv.Itoa(res.StatusCode), fmt.Errorf("Error issuing read request in policyAssignmentRefreshFunc for Policy Assignment %q: %s", name, err) } @@ -272,3 +308,28 @@ func policyDefinitionRefreshFunc(ctx context.Context, client policy.DefinitionsC return res, strconv.Itoa(res.StatusCode), nil } } + +func resourceArmPolicyDefinitionImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if managementGroupID := parseManagementGroupIdFromPolicyId(d.Id()); managementGroupID != "" { + d.Set("management_group_id", managementGroupID) + + if name, _ := parsePolicyDefinitionNameFromId(d.Id()); name != "" { + d.Set("name", name) + } + + return []*schema.ResourceData{d}, nil + } + + //import a subscription based policy as before + return schema.ImportStatePassthrough(d, meta) +} + +func getPolicyDefinition(ctx context.Context, client policy.DefinitionsClient, name string, managementGroupID string) (res policy.Definition, err error) { + if managementGroupID == "" { + res, err = client.Get(ctx, name) + } else { + res, err = client.GetAtManagementGroup(ctx, name, managementGroupID) + } + + return res, err +} diff --git a/azurerm/resource_arm_policy_definition_test.go b/azurerm/resource_arm_policy_definition_test.go index a45bc3d1e149..2e3682f7f695 100644 --- a/azurerm/resource_arm_policy_definition_test.go +++ b/azurerm/resource_arm_policy_definition_test.go @@ -35,6 +35,58 @@ func TestAccAzureRMPolicyDefinition_basic(t *testing.T) { }) } +func TestAccAzureRMPolicyDefinitionAtMgmtGroup_basic(t *testing.T) { + resourceName := "azurerm_policy_definition.test" + mgmtGroupName := "azurerm_management_group.test" + + ri := acctest.RandInt() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPolicyDefinitionDestroy, + Steps: []resource.TestStep{ + { + Config: testAzureRMPolicyDefinition_ManagementGroup(ri), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPolicyDefinitionExistsInMgmtGroup(resourceName, mgmtGroupName), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMPolicyDefinitionExistsInMgmtGroup(policyName string, managementGroupName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[policyName] + if !ok { + return fmt.Errorf("not found: %s", policyName) + } + + policyName := rs.Primary.Attributes["name"] + managementGroupID := rs.Primary.Attributes["management_group_id"] + + client := testAccProvider.Meta().(*ArmClient).policyDefinitionsClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + resp, err := client.GetAtManagementGroup(ctx, policyName, managementGroupID) + if err != nil { + return fmt.Errorf("Bad: GetAtManagementGroup on policyDefinitionsClient: %s", err) + } + + if resp.StatusCode == http.StatusNotFound { + return fmt.Errorf("policy does not exist: %s", policyName) + } + + return nil + } +} + func testCheckAzureRMPolicyDefinitionExists(name string) resource.TestCheckFunc { return func(s *terraform.State) error { rs, ok := s.RootModule().Resources[name] @@ -122,3 +174,46 @@ PARAMETERS } `, ri, ri) } + +func testAzureRMPolicyDefinition_ManagementGroup(ri int) string { + return fmt.Sprintf(` +resource "azurerm_management_group" "test" { + display_name = "acctestmg-%d" +} + +resource "azurerm_policy_definition" "test" { + name = "acctestpol-%d" + policy_type = "Custom" + mode = "All" + display_name = "acctestpol-%d" + management_group_id = "${azurerm_management_group.test.group_id}" + + policy_rule = < **Note:** if you are using `azurerm_management_group` to assign a value to `management_group_id`, be sure to use `.group_id` and not `.id`. + * `policy_rule` - (Optional) The policy rule for the policy definition. This is a json object representing the rule that contains an if and a then block. @@ -91,3 +97,7 @@ Policy Definitions can be imported using the `policy name`, e.g. ```shell terraform import azurerm_policy_definition.testPolicy /subscriptions//providers/Microsoft.Authorization/policyDefinitions/ ``` +or +```shell +terraform import azurerm_policy_definition.testPolicy /providers/Microsoft.Management/managementgroups//providers/Microsoft.Authorization/policyDefinitions/ +```