diff --git a/azurerm/internal/services/network/subnet.go b/azurerm/internal/services/network/subnet.go new file mode 100644 index 000000000000..b6a4994df3f0 --- /dev/null +++ b/azurerm/internal/services/network/subnet.go @@ -0,0 +1,54 @@ +package network + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type SubnetID struct { + ResourceGroup string + VirtualNetworkName string + Name string +} + +func ParseSubnetID(input string) (*SubnetID, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse Subnet ID %q: %+v", input, err) + } + + subnet := SubnetID{ + ResourceGroup: id.ResourceGroup, + } + + if subnet.VirtualNetworkName, err = id.PopSegment("virtualNetworks"); err != nil { + return nil, err + } + + if subnet.Name, err = id.PopSegment("subnets"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &subnet, nil +} + +// ValidateSubnetID validates that the specified ID is a valid App Service ID +func ValidateSubnetID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := ParseSubnetID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/network/virtual_network.go b/azurerm/internal/services/network/virtual_network.go new file mode 100644 index 000000000000..ffd47239f6ff --- /dev/null +++ b/azurerm/internal/services/network/virtual_network.go @@ -0,0 +1,49 @@ +package network + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type VirtualNetworkID struct { + ResourceGroup string + Name string +} + +func ParseVirtualNetworkID(input string) (*VirtualNetworkID, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse Subnet ID %q: %+v", input, err) + } + + vnet := VirtualNetworkID{ + ResourceGroup: id.ResourceGroup, + } + + if vnet.Name, err = id.PopSegment("virtualNetworks"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &vnet, nil +} + +// ValidateVirtualNetworkID validates that the specified ID is a valid App Service ID +func ValidateVirtualNetworkID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := ParseVirtualNetworkID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/azurerm/internal/services/web/app_service_environment.go b/azurerm/internal/services/web/app_service_environment.go new file mode 100644 index 000000000000..6f4eba92fc6a --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment.go @@ -0,0 +1,74 @@ +package web + +import ( + "fmt" + "regexp" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type AppServiceEnvironmentResourceID struct { + ResourceGroup string + Name string +} + +func ParseAppServiceEnvironmentID(input string) (*AppServiceEnvironmentResourceID, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse App Service Environment ID %q: %+v", input, err) + } + + appServiceEnvironment := AppServiceEnvironmentResourceID{ + ResourceGroup: id.ResourceGroup, + } + + if appServiceEnvironment.Name, err = id.PopSegment("hostingEnvironments"); err != nil { + return nil, err + } + + if err := id.ValidateNoEmptySegments(input); err != nil { + return nil, err + } + + return &appServiceEnvironment, nil +} + +// ValidateAppServiceID validates that the specified ID is a valid App Service ID +func ValidateAppServiceEnvironmentID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := ParseAppServiceEnvironmentID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} + +func validateAppServiceEnvironmentName(v interface{}, k string) (warnings []string, errors []error) { + value := v.(string) + + if matched := regexp.MustCompile(`^[0-9a-zA-Z][-0-9a-zA-Z]{0,61}[0-9a-zA-Z]$`).Match([]byte(value)); !matched { + errors = append(errors, fmt.Errorf("%q may only contain alphanumeric characters and dashes up to 60 characters in length, and must start and end in an alphanumeric", k)) + } + + return warnings, errors +} + +func validateAppServiceEnvironmentPricingTier(v interface{}, k string) (warnings []string, errors []error) { + tier := v.(string) + + valid := []string{"I1", "I2", "I3"} + + for _, val := range valid { + if val == tier { + return + } + } + errors = append(errors, fmt.Errorf("pricing_tier must be one of %q", valid)) + return warnings, errors +} diff --git a/azurerm/internal/services/web/app_service_environment_test.go b/azurerm/internal/services/web/app_service_environment_test.go new file mode 100644 index 000000000000..942cac22492f --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_test.go @@ -0,0 +1,103 @@ +package web + +import "testing" + +func TestParseAppServiceEnvironmentID(t *testing.T) { + testData := []struct { + Name string + Input string + Expected *AppServiceEnvironmentResourceID + }{ + { + Name: "Empty", + Input: "", + Expected: nil, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Expected: nil, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Expected: nil, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Expected: nil, + }, + { + Name: "Missing environment name value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/providers/Microsoft.Web/hostingEnvironments/", + Expected: nil, + }, + { + Name: "Valid", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup1/providers/Microsoft.Web/hostingEnvironments/TestASEv2", + Expected: &AppServiceEnvironmentResourceID{ + ResourceGroup: "testGroup1", + Name: "TestASEv2", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := ParseAppServiceEnvironmentID(v.Input) + if err != nil { + if v.Expected == nil { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expected.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expected.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expected.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expected.ResourceGroup, actual.ResourceGroup) + } + } +} + +func TestValidateAppServiceEnvironmentID(t *testing.T) { + cases := []struct { + ID string + Valid bool + }{ + { + ID: "", + Valid: false, + }, + { + ID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Valid: false, + }, + { + ID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/", + Valid: false, + }, + { + ID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/providers/Microsoft.Web/hostingEnvironments/", + Valid: false, + }, + { + ID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup1/providers/Microsoft.Web/hostingEnvironments/TestASEv2", + Valid: true, + }, + } + for _, tc := range cases { + t.Logf("[DEBUG] Testing Value %s", tc.ID) + _, errors := ValidateAppServiceEnvironmentID(tc.ID, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/azurerm/internal/services/web/client/client.go b/azurerm/internal/services/web/client/client.go index 696f8b593547..0ff3d10a458c 100644 --- a/azurerm/internal/services/web/client/client.go +++ b/azurerm/internal/services/web/client/client.go @@ -2,18 +2,23 @@ package client import ( "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" + asev2 "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/common" ) type Client struct { - AppServicePlansClient *web.AppServicePlansClient - AppServicesClient *web.AppsClient - BaseClient *web.BaseClient - CertificatesClient *web.CertificatesClient - CertificatesOrderClient *web.AppServiceCertificateOrdersClient + AppServiceEnvironmentsClient *asev2.AppServiceEnvironmentsClient + AppServicePlansClient *web.AppServicePlansClient + AppServicesClient *web.AppsClient + BaseClient *web.BaseClient + CertificatesClient *web.CertificatesClient + CertificatesOrderClient *web.AppServiceCertificateOrdersClient } func NewClient(o *common.ClientOptions) *Client { + appServiceEnvironmentsClient := asev2.NewAppServiceEnvironmentsClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&appServiceEnvironmentsClient.Client, o.ResourceManagerAuthorizer) + appServicePlansClient := web.NewAppServicePlansClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&appServicePlansClient.Client, o.ResourceManagerAuthorizer) @@ -30,10 +35,11 @@ func NewClient(o *common.ClientOptions) *Client { o.ConfigureClient(&certificatesOrderClient.Client, o.ResourceManagerAuthorizer) return &Client{ - AppServicePlansClient: &appServicePlansClient, - AppServicesClient: &appServicesClient, - BaseClient: &baseClient, - CertificatesClient: &certificatesClient, - CertificatesOrderClient: &certificatesOrderClient, + AppServiceEnvironmentsClient: &appServiceEnvironmentsClient, + AppServicePlansClient: &appServicePlansClient, + AppServicesClient: &appServicesClient, + BaseClient: &baseClient, + CertificatesClient: &certificatesClient, + CertificatesOrderClient: &certificatesOrderClient, } } diff --git a/azurerm/internal/services/web/data_source_app_service_environment.go b/azurerm/internal/services/web/data_source_app_service_environment.go new file mode 100644 index 000000000000..c95e62f39355 --- /dev/null +++ b/azurerm/internal/services/web/data_source_app_service_environment.go @@ -0,0 +1,72 @@ +package web + +import ( + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmAppServiceEnvironment() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmAppServiceEnvironmentRead, + + Timeouts: &schema.ResourceTimeout{ + Read: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + + "front_end_scale_factor": { + Type: schema.TypeInt, + Computed: true, + }, + + "pricing_tier": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tags.SchemaDataSource(), + }, + } +} + +func dataSourceArmAppServiceEnvironmentRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServiceEnvironmentsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + resourceGroup := d.Get("resource_group_name").(string) + name := d.Get("name").(string) + + resp, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: App Service Environment %q (Resource Group %q) was not found", name, resourceGroup) + } + return fmt.Errorf("Error making read request on Azure App Service Environment %q: %+v", name, err) + } + + d.SetId(*resp.ID) + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + d.Set("front_end_scale_factor", int(*resp.FrontEndScaleFactor)) + d.Set("pricing_tier", convertToIsolatedSKU(*resp.MultiSize)) + d.Set("location", azure.NormalizeLocation(*resp.Location)) + + return tags.FlattenAndSet(d, resp.Tags) +} diff --git a/azurerm/internal/services/web/registration.go b/azurerm/internal/services/web/registration.go index 53fc4553211b..37589d215f5b 100644 --- a/azurerm/internal/services/web/registration.go +++ b/azurerm/internal/services/web/registration.go @@ -21,10 +21,11 @@ func (r Registration) WebsiteCategories() []string { // SupportedDataSources returns the supported Data Sources supported by this Service func (r Registration) SupportedDataSources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azurerm_app_service_plan": dataSourceAppServicePlan(), - "azurerm_app_service_certificate": dataSourceAppServiceCertificate(), "azurerm_app_service": dataSourceArmAppService(), "azurerm_app_service_certificate_order": dataSourceArmAppServiceCertificateOrder(), + "azurerm_app_service_environment": dataSourceArmAppServiceEnvironment(), + "azurerm_app_service_certificate": dataSourceAppServiceCertificate(), //TODO: rename this for consistency? + "azurerm_app_service_plan": dataSourceAppServicePlan(), //TODO: rename this for consistency? "azurerm_function_app": dataSourceArmFunctionApp(), } } @@ -36,6 +37,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_app_service_certificate": resourceArmAppServiceCertificate(), "azurerm_app_service_certificate_order": resourceArmAppServiceCertificateOrder(), "azurerm_app_service_custom_hostname_binding": resourceArmAppServiceCustomHostnameBinding(), + "azurerm_app_service_environment": resourceArmAppServiceEnvironment(), "azurerm_app_service_plan": resourceArmAppServicePlan(), "azurerm_app_service_slot": resourceArmAppServiceSlot(), "azurerm_app_service_source_control_token": resourceArmAppServiceSourceControlToken(), diff --git a/azurerm/internal/services/web/resource_arm_app_service_environment.go b/azurerm/internal/services/web/resource_arm_app_service_environment.go new file mode 100644 index 000000000000..cfd3e9e8d021 --- /dev/null +++ b/azurerm/internal/services/web/resource_arm_app_service_environment.go @@ -0,0 +1,282 @@ +package web + +import ( + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web" + "github.com/hashicorp/go-azure-helpers/response" + "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/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network" + "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 resourceArmAppServiceEnvironment() *schema.Resource { + return &schema.Resource{ + Create: resourceArmAppServiceEnvironmentCreateOrUpdate, + Read: resourceArmAppServiceEnvironmentRead, + Update: resourceArmAppServiceEnvironmentCreateOrUpdate, + Delete: resourceArmAppServiceEnvironmentDelete, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := ParseAppServiceEnvironmentID(id) + return err + }), + + // Need to find sane values for below, some operations on this resource can take an exceptionally long time + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(4 * time.Hour), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(4 * time.Hour), + Delete: schema.DefaultTimeout(4 * time.Hour), + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validateAppServiceEnvironmentName, + }, + + "internal_load_balancing_mode": { + Type: schema.TypeString, + Optional: true, + Default: string(web.InternalLoadBalancingModeNone), + ValidateFunc: validation.StringInSlice([]string{ + string(web.InternalLoadBalancingModeNone), + string(web.InternalLoadBalancingModePublishing), + string(web.InternalLoadBalancingModeWeb), + }, false), + }, + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: network.ValidateSubnetID, + }, + + // Note: This is currently 'multiSize' in the API for historic v1 reasons, may change in future? + "pricing_tier": { + Type: schema.TypeString, + Optional: true, + Default: "I1", + ValidateFunc: validateAppServiceEnvironmentPricingTier, + }, + + "front_end_scale_factor": { + Type: schema.TypeInt, + Optional: true, + Default: 15, + ValidateFunc: validation.IntBetween(5, 15), + }, + + "location": { + Type: schema.TypeString, + Computed: true, + StateFunc: azure.NormalizeLocation, + }, + + "resource_group_name": { + Type: schema.TypeString, + Computed: true, + }, + + "tags": tags.Schema(), + }, + } +} +func resourceArmAppServiceEnvironmentCreateOrUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServiceEnvironmentsClient + vnetClient := meta.(*clients.Client).Network.VnetClient + ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + name := d.Get("name").(string) + internalLoadBalancingMode := d.Get("internal_load_balancing_mode").(string) + t := d.Get("tags").(map[string]interface{}) + + subnetId := d.Get("subnet_id").(string) + subnet, err := network.ParseSubnetID(subnetId) + if err != nil { + return fmt.Errorf("Error parsing subnet id %q: %+v", subnetId, err) + } + + resourceGroup := subnet.ResourceGroup + + vnet, err := vnetClient.Get(ctx, resourceGroup, subnet.VirtualNetworkName, "") + if err != nil { + return fmt.Errorf("Error reading Virtual Network %q for App Service Environment %q: %+v", subnet.VirtualNetworkName, name, err) + } + + var location string + if vnetLoc := vnet.Location; vnetLoc != nil { + location = azure.NormalizeLocation(*vnetLoc) + } else { + return fmt.Errorf("Error determining Location from Virtual Network %s", *vnet.Name) + } + + frontEndScaleFactor := d.Get("front_end_scale_factor").(int) + + pricingTier := d.Get("pricing_tier").(string) + + // the SDK is coded primarily for v1, which needs a non-null entry for workerpool, so we construct an empty slice for it + wp := []web.WorkerPool{{}} + + envelope := web.AppServiceEnvironmentResource{ + Location: utils.String(location), + Kind: utils.String("ASEV2"), + AppServiceEnvironment: &web.AppServiceEnvironment{ + FrontEndScaleFactor: utils.Int32(int32(frontEndScaleFactor)), + MultiSize: utils.String(convertFromIsolatedSKU(pricingTier)), + Name: utils.String(name), + Location: utils.String(location), + InternalLoadBalancingMode: web.InternalLoadBalancingMode(internalLoadBalancingMode), + VirtualNetwork: &web.VirtualNetworkProfile{ + ID: utils.String(subnetId), + Subnet: utils.String(subnet.Name), + }, + WorkerPools: &wp, + }, + Tags: tags.Expand(t), + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, envelope) + if err != nil { + return fmt.Errorf("Error creating App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + return fmt.Errorf("Error waiting for the creation of App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + read, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return fmt.Errorf("Error retrieving App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.SetId(*read.ID) + + return resourceArmAppServiceEnvironmentRead(d, meta) +} + +func resourceArmAppServiceEnvironmentRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServiceEnvironmentsClient + ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := ParseAppServiceEnvironmentID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Name + + appServiceEnvironment, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(appServiceEnvironment.Response) { + log.Printf("[DEBUG] App Service Environmment %q (Resource Group %q) was not found!", name, resourceGroup) + d.SetId("") + return nil + } + return fmt.Errorf("Error retrieving App Service Environmment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + if location := appServiceEnvironment.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if err := tags.FlattenAndSet(d, appServiceEnvironment.Tags); err != nil { + return fmt.Errorf("Error flattening and setting tags in App Service Environment %q (resource group %q): %+v", name, resourceGroup, err) + } + + ase := appServiceEnvironment.AppServiceEnvironment + if ase.InternalLoadBalancingMode != "" { + d.Set("internal_load_balancing_mode", ase.InternalLoadBalancingMode) + } + if ase.VirtualNetwork.ID != nil { + d.Set("subnet_id", ase.VirtualNetwork.ID) + } + if ase.FrontEndScaleFactor != nil { + d.Set("front_end_scale_factor", int(*ase.FrontEndScaleFactor)) + } + if ase.MultiSize != nil { + d.Set("pricing_tier", convertToIsolatedSKU(*ase.MultiSize)) + } + + return nil +} + +func resourceArmAppServiceEnvironmentDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServiceEnvironmentsClient + ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := ParseAppServiceEnvironmentID(d.Id()) + if err != nil { + return err + } + + resGroup := id.ResourceGroup + name := id.Name + + log.Printf("[DEBUG] Deleting App Service Environment %q (Resource Group %q)", name, resGroup) + + // `true` below deletes any child resources (e.g. App Services / Plans / Certificates etc) + // This potentially destroys resources outside of Terraform's state without the user knowing + // It is set to true as this is consistent with other instances of this type of functionality in the provider. + future, err := client.Delete(ctx, resGroup, name, utils.Bool(true)) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return err + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return err + } + + return nil +} + +func convertFromIsolatedSKU(isolated string) (vmSKU string) { + switch isolated { + case "I1": + vmSKU = "Standard_D1_V2" + case "I2": + vmSKU = "Standard_D2_V2" + case "I3": + vmSKU = "Standard_D3_V2" + } + return vmSKU +} + +func convertToIsolatedSKU(vmSKU string) (isolated string) { + switch vmSKU { + case "Standard_D1_V2": + isolated = "I1" + case "Standard_D2_V2": + isolated = "I2" + case "Standard_D3_V2": + isolated = "I3" + } + return isolated +} diff --git a/azurerm/internal/services/web/tests/data_source_app_service_environment_test.go b/azurerm/internal/services/web/tests/data_source_app_service_environment_test.go new file mode 100644 index 000000000000..a26d0453d801 --- /dev/null +++ b/azurerm/internal/services/web/tests/data_source_app_service_environment_test.go @@ -0,0 +1,39 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" +) + +func TestDataSourceAzureRMAppServiceEnvironment_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_app_service_environment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + Steps: []resource.TestStep{ + { + Config: testAccDatasourceAppServiceEnvironment_basic(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet(data.ResourceName, "front_end_scale_factor"), + resource.TestCheckResourceAttrSet(data.ResourceName, "pricing_tier"), + ), + }, + }, + }) +} + +func testAccDatasourceAppServiceEnvironment_basic(data acceptance.TestData) string { + config := testAccAzureRMAppServiceEnvironment_basic(data) + return fmt.Sprintf(` +%s + +data "azurerm_app_service_environment" "test" { + name = "${azurerm_app_service_environment.test.name}" + resource_group_name = "${azurerm_app_service_environment.test.resource_group_name}" +} +`, config) +} diff --git a/azurerm/internal/services/web/tests/resource_arm_app_service_environment_test.go b/azurerm/internal/services/web/tests/resource_arm_app_service_environment_test.go new file mode 100644 index 000000000000..1623cc2d2a6b --- /dev/null +++ b/azurerm/internal/services/web/tests/resource_arm_app_service_environment_test.go @@ -0,0 +1,302 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMAppServiceEnvironment_basicWindows(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMAppServiceEnvironment_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceEnvironmentExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "pricing_tier", "I1"), + resource.TestCheckResourceAttr(data.ResourceName, "front_end_scale_factor", "15"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMAppServiceEnvironment_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMAppServiceEnvironment_basic(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceEnvironmentExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "pricing_tier", "I1"), + resource.TestCheckResourceAttr(data.ResourceName, "front_end_scale_factor", "15"), + ), + }, + data.ImportStep(), + { + Config: testAccAzureRMAppServiceEnvironment_update(data), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(data.ResourceName, "pricing_tier", "I2"), + resource.TestCheckResourceAttr(data.ResourceName, "front_end_scale_factor", "10"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMAppServiceEnvironment_tierAndScaleFactor(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment", "test") + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceEnvironmentDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMAppServiceEnvironment_tierAndScaleFactor(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceEnvironmentExists(data.ResourceName), + resource.TestCheckResourceAttr(data.ResourceName, "pricing_tier", "I2"), + resource.TestCheckResourceAttr(data.ResourceName, "front_end_scale_factor", "10"), + ), + }, + data.ImportStep(), + }, + }) +} + +func TestAccAzureRMAppServiceEnvironment_withAppServicePlan(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment", "test") + aspData := acceptance.BuildTestData(t, "azurerm_app_service_plan", "test") + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMAppServiceEnvironment_withAppServicePlan(data), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMAppServiceEnvironmentExists(data.ResourceName), + testCheckAppServicePlanMemberOfAppServiceEnvironment(data.ResourceName, aspData.ResourceName), + ), + }, + }, + }) +} + +func testAccAzureRMAppServiceEnvironment_basic(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-%[1]d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.0.0.0/16"] + + subnet { + name = "asesubnet" + address_prefix = "10.0.1.0/24" + } + + subnet { + name = "gatewaysubnet" + address_prefix = "10.0.2.0/24" + } +} + +data "azurerm_subnet" "test" { + name = "asesubnet" + virtual_network_name = "${azurerm_virtual_network.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_app_service_environment" "test" { + name = "acctest-ase-%[1]d" + subnet_id = "${data.azurerm_subnet.test.id}" +} +`, data.RandomInteger, data.Locations.Primary) +} + +func testAccAzureRMAppServiceEnvironment_tierAndScaleFactor(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%[1]d" + location = "%[2]s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-%[1]d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + address_space = ["10.0.0.0/16"] + + subnet { + name = "asesubnet" + address_prefix = "10.0.1.0/24" + } + + subnet { + name = "gatewaysubnet" + address_prefix = "10.0.2.0/24" + } +} + +data "azurerm_subnet" "test" { + name = "asesubnet" + virtual_network_name = "${azurerm_virtual_network.test.name}" + resource_group_name = "${azurerm_resource_group.test.name}" +} + +resource "azurerm_app_service_environment" "test" { + name = "acctest-ase-%[1]d" + subnet_id = "${data.azurerm_subnet.test.id}" + pricing_tier = "I2" + front_end_scale_factor = 10 +} +`, data.RandomInteger, data.Locations.Primary) +} + +func testAccAzureRMAppServiceEnvironment_update(data acceptance.TestData) string { + return testAccAzureRMAppServiceEnvironment_tierAndScaleFactor(data) +} + +func testAccAzureRMAppServiceEnvironment_withAppServicePlan(data acceptance.TestData) string { + template := testAccAzureRMAppServiceEnvironment_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_app_service_plan" "test"{ + name = "acctest-ASP-%d" + location = "${azurerm_resource_group.test.location}" + resource_group_name = "${azurerm_resource_group.test.name}" + app_service_environment_id = "${azurerm_app_service_environment.test.id}" + + sku { + tier = "Basic" + size = "B1" + } + +} +`, template, data.RandomInteger) +} + +func testCheckAzureRMAppServiceEnvironmentExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServiceEnvironmentsClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Not found: %s", resourceName) + } + + appServiceEnvironmentName := rs.Primary.Attributes["name"] + resourceGroup, hasResourceGroup := rs.Primary.Attributes["resource_group_name"] + if !hasResourceGroup { + return fmt.Errorf("Bad: no resource group found in state for App Service Environment: %s", appServiceEnvironmentName) + } + + resp, err := client.Get(ctx, resourceGroup, appServiceEnvironmentName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: App Service Environment %q (resource group %q) does not exist", appServiceEnvironmentName, resourceGroup) + } + + return fmt.Errorf("Bad: Get on appServiceEnvironmentClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMAppServiceEnvironmentDestroy(s *terraform.State) error { + client := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServiceEnvironmentsClient + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_app_service_environment" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + resp, err := client.Get(ctx, resourceGroup, name) + + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return nil + } + + return err + } + + return nil + } + + return nil +} + +func testCheckAppServicePlanMemberOfAppServiceEnvironment(ase string, asp string) resource.TestCheckFunc { + return func(s *terraform.State) error { + aseClient := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServiceEnvironmentsClient + aspClient := acceptance.AzureProvider.Meta().(*clients.Client).Web.AppServicePlansClient + ctx := acceptance.AzureProvider.Meta().(*clients.Client).StopContext + + aseResource, ok := s.RootModule().Resources[ase] + if !ok { + return fmt.Errorf("Not found: %s", ase) + } + + appServiceEnvironmentName := aseResource.Primary.Attributes["name"] + appServiceEnvironmentResourceGroup := aseResource.Primary.Attributes["resource_group_name"] + + aseResp, err := aseClient.Get(ctx, appServiceEnvironmentResourceGroup, appServiceEnvironmentName) + if err != nil { + if utils.ResponseWasNotFound(aseResp.Response) { + return fmt.Errorf("Bad: App Service Environment %q (resource group %q) does not exist: %+v", appServiceEnvironmentName, appServiceEnvironmentResourceGroup, err) + } + } + + aspResource, ok := s.RootModule().Resources[asp] + if !ok { + return fmt.Errorf("Not found: %s", ase) + } + + appServicePlanName := aspResource.Primary.Attributes["name"] + appServicePlanResourceGroup := aspResource.Primary.Attributes["resource_group_name"] + + aspResp, err := aspClient.Get(ctx, appServicePlanResourceGroup, appServicePlanName) + if err != nil { + if utils.ResponseWasNotFound(aseResp.Response) { + return fmt.Errorf("Bad: App Service Plan %q (resource group %q) does not exist: %+v", appServicePlanName, appServicePlanResourceGroup, err) + } + } + if aspResp.HostingEnvironmentProfile.ID != aseResp.ID { + return fmt.Errorf("Bad: App Service Plan %s not a member of App Service Environment %s", appServicePlanName, appServiceEnvironmentName) + } + + return nil + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ef8827c62e16..21fb930017fc 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -80,6 +80,7 @@ github.com/Azure/azure-sdk-for-go/services/signalr/mgmt/2018-10-01/signalr github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2019-04-01/storage github.com/Azure/azure-sdk-for-go/services/streamanalytics/mgmt/2016-03-01/streamanalytics github.com/Azure/azure-sdk-for-go/services/trafficmanager/mgmt/2018-04-01/trafficmanager +github.com/Azure/azure-sdk-for-go/services/web/mgmt/2018-02-01/web github.com/Azure/azure-sdk-for-go/services/web/mgmt/2019-08-01/web github.com/Azure/azure-sdk-for-go/version # github.com/Azure/go-autorest/autorest v0.9.3 diff --git a/website/docs/d/app_service_environment.html.markdown b/website/docs/d/app_service_environment.html.markdown new file mode 100644 index 000000000000..7b6b1b89a1ea --- /dev/null +++ b/website/docs/d/app_service_environment.html.markdown @@ -0,0 +1,43 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_app_service_environment" +description: |- + Gets information about an existing App Service Environment. +--- + +# Data Source: azurerm_app_service_environment + +Use this data source to access information about an existing App Service Environment + +## Example Usage + +```hcl +data "azure_app_service_plan" "example" { + name = "example-ase" + resource_group_name = "example-rg" +} + +output "app_service_environment_id" { + value = "${data.azurerm_app_service_environment.id}" +} + +``` + +## Argument Reference + +* `name` - (Required) The name of the App Service Environment. + +* `resource_group_name` - (Required) The Name of the Resource Group where the App Service Environment exists. + +## Attribute Reference + +* `id` - The ID of the App Service Environment. + +* `location` - The Azure location where the App Service Environment exists + +* `front_end_scale_factor` - The number of app instances per App Service Environment Front End + +* `pricing_tier` - The Pricing Tier (Isolated SKU) of the App Service Environment. + +* `tags` - A mapping of tags assigned to the resource. \ No newline at end of file diff --git a/website/docs/r/app_service_environment.html.markdown b/website/docs/r/app_service_environment.html.markdown new file mode 100644 index 000000000000..e863e6df06eb --- /dev/null +++ b/website/docs/r/app_service_environment.html.markdown @@ -0,0 +1,84 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_app_service_environment" +description: |- + Manages an App Service Environment v2. + +--- + +# azurerm_app_service_environment + +Manages a App Service Environment v2 + +*WARNING* Deleting an App Service Environment resource will also delete App Service Plans and App Services associated with it. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "exampleRG1" + location = "westeurope" +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet1" + location = "${azurerm_resource_group.example.location}" + resource_group_name = "${azurerm_resource_group.example.name}" + address_space = ["10.0.0.0/16"] + + subnet { + name = "asesubnet" + address_prefix = "10.0.1.0/24" + } + + subnet { + name = "gatewaysubnet" + address_prefix = "10.0.2.0/24" + } +} + +data "azurerm_subnet" "example" { + name = "asesubnet" + virtual_network_name = "${azurerm_virtual_network.example.name}" + resource_group_name = "${azurerm_resource_group.example.name}" +} + +resource "azurerm_app_service_environment" "example" { + name = "example-ase" + subnet_id = "${data.azurerm_subnet.example.id}" + pricing_tier = "I2" + front_end_scale_factor = 10 +} + +``` + +## Argument Reference + +* `name` - (Required) name of the App Service Environment. + +~> *NOTE* Must meet DNS name specification. + +* `subnet_id` - (Required) Resource ID for the ASE subnet. + +~> *NOTE* a /24 or larger CIDR is required. + +* `pricing_tier` - (Optional) Pricing tier for the front end instances. Possible values are `I1` (default), `I2` and `I3`. + +* `front_end_scale_factor` - (Optional) Scale factor for front end instances. Possible values are between `15` (default) and `5`. + +~> *NOTE* Lowering/changing this value has cost implications, see https://docs.microsoft.com/en-us/azure/app-service/environment/using-an-ase#front-end-scaling for details. + +## Attribute Reference + +* `id` - The ID of the App Services Environment. + +* `resource_group_name` - The name of the resource group. + +* `location` - The location the App Service Environment is deployed into. + +## Import + +```shell +terraform import azurerm_app_service_environment.myAppServiceEnv /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Web/hostingEnvironments/myAppServiceEnv +``` \ No newline at end of file