diff --git a/azurerm/internal/services/managementgroup/parse/management_group.go b/azurerm/internal/services/managementgroup/parse/management_group.go index 46dfab265779..f1db83534034 100644 --- a/azurerm/internal/services/managementgroup/parse/management_group.go +++ b/azurerm/internal/services/managementgroup/parse/management_group.go @@ -36,3 +36,14 @@ func ManagementGroupID(input string) (*ManagementGroupId, error) { return &id, nil } + +func NewManagementGroupId(managementGroupName string) ManagementGroupId { + return ManagementGroupId{ + Name: managementGroupName, + } +} + +func (r ManagementGroupId) ID() string { + managemntGroupIdFmt := "/providers/Microsoft.Management/managementGroups/%s" + return fmt.Sprintf(managemntGroupIdFmt, r.Name) +} diff --git a/azurerm/internal/services/resource/client/client.go b/azurerm/internal/services/resource/client/client.go index 25f00336ee47..e4bbcf54d77b 100644 --- a/azurerm/internal/services/resource/client/client.go +++ b/azurerm/internal/services/resource/client/client.go @@ -2,17 +2,19 @@ package client import ( providers "github.com/Azure/azure-sdk-for-go/profiles/2017-03-09/resources/mgmt/resources" + "github.com/Azure/azure-sdk-for-go/services/preview/resources/mgmt/2019-06-01-preview/templatespecs" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2016-09-01/locks" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-06-01/resources" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/common" ) type Client struct { - DeploymentsClient *resources.DeploymentsClient - GroupsClient *resources.GroupsClient - LocksClient *locks.ManagementLocksClient - ProvidersClient *providers.ProvidersClient - ResourcesClient *resources.Client + DeploymentsClient *resources.DeploymentsClient + GroupsClient *resources.GroupsClient + LocksClient *locks.ManagementLocksClient + ProvidersClient *providers.ProvidersClient + ResourcesClient *resources.Client + TemplateSpecsVersionsClient *templatespecs.VersionsClient } func NewClient(o *common.ClientOptions) *Client { @@ -32,11 +34,15 @@ func NewClient(o *common.ClientOptions) *Client { resourcesClient := resources.NewClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&resourcesClient.Client, o.ResourceManagerAuthorizer) + templatespecsVersionsClient := templatespecs.NewVersionsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&templatespecsVersionsClient.Client, o.ResourceManagerAuthorizer) + return &Client{ - GroupsClient: &groupsClient, - DeploymentsClient: &deploymentsClient, - LocksClient: &locksClient, - ProvidersClient: &providersClient, - ResourcesClient: &resourcesClient, + GroupsClient: &groupsClient, + DeploymentsClient: &deploymentsClient, + LocksClient: &locksClient, + ProvidersClient: &providersClient, + ResourcesClient: &resourcesClient, + TemplateSpecsVersionsClient: &templatespecsVersionsClient, } } diff --git a/azurerm/internal/services/resource/management_group_template_deployment_resource.go b/azurerm/internal/services/resource/management_group_template_deployment_resource.go new file mode 100644 index 000000000000..a65e6b905f8b --- /dev/null +++ b/azurerm/internal/services/resource/management_group_template_deployment_resource.go @@ -0,0 +1,387 @@ +package resource + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-06-01/resources" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/location" + mgParse "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/managementgroup/parse" + mgValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/managementgroup/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/resource/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/resource/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + azSchema "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tf/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func managementGroupTemplateDeploymentResource() *schema.Resource { + return &schema.Resource{ + Create: managementGroupTemplateDeploymentResourceCreate, + Read: managementGroupTemplateDeploymentResourceRead, + Update: managementGroupTemplateDeploymentResourceUpdate, + Delete: managementGroupTemplateDeploymentResourceDelete, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.ManagementGroupTemplateDeploymentID(id) + return err + }), + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(180 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(180 * time.Minute), + Delete: schema.DefaultTimeout(180 * time.Minute), + }, + + // (@jackofallops - lintignore needed as we need to make sure the JSON is usable in `output_content`) + + //lintignore:S033 + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.TemplateDeploymentName, + }, + + "management_group_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: mgValidate.ManagementGroupID, + }, + + "location": location.Schema(), + + "template_content": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ExactlyOneOf: []string{ + "template_content", + "template_spec_version_id", + }, + StateFunc: utils.NormalizeJson, + }, + + "template_spec_version_id": { + Type: schema.TypeString, + Optional: true, + ExactlyOneOf: []string{ + "template_content", + "template_spec_version_id", + }, + ValidateFunc: validate.TemplateSpecVersionID, + }, + + // Optional + "debug_level": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice(templateDeploymentDebugLevels, false), + }, + + "parameters_content": { + Type: schema.TypeString, + Optional: true, + Computed: true, + StateFunc: utils.NormalizeJson, + }, + + "tags": tags.Schema(), + + // Computed + "output_content": { + Type: schema.TypeString, + Computed: true, + StateFunc: utils.NormalizeJson, + // NOTE: outputs can be strings, ints, objects etc - whilst using a nested object was considered + // parsing the JSON using `jsondecode` allows the users to interact with/map objects as required + }, + }, + } +} + +func managementGroupTemplateDeploymentResourceCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Resource.DeploymentsClient + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) + defer cancel() + + managementGroupId, err := mgParse.ManagementGroupID(d.Get("management_group_id").(string)) + if err != nil { + return err + } + + id := parse.NewManagementGroupTemplateDeploymentID(managementGroupId.Name, d.Get("name").(string)) + + existing, err := client.GetAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + } + if existing.Properties != nil { + return tf.ImportAsExistsError("azurerm_management_group_template_deployment", id.ID()) + } + + deployment := resources.ScopedDeployment{ + Location: utils.String(location.Normalize(d.Get("location").(string))), + Properties: &resources.DeploymentProperties{ + DebugSetting: expandTemplateDeploymentDebugSetting(d.Get("debug_level").(string)), + Mode: resources.Incremental, + }, + Tags: tags.Expand(d.Get("tags").(map[string]interface{})), + } + + if templateRaw, ok := d.GetOk("template_content"); ok { + template, err := expandTemplateDeploymentBody(templateRaw.(string)) + if err != nil { + return fmt.Errorf("expanding `template_content`: %+v", err) + } + deployment.Properties.Template = template + } + + if templateSpecVersionID, ok := d.GetOk("template_spec_version_id"); ok { + deployment.Properties.TemplateLink = &resources.TemplateLink{ + ID: utils.String(templateSpecVersionID.(string)), + } + } + + if v, ok := d.GetOk("parameters_content"); ok && v != "" { + parameters, err := expandTemplateDeploymentBody(v.(string)) + if err != nil { + return fmt.Errorf("expanding `parameters_content`: %+v", err) + } + deployment.Properties.Parameters = parameters + } + + log.Printf("[DEBUG] Running validation of Management Group Template Deployment %q..", id.DeploymentName) + if err := validateManagementGroupTemplateDeployment(ctx, id, deployment, client); err != nil { + return fmt.Errorf("validating Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + log.Printf("[DEBUG] Validated Management Group Template Deployment %q..", id.DeploymentName) + + log.Printf("[DEBUG] Provisioning Management Group Template Deployment %q..", id.DeploymentName) + future, err := client.CreateOrUpdateAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName, deployment) + if err != nil { + return fmt.Errorf("creating Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + log.Printf("[DEBUG] Waiting for deployment of Management Group Template Deployment %q..", id.DeploymentName) + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for creation of Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + d.SetId(id.ID()) + return managementGroupTemplateDeploymentResourceRead(d, meta) +} + +func managementGroupTemplateDeploymentResourceUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Resource.DeploymentsClient + ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ManagementGroupTemplateDeploymentID(d.Id()) + if err != nil { + return err + } + + log.Printf("[DEBUG] Retrieving Management Group Template Deployment %q..", id.DeploymentName) + template, err := client.GetAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + return fmt.Errorf("retrieving Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + if template.Properties == nil { + return fmt.Errorf("retrieving Management Group Template Deployment %q: `properties` was nil", id.DeploymentName) + } + + // the API doesn't have a Patch operation, so we'll need to build one + deployment := resources.ScopedDeployment{ + Location: template.Location, + Properties: &resources.DeploymentProperties{ + DebugSetting: template.Properties.DebugSetting, + Mode: resources.Incremental, + }, + Tags: template.Tags, + } + + if d.HasChange("debug_level") { + deployment.Properties.DebugSetting = expandTemplateDeploymentDebugSetting(d.Get("debug_level").(string)) + } + + if d.HasChange("parameters_content") { + parameters, err := expandTemplateDeploymentBody(d.Get("parameters_content").(string)) + if err != nil { + return fmt.Errorf("expanding `parameters_content`: %+v", err) + } + deployment.Properties.Parameters = parameters + } + + if d.HasChange("template_content") { + templateContents, err := expandTemplateDeploymentBody(d.Get("template_content").(string)) + if err != nil { + return fmt.Errorf("expanding `template_content`: %+v", err) + } + + deployment.Properties.Template = templateContents + } else { + // retrieve the existing content and reuse that + exportedTemplate, err := client.ExportTemplateAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + return fmt.Errorf("retrieving Contents for Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + deployment.Properties.Template = exportedTemplate.Template + } + + if d.HasChange("template_spec_version_id") { + deployment.Properties.TemplateLink = &resources.TemplateLink{ + ID: utils.String(d.Get("template_spec_version_id").(string)), + } + } + + if d.HasChange("tags") { + deployment.Tags = tags.Expand(d.Get("tags").(map[string]interface{})) + } + + log.Printf("[DEBUG] Running validation of Management Group Template Deployment %q..", id.DeploymentName) + if err := validateManagementGroupTemplateDeployment(ctx, *id, deployment, client); err != nil { + return fmt.Errorf("validating Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + log.Printf("[DEBUG] Validated Management Group Template Deployment %q..", id.DeploymentName) + + log.Printf("[DEBUG] Provisioning Management Group Template Deployment %q)..", id.DeploymentName) + future, err := client.CreateOrUpdateAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName, deployment) + if err != nil { + return fmt.Errorf("creating Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + log.Printf("[DEBUG] Waiting for deployment of Management Group Template Deployment %q..", id.DeploymentName) + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for creation of Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + return managementGroupTemplateDeploymentResourceRead(d, meta) +} + +func managementGroupTemplateDeploymentResourceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Resource.DeploymentsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ManagementGroupTemplateDeploymentID(d.Id()) + if err != nil { + return err + } + + resp, err := client.GetAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[DEBUG] Management Group Template Deployment %q was not found - removing from state", id.DeploymentName) + d.SetId("") + return nil + } + + return fmt.Errorf("retrieving Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + templateContents, err := client.ExportTemplateAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + return fmt.Errorf("retrieving Template Content for Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + d.Set("name", id.DeploymentName) + managementGroupId := mgParse.NewManagementGroupId(id.ManagementGroupName) + d.Set("management_group_id", managementGroupId.ID()) + d.Set("location", location.NormalizeNilable(resp.Location)) + + if props := resp.Properties; props != nil { + d.Set("debug_level", flattenTemplateDeploymentDebugSetting(props.DebugSetting)) + + filteredParams := filterOutTemplateDeploymentParameters(props.Parameters) + flattenedParams, err := flattenTemplateDeploymentBody(filteredParams) + if err != nil { + return fmt.Errorf("flattening `parameters_content`: %+v", err) + } + d.Set("parameters_content", flattenedParams) + + flattenedOutputs, err := flattenTemplateDeploymentBody(props.Outputs) + if err != nil { + return fmt.Errorf("flattening `output_content`: %+v", err) + } + d.Set("output_content", flattenedOutputs) + + templateLinkId := "" + if props.TemplateLink != nil { + if props.TemplateLink.ID != nil { + templateLinkId = *props.TemplateLink.ID + } + } + d.Set("template_spec_version_id", templateLinkId) + } + + flattenedTemplate, err := flattenTemplateDeploymentBody(templateContents.Template) + if err != nil { + return fmt.Errorf("flattening `template_content`: %+v", err) + } + d.Set("template_content", flattenedTemplate) + + return tags.FlattenAndSet(d, resp.Tags) +} + +func managementGroupTemplateDeploymentResourceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Resource.DeploymentsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.ManagementGroupTemplateDeploymentID(d.Id()) + if err != nil { + return err + } + + // at this time unfortunately the Resources RP doesn't expose a means of deleting top-level objects + // so we're unable to delete these during deletion - this'll need to be detailed in the docs + + log.Printf("[DEBUG] Deleting Management Group Template Deployment %q..", id.DeploymentName) + future, err := client.DeleteAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName) + if err != nil { + return fmt.Errorf("deleting Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + + log.Printf("[DEBUG] Waiting for deletion of Management Group Template Deployment %q..", id.DeploymentName) + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for deletion of Management Group Template Deployment %q: %+v", id.DeploymentName, err) + } + log.Printf("[DEBUG] Deleted Management Group Template Deployment %q.", id.DeploymentName) + + return nil +} + +func validateManagementGroupTemplateDeployment(ctx context.Context, id parse.ManagementGroupTemplateDeploymentId, deployment resources.ScopedDeployment, client *resources.DeploymentsClient) error { + validationFuture, err := client.ValidateAtManagementGroupScope(ctx, id.ManagementGroupName, id.DeploymentName, deployment) + if err != nil { + return fmt.Errorf("requesting validating: %+v", err) + } + if err := validationFuture.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for validation: %+v", err) + } + validationResult, err := validationFuture.Result(*client) + if err != nil { + return fmt.Errorf("retrieving validation result: %+v", err) + } + if validationResult.Error != nil { + if validationResult.Error.Message != nil { + return fmt.Errorf("%s", *validationResult.Error.Message) + } + return fmt.Errorf("%+v", *validationResult.Error) + } + + return nil +} diff --git a/azurerm/internal/services/resource/management_group_template_deployment_resource_test.go b/azurerm/internal/services/resource/management_group_template_deployment_resource_test.go new file mode 100644 index 000000000000..c5ae1e561762 --- /dev/null +++ b/azurerm/internal/services/resource/management_group_template_deployment_resource_test.go @@ -0,0 +1,156 @@ +package resource_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/resource/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type ManagementGroupTemplateDeploymentResource struct{} + +func TestAccManagementGroupTemplateDeployment_empty(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_management_group_template_deployment", "test") + r := ManagementGroupTemplateDeploymentResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.emptyConfig(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + // set some tags + Config: r.emptyWithTagsConfig(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccManagementGroupTemplateDeployment_templateSpec(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_management_group_template_deployment", "test") + r := ManagementGroupTemplateDeploymentResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.templateSpecVersionConfig(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func (ManagementGroupTemplateDeploymentResource) templateSpecVersionConfig(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_management_group" "test" { + name = "TestAcc-Deployment-%[1]d" +} + +data "azurerm_template_spec_version" "test" { + name = "acctest-standing-data-empty" + resource_group_name = "standing-data-for-acctest" + version = "v1.0.0" +} + +resource "azurerm_management_group_template_deployment" "test" { + name = "acctestMGdeploy-%[1]d" + management_group_id = azurerm_management_group.test.id + location = %[2]q + + template_spec_version_id = data.azurerm_template_spec_version.test.id + +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} + +func (ManagementGroupTemplateDeploymentResource) emptyConfig(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_management_group" "test" { + name = "TestAcc-Deployment-%[1]d" +} + +resource "azurerm_management_group_template_deployment" "test" { + name = "acctestMGdeploy-%[1]d" + management_group_id = azurerm_management_group.test.id + location = %[2]q + + template_content = <