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_management_group #1788

Merged
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions azurerm/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/preview/msi/mgmt/2015-08-31-preview/msi"
"github.com/Azure/azure-sdk-for-go/services/preview/operationalinsights/mgmt/2015-11-01-preview/operationalinsights"
"github.com/Azure/azure-sdk-for-go/services/preview/operationsmanagement/mgmt/2015-11-01-preview/operationsmanagement"
"github.com/Azure/azure-sdk-for-go/services/preview/resources/mgmt/2018-03-01-preview/management"
"github.com/Azure/azure-sdk-for-go/services/preview/sql/mgmt/2015-05-01-preview/sql"
"github.com/Azure/azure-sdk-for-go/services/recoveryservices/mgmt/2016-06-01/recoveryservices"
"github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis"
Expand All @@ -53,6 +54,7 @@ import (
"github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2017-10-01/storage"
"github.com/Azure/azure-sdk-for-go/services/trafficmanager/mgmt/2017-05-01/trafficmanager"
"github.com/Azure/azure-sdk-for-go/services/web/mgmt/2018-02-01/web"

mainStorage "github.com/Azure/azure-sdk-for-go/storage"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/adal"
Expand Down Expand Up @@ -165,6 +167,10 @@ type ArmClient struct {
// Logic
logicWorkflowsClient logic.WorkflowsClient

// Management Groups
managementGroupsClient managementgroups.Client
managementGroupsSubscriptionClient managementgroups.SubscriptionsClient

// Monitor
actionGroupsClient insights.ActionGroupsClient
monitorAlertRulesClient insights.AlertRulesClient
Expand Down Expand Up @@ -428,6 +434,7 @@ func getArmClient(c *authentication.Config) (*ArmClient, error) {
client.registerOperationalInsightsClients(endpoint, c.SubscriptionID, auth, sender)
client.registerRecoveryServiceClients(endpoint, c.SubscriptionID, auth)
client.registerPolicyClients(endpoint, c.SubscriptionID, auth)
client.registerManagementGroupClients(endpoint, auth)
client.registerRedisClients(endpoint, c.SubscriptionID, auth, sender)
client.registerRelayClients(endpoint, c.SubscriptionID, auth, sender)
client.registerResourcesClients(endpoint, c.SubscriptionID, auth)
Expand Down Expand Up @@ -1039,6 +1046,16 @@ func (c *ArmClient) registerPolicyClients(endpoint, subscriptionId string, auth
c.policyDefinitionsClient = policyDefinitionsClient
}

func (c *ArmClient) registerManagementGroupClients(endpoint string, auth autorest.Authorizer) {
tombuildsstuff marked this conversation as resolved.
Show resolved Hide resolved
managementGroupsClient := managementgroups.NewClientWithBaseURI(endpoint)
c.configureClient(&managementGroupsClient.Client, auth)
c.managementGroupsClient = managementGroupsClient

managementGroupsSubscriptionClient := managementgroups.NewSubscriptionsClientWithBaseURI(endpoint)
c.configureClient(&managementGroupsSubscriptionClient.Client, auth)
c.managementGroupsSubscriptionClient = managementGroupsSubscriptionClient
}

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 @@ -196,6 +196,7 @@ func Provider() terraform.ResourceProvider {
"azurerm_logic_app_workflow": resourceArmLogicAppWorkflow(),
"azurerm_managed_disk": resourceArmManagedDisk(),
"azurerm_management_lock": resourceArmManagementLock(),
"azurerm_management_group": resourceManagementGroup(),
"azurerm_metric_alertrule": resourceArmMetricAlertRule(),
"azurerm_monitor_action_group": resourceArmMonitorActionGroup(),
"azurerm_mysql_configuration": resourceArmMySQLConfiguration(),
Expand Down
198 changes: 198 additions & 0 deletions azurerm/resource_arm_management_group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package azurerm

import (
"fmt"
"log"
"strings"

"github.com/Azure/azure-sdk-for-go/services/preview/resources/mgmt/2018-03-01-preview/management"
"github.com/hashicorp/terraform/helper/schema"
"github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils"
)

func resourceManagementGroup() *schema.Resource {
return &schema.Resource{
Create: resourceManagementGroupCreateUpdate,
Update: resourceManagementGroupCreateUpdate,
Read: resourceManagementGroupRead,
Delete: resourceManagementGroupDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},

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

"subscription_ids": {
Type: schema.TypeList,
Optional: true,
Required: false,
Computed: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

we can remove Required and Computed here (since they both default to false)

Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func resourceManagementGroupCreateUpdate(d *schema.ResourceData, meta interface{}) error {

client := meta.(*ArmClient).managementGroupsClient
subscriptionsClient := meta.(*ArmClient).managementGroupsSubscriptionClient
ctx := meta.(*ArmClient).StopContext

armTenantID := meta.(*ArmClient).tenantId
name := d.Get("name").(string)
subscriptionIds := d.Get("subscription_ids").([]interface{})
log.Printf("[INFO] Creating management group %q", name)

createParentGroupInfo := managementgroups.CreateParentGroupInfo{
ID: utils.String("/providers/Microsoft.Management/managementGroups/" + armTenantID),
}
details := managementgroups.CreateManagementGroupDetails{
Parent: &createParentGroupInfo,
}

managementGroupProperties := managementgroups.CreateManagementGroupProperties{
TenantID: &armTenantID,
DisplayName: &name,
Copy link
Contributor

Choose a reason for hiding this comment

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

given Name and Display Name could be different (and have different validation requirements) - should we make this a separate field?

Details: &details,
}
Copy link
Contributor

Choose a reason for hiding this comment

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

minor could we in-line the CreateManagementGroupProperties, CreateManagementGroupDetails and CreateParentGroupInfo objects below and dereference the strings using utils.String() eg

parentId := fmt.Sprintf("/providers/Microsoft.Management/managementGroups/%s", armTenantID)
properties :=  managementgroups.CreateManagementGroupRequest{
  CreateManagementGroupProperties: &managementgroups.CreateManagementGroupProperties{
    TenantID:    utils.String(armTenantID),
    DisplayName: utils.String(name),
    Details: &managementgroups.CreateManagementGroupDetails{
      Parent: &managementgroups.CreateParentGroupInfo{
        ID: utils.String(parentId),
      }
    }
  }
}


createManagementGroupRequest := managementgroups.CreateManagementGroupRequest{
ID: nil,
Type: utils.String("/providers/Microsoft.Management/managementGroups"),
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 or Type here - can we remove these?

Name: &name,
Copy link
Contributor

Choose a reason for hiding this comment

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

we shouldn't need to set the name field here since it's specified in the CreateOrUpdate method below (which forms the URI as part of the request) - so I think we can remove this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tried it out.. gave me a Original Error: autorest/azure: Service returned an error. Status=400 Code="BadRequest" Message="Name of the group doesn't match the group id." error..

CreateManagementGroupProperties: &managementGroupProperties,
}

log.Printf("[DEBUG] Invoking managementGroupClient")
createManagementGroupFuture, err := client.CreateOrUpdate(ctx, name, createManagementGroupRequest, "no-cache")
if err != nil {
log.Printf("[DEBUG] Error invoking REST API call")
return err
}

err = createManagementGroupFuture.WaitForCompletion(ctx, client.Client)

_, err = createManagementGroupFuture.Result(client)
Copy link
Contributor

Choose a reason for hiding this comment

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

since we don't need the result of the Future here we should be able to just wait for the completion of the Future instead (also we can reuse the err object in the Get/wrap the errors) e.g.

future, err := client.CreateOrUpdate(ctx, name, properties, "no-cache")
if err != nil {
  return fmt.Errorf("Error creating Management Group %q: %+v", name, err)
}

err = future.WaitForCompletion(ctx, client.Client)
if err != nil {
  return fmt.Errorf("Error waiting for creation of Management Group %q: %+v", name, err)
}

resp, err := client.Get(ctx, name, "", &recurse, "", "no-cache")
if err != nil {
  return fmt.Errorf("Error retrieving Management Group %q: %+v", name, err)
}

d.SetId(*resp.ID)

if err != nil {
log.Printf("[DEBUG] Error in API response")
return err
}

recurse := false

getResp, getErr := client.Get(ctx, name, "", &recurse, "", "no-cache")
if getErr != nil {
log.Printf("[DEBUG] Error retrieving management group details")
return err
}
d.SetId(*getResp.ID)

for _, subscription := range subscriptionIds {
data := subscription.(string)
log.Printf("[DEBUG] Adding subscriptionId %q to management group %q", data, name)
Copy link
Contributor

Choose a reason for hiding this comment

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

given this is an entirely different API call - I'm wondering if this'd make sense as a separate resource?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

based on the SDK package /mgmt/2018-03-01-preview/management, there are two different clients for managementgroup (client) and subscriptions (subscriptionclient) - in this case though, subscriptionclient really only gets used in the managementgroup context, and purely to associate a subscription to a management group (and not for creating subscriptions).. i think there's merit creating a dedicated subscription object, but for the creation/deletion of subscriptions (which is a different SDK package). My two cents..

_, err = subscriptionsClient.Create(ctx, name, data, "no-cache")
if err != nil {
log.Printf("[DEBUG] Error assigning subscription %q to management group %q", data, name)
return err
}
}

return resourceManagementGroupRead(d, meta)
}

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

recurse := true
resp, err := client.Get(ctx, d.Get("name").(string), "children", &recurse, "", "no-cache")
if err != nil {
if utils.ResponseWasNotFound(resp.Response) {
log.Printf("[INFO] Error reading Management Group %q - removing from state", d.Id())
d.SetId("")
return nil
}

return fmt.Errorf("Error reading Management Group %+v", err)
}

subscriptionIds := []string{}
children := resp.Children
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 use the .Properties object available in the SDK (which is where this actually sits in the response) - and nil-check it in-case the API Response / Swagger changes? e.g.

if props := resp.Properties; props != nil {
  if children := props.Children; children != nil {
    for _, subscription := range *children {
      # ...
    }
  }
}

if children != nil {
for _, child := range *children {
subscriptionID, err := parseSubscriptionID(*child.ID)
if err != nil {
log.Printf("%q", err)
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 wrap and return this error?

}
log.Printf("[INFO] Reading subscription %q from management group %q", subscriptionID, d.Get("name").(string))
subscriptionIds = append(subscriptionIds, subscriptionID)
}
}

d.Set("subscription_ids", subscriptionIds)
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 also set the name and display_name field here?


return nil
}

func resourceManagementGroupDelete(d *schema.ResourceData, meta interface{}) error {
//before deleting a management group, return any subscriptions to the root management group

client := meta.(*ArmClient).managementGroupsClient
subscriptionsClient := meta.(*ArmClient).managementGroupsSubscriptionClient
ctx := meta.(*ArmClient).StopContext
armTenantID := meta.(*ArmClient).tenantId
name := d.Get("name").(string)

subscriptionIds := d.Get("subscription_ids").([]interface{})
if subscriptionIds != nil {
for _, subscription := range subscriptionIds {
data := subscription.(string)
log.Printf("[DEBUG] Adding subscriptionId %q to management group %q", data, armTenantID)
_, err := subscriptionsClient.Create(ctx, armTenantID, data, "no-cache")
if err != nil {
log.Printf("[DEBUG] Error assigning subscription %q to management group %q", data, armTenantID)
return err
}
}
}

resp, err := client.Delete(ctx, name, "no-cache")
if err != nil {
log.Printf("[DEBUG] Error deleting management group %q", name)
return fmt.Errorf("Error deleting management group %q", name)
}

err = resp.WaitForCompletion(ctx, client.Client)
if err != nil {
return fmt.Errorf("Error deleting management group %q", name)
}

_, err = resp.Result(client)

if err != nil {
return fmt.Errorf("Error deleting management group %q", name)
}

return nil
}
func parseSubscriptionID(id string) (string, error) {
components := strings.Split(id, "/")

if len(components) == 0 {
return "", fmt.Errorf("Subscription Id is empty or not formatted correctly: %s", id)
}

if len(components) != 3 {
return "", fmt.Errorf("Subscription Id should have 2 segments, got %d: '%s'", len(components)-1, id)
}

return components[2], nil
}
125 changes: 125 additions & 0 deletions azurerm/resource_arm_management_group_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package azurerm

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

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

func TestAccAzureRMManagementGroup_basic(t *testing.T) {
resourceName := "azurerm_management_group.test"

ri := acctest.RandInt()

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMManagementGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAzureRmManagementGroup_basic(ri),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMManagementGroupExists(resourceName),
),
},
},
})
}

func TestAccAzureRMManagementGroup_withSubscriptions(t *testing.T) {
tombuildsstuff marked this conversation as resolved.
Show resolved Hide resolved
resourceName := "azurerm_management_group.test"
//use subscriptionID from ENV VARS

ri := acctest.RandInt()
subscriptionID := os.Getenv("ARM_SUBSCRIPTION_ID")
resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testCheckAzureRMManagementGroupDestroy,
Steps: []resource.TestStep{
{
Config: testAzureRmManagementGroup_withSubscriptions(ri, subscriptionID),
Check: resource.ComposeTestCheckFunc(
testCheckAzureRMManagementGroupExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "subscription_ids.#", "1"),
resource.TestCheckResourceAttr(resourceName, "subscription_ids.#", subscriptionID),
),
},
},
})
}

func testCheckAzureRMManagementGroupExists(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)
}

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

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

recurse := false
resp, err := client.Get(ctx, name, "", &recurse, "", "no-cache")
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 testCheckAzureRMManagementGroupDestroy(s *terraform.State) error {
client := testAccProvider.Meta().(*ArmClient).managementGroupsClient
ctx := testAccProvider.Meta().(*ArmClient).StopContext

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

name := rs.Primary.Attributes["name"]
recurse := false
resp, err := client.Get(ctx, name, "", &recurse, "", "no-cache")

if err != nil {
return nil
}

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

Choose a reason for hiding this comment

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

policy -> Management Group

}
}

return nil
}

func testAzureRmManagementGroup_basic(ri int) string {
return fmt.Sprintf(`
resource "azurerm_management_group" "test" {
name = "acctestmg-%d"
}
`, ri)
}

func testAzureRmManagementGroup_withSubscriptions(ri int, subscriptionID string) string {

return fmt.Sprintf(`
resource "azurerm_management_group" "test" {
name = "acctestmg-%d"
subscription_ids = [
"%q"
Copy link
Contributor

Choose a reason for hiding this comment

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

this is missing a closing ] bracket - can we add this in?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed with latest commit.

}
`, ri, subscriptionID)
}
Loading