diff --git a/azurerm/internal/services/network/parse/subnet.go b/azurerm/internal/services/network/parse/subnet.go new file mode 100644 index 000000000000..a60ca6a00731 --- /dev/null +++ b/azurerm/internal/services/network/parse/subnet.go @@ -0,0 +1,38 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type SubnetId struct { + ResourceGroup string + VirtualNetworkName string + Name string +} + +func SubnetID(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 +} diff --git a/azurerm/internal/services/network/parse/subnet_test.go b/azurerm/internal/services/network/parse/subnet_test.go new file mode 100644 index 000000000000..ef696cba59b8 --- /dev/null +++ b/azurerm/internal/services/network/parse/subnet_test.go @@ -0,0 +1,90 @@ +package parse + +import ( + "testing" +) + +func TestSubnetID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *SubnetId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Missing Virtual Networks Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/", + Error: true, + }, + { + Name: "Missing Subnets Key", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/network1", + Error: true, + }, + { + Name: "Missing Subnets Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/network1/subnets/", + Error: true, + }, + { + Name: "Subnet ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/network1/subnets/subnet1", + Error: false, + Expect: &SubnetId{ + ResourceGroup: "resGroup1", + VirtualNetworkName: "network1", + Name: "subnet1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/network1/Subnets/subnet1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := SubnetID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.VirtualNetworkName != v.Expect.VirtualNetworkName { + t.Fatalf("Expected %q but got %q for Virtual Network Name", v.Expect.VirtualNetworkName, actual.VirtualNetworkName) + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/network/parse/virtual_network.go b/azurerm/internal/services/network/parse/virtual_network.go new file mode 100644 index 000000000000..b789e8aefb40 --- /dev/null +++ b/azurerm/internal/services/network/parse/virtual_network.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type VirtualNetworkId struct { + ResourceGroup string + Name string +} + +func VirtualNetworkID(input string) (*VirtualNetworkId, error) { + id, err := azure.ParseAzureResourceID(input) + if err != nil { + return nil, fmt.Errorf("[ERROR] Unable to parse Virtual Network 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 +} diff --git a/azurerm/internal/services/network/parse/virtual_network_test.go b/azurerm/internal/services/network/parse/virtual_network_test.go new file mode 100644 index 000000000000..b9eb6b35d357 --- /dev/null +++ b/azurerm/internal/services/network/parse/virtual_network_test.go @@ -0,0 +1,70 @@ +package parse + +import ( + "testing" +) + +func TestVirtualNetworkID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *VirtualNetworkId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "No Resource Groups Segment", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000", + Error: true, + }, + { + Name: "No Resource Groups Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/", + Error: true, + }, + { + Name: "Resource Group ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/foo/", + Error: true, + }, + { + Name: "Virtual Network ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/virtualNetworks/network1", + Error: false, + Expect: &VirtualNetworkId{ + ResourceGroup: "resGroup1", + Name: "network1", + }, + }, + { + Name: "Wrong Casing", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/resGroup1/providers/Microsoft.Network/VirtualNetworks/network1", + Error: true, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := VirtualNetworkID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.Name != v.Expect.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.Name, actual.Name) + } + + if actual.ResourceGroup != v.Expect.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ResourceGroup, actual.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/network/validate/subnet.go b/azurerm/internal/services/network/validate/subnet.go new file mode 100644 index 000000000000..625f73014824 --- /dev/null +++ b/azurerm/internal/services/network/validate/subnet.go @@ -0,0 +1,23 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" +) + +// SubnetID validates that the specified ID is a valid Subnet ID +func SubnetID(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 := parse.SubnetID(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/validate/virtual_network.go b/azurerm/internal/services/network/validate/virtual_network.go new file mode 100644 index 000000000000..03b10e31f2ef --- /dev/null +++ b/azurerm/internal/services/network/validate/virtual_network.go @@ -0,0 +1,23 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" +) + +// VirtualNetworkID validates that the specified ID is a valid Virtual Network ID +func VirtualNetworkID(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 := parse.VirtualNetworkID(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_data_source.go b/azurerm/internal/services/web/app_service_environment_data_source.go new file mode 100644 index 000000000000..4e99c9de26c3 --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_data_source.go @@ -0,0 +1,86 @@ +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, + }, + + "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 retrieving App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.SetId(*resp.ID) + + d.Set("name", name) + d.Set("resource_group_name", resourceGroup) + + if loc := resp.Location; loc != nil { + d.Set("location", azure.NormalizeLocation(*loc)) + } + + if props := resp.AppServiceEnvironment; props != nil { + frontendScaleFactor := 0 + if props.FrontEndScaleFactor != nil { + frontendScaleFactor = int(*props.FrontEndScaleFactor) + } + d.Set("front_end_scale_factor", frontendScaleFactor) + + pricingTier := "" + if props.MultiSize != nil { + pricingTier = convertFromIsolatedSKU(*props.MultiSize) + } + d.Set("pricing_tier", pricingTier) + } + + return tags.FlattenAndSet(d, resp.Tags) +} diff --git a/azurerm/internal/services/web/app_service_environment_resource.go b/azurerm/internal/services/web/app_service_environment_resource.go new file mode 100644 index 000000000000..bda7acb22fb6 --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_resource.go @@ -0,0 +1,376 @@ +package web + +import ( + "context" + "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/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + networkParse "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" + networkValidate "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/web/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/web/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 resourceArmAppServiceEnvironment() *schema.Resource { + return &schema.Resource{ + Create: resourceArmAppServiceEnvironmentCreate, + Read: resourceArmAppServiceEnvironmentRead, + Update: resourceArmAppServiceEnvironmentUpdate, + Delete: resourceArmAppServiceEnvironmentDelete, + Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { + _, err := parse.AppServiceEnvironmentID(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: validate.AppServiceEnvironmentName, + }, + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: networkValidate.SubnetID, + }, + + "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), + }, + + "front_end_scale_factor": { + Type: schema.TypeInt, + Optional: true, + Default: 15, + ValidateFunc: validation.IntBetween(5, 15), + }, + + "pricing_tier": { + Type: schema.TypeString, + Optional: true, + Default: "I1", + ValidateFunc: validation.StringInSlice([]string{ + "I1", + "I2", + "I3", + }, false), + }, + + "tags": tags.ForceNewSchema(), + + // Computed + "location": { + Type: schema.TypeString, + Computed: true, + }, + + "resource_group_name": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceArmAppServiceEnvironmentCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Web.AppServiceEnvironmentsClient + networksClient := 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 := networkParse.SubnetID(subnetId) + if err != nil { + return err + } + + resourceGroup := subnet.ResourceGroup + vnet, err := networksClient.Get(ctx, resourceGroup, subnet.VirtualNetworkName, "") + if err != nil { + return fmt.Errorf("Error retrieving Virtual Network %q (Resource Group %q): %+v", subnet.VirtualNetworkName, resourceGroup, err) + } + + // the App Service Environment has to be in the same location as the Virtual Network + var location string + if loc := vnet.Location; loc != nil { + location = azure.NormalizeLocation(*loc) + } else { + return fmt.Errorf("Error determining Location from Virtual Network %q (Resource Group %q): `location` was nil", subnet.VirtualNetworkName, resourceGroup) + } + + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing App Service Environment %q (Resource Group %q): %s", name, resourceGroup, err) + } + } + + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_app_service_environment", *existing.ID) + } + + frontEndScaleFactor := d.Get("front_end_scale_factor").(int) + pricingTier := d.Get("pricing_tier").(string) + + envelope := web.AppServiceEnvironmentResource{ + Location: utils.String(location), + Kind: utils.String("ASEV2"), + AppServiceEnvironment: &web.AppServiceEnvironment{ + Name: utils.String(name), + Location: utils.String(location), + InternalLoadBalancingMode: web.InternalLoadBalancingMode(internalLoadBalancingMode), + FrontEndScaleFactor: utils.Int32(int32(frontEndScaleFactor)), + MultiSize: utils.String(convertFromIsolatedSKU(pricingTier)), + VirtualNetwork: &web.VirtualNetworkProfile{ + ID: utils.String(subnetId), + Subnet: utils.String(subnet.Name), + }, + + // the SDK is coded primarily for v1, which needs a non-null entry for workerpool, so we construct an empty slice for it + // TODO: remove this hack once https://github.com/Azure/azure-rest-api-specs/pull/8433 has been merged + WorkerPools: &[]web.WorkerPool{{}}, + }, + Tags: tags.Expand(t), + } + + // whilst this returns a future go-autorest has a max number of retries + if _, err := client.CreateOrUpdate(ctx, resourceGroup, name, envelope); err != nil { + return fmt.Errorf("Error creating App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + // as such we'll ignore it and use a custom poller instead + if err := waitForAppServiceEnvironmentToStabilize(ctx, client, resourceGroup, name); 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 resourceArmAppServiceEnvironmentUpdate(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 := parse.AppServiceEnvironmentID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Name + + environment := web.AppServiceEnvironmentPatchResource{ + AppServiceEnvironment: &web.AppServiceEnvironment{}, + } + + if d.HasChange("internal_load_balancing_mode") { + v := d.Get("internal_load_balancing_mode").(string) + environment.AppServiceEnvironment.InternalLoadBalancingMode = web.InternalLoadBalancingMode(v) + } + + if d.HasChange("front_end_scale_factor") { + v := d.Get("front_end_scale_factor").(int) + environment.AppServiceEnvironment.FrontEndScaleFactor = utils.Int32(int32(v)) + } + + if d.HasChange("pricing_tier") { + v := d.Get("pricing_tier").(string) + v = convertFromIsolatedSKU(v) + environment.AppServiceEnvironment.MultiSize = utils.String(v) + } + + if _, err := client.Update(ctx, resourceGroup, name, environment); err != nil { + return fmt.Errorf("Error updating App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err := waitForAppServiceEnvironmentToStabilize(ctx, client, resourceGroup, name); err != nil { + return fmt.Errorf("Error waiting for Update of App Service Environment %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + 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 := parse.AppServiceEnvironmentID(d.Id()) + if err != nil { + return err + } + + resourceGroup := id.ResourceGroup + name := id.Name + + existing, err := client.Get(ctx, resourceGroup, name) + if err != nil { + if utils.ResponseWasNotFound(existing.Response) { + log.Printf("[DEBUG] App Service Environmment %q (Resource Group %q) was not found - removing from state!", 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 := existing.Location; location != nil { + d.Set("location", azure.NormalizeLocation(*location)) + } + + if props := existing.AppServiceEnvironment; props != nil { + d.Set("internal_load_balancing_mode", string(props.InternalLoadBalancingMode)) + + subnetId := "" + if props.VirtualNetwork != nil && props.VirtualNetwork.ID != nil { + subnetId = *props.VirtualNetwork.ID + } + d.Set("subnet_id", subnetId) + + frontendScaleFactor := 0 + if props.FrontEndScaleFactor != nil { + frontendScaleFactor = int(*props.FrontEndScaleFactor) + } + d.Set("front_end_scale_factor", frontendScaleFactor) + + pricingTier := "" + if props.MultiSize != nil { + pricingTier = convertToIsolatedSKU(*props.MultiSize) + } + d.Set("pricing_tier", pricingTier) + } + + return tags.FlattenAndSet(d, existing.Tags) +} + +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 := parse.AppServiceEnvironmentID(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) + + // TODO: should this behaviour be added to the `features` block? + forceDeleteAllChildren := utils.Bool(false) + future, err := client.Delete(ctx, resGroup, name, forceDeleteAllChildren) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error deleting App Service Environment %q (Resource Group %q): %+v", name, resGroup, err) + } + + err = future.WaitForCompletionRef(ctx, client.Client) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + + return fmt.Errorf("Error waiting for deletion of App Service Environment %q (Resource Group %q): %+v", name, resGroup, err) + } + + return nil +} + +func waitForAppServiceEnvironmentToStabilize(ctx context.Context, client *web.AppServiceEnvironmentsClient, resourceGroup string, name string) error { + for { + time.Sleep(1 * time.Minute) + + read, err := client.Get(ctx, resourceGroup, name) + if err != nil { + return err + } + + if read.AppServiceEnvironment == nil { + return fmt.Errorf("`properties` was nil") + } + + state := read.AppServiceEnvironment.ProvisioningState + if state == web.ProvisioningStateSucceeded { + return nil + } + + if state == web.ProvisioningStateInProgress { + continue + } + + return fmt.Errorf("Unexpected ProvisioningState: %q", state) + } +} + +// Note: These are abstractions and possibly subject to change if Azure changes the underlying SKU for Isolated instances. +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/client/client.go b/azurerm/internal/services/web/client/client.go index 696f8b593547..6c89651665ad 100644 --- a/azurerm/internal/services/web/client/client.go +++ b/azurerm/internal/services/web/client/client.go @@ -6,14 +6,18 @@ import ( ) type Client struct { - AppServicePlansClient *web.AppServicePlansClient - AppServicesClient *web.AppsClient - BaseClient *web.BaseClient - CertificatesClient *web.CertificatesClient - CertificatesOrderClient *web.AppServiceCertificateOrdersClient + AppServiceEnvironmentsClient *web.AppServiceEnvironmentsClient + AppServicePlansClient *web.AppServicePlansClient + AppServicesClient *web.AppsClient + BaseClient *web.BaseClient + CertificatesClient *web.CertificatesClient + CertificatesOrderClient *web.AppServiceCertificateOrdersClient } func NewClient(o *common.ClientOptions) *Client { + appServiceEnvironmentsClient := web.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 +34,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/parse/app_service_environment.go b/azurerm/internal/services/web/parse/app_service_environment.go new file mode 100644 index 000000000000..7189915717ae --- /dev/null +++ b/azurerm/internal/services/web/parse/app_service_environment.go @@ -0,0 +1,33 @@ +package parse + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" +) + +type AppServiceEnvironmentResourceID struct { + ResourceGroup string + Name string +} + +func AppServiceEnvironmentID(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 +} diff --git a/azurerm/internal/services/web/parse/app_service_environment_test.go b/azurerm/internal/services/web/parse/app_service_environment_test.go new file mode 100644 index 000000000000..5600c8b6efdb --- /dev/null +++ b/azurerm/internal/services/web/parse/app_service_environment_test.go @@ -0,0 +1,71 @@ +package parse + +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", + }, + }, + { + Name: "Wrong Case", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/testGroup1/providers/Microsoft.Web/HostingEnvironments/TestASEv2", + Expected: nil, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := AppServiceEnvironmentID(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) + } + } +} diff --git a/azurerm/internal/services/web/registration.go b/azurerm/internal/services/web/registration.go index 53fc4553211b..282159c5e36e 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(), + "azurerm_app_service_plan": dataSourceAppServicePlan(), "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_plan.go b/azurerm/internal/services/web/resource_arm_app_service_plan.go index 3a40be35b8a1..6e9d7e38421d 100644 --- a/azurerm/internal/services/web/resource_arm_app_service_plan.go +++ b/azurerm/internal/services/web/resource_arm_app_service_plan.go @@ -33,10 +33,10 @@ func resourceArmAppServicePlan() *schema.Resource { }), Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(30 * time.Minute), + Create: schema.DefaultTimeout(60 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(30 * time.Minute), - Delete: schema.DefaultTimeout(30 * time.Minute), + Update: schema.DefaultTimeout(60 * time.Minute), + Delete: schema.DefaultTimeout(60 * time.Minute), }, Schema: map[string]*schema.Schema{ @@ -264,17 +264,23 @@ func resourceArmAppServicePlanRead(d *schema.ResourceData, meta interface{}) err d.Set("kind", resp.Kind) if props := resp.AppServicePlanProperties; props != nil { - if profile := props.HostingEnvironmentProfile; profile != nil { - d.Set("app_service_environment_id", profile.ID) + appServiceEnvironmentId := "" + if props.HostingEnvironmentProfile != nil && props.HostingEnvironmentProfile.ID != nil { + appServiceEnvironmentId = *props.HostingEnvironmentProfile.ID } + d.Set("app_service_environment_id", appServiceEnvironmentId) + maximumNumberOfWorkers := 0 if props.MaximumNumberOfWorkers != nil { - d.Set("maximum_number_of_workers", int(*props.MaximumNumberOfWorkers)) + maximumNumberOfWorkers = int(*props.MaximumNumberOfWorkers) } + d.Set("maximum_number_of_workers", maximumNumberOfWorkers) + maximumElasticWorkerCount := 0 if props.MaximumElasticWorkerCount != nil { - d.Set("maximum_elastic_worker_count", int(*props.MaximumElasticWorkerCount)) + maximumElasticWorkerCount = int(*props.MaximumElasticWorkerCount) } + d.Set("maximum_elastic_worker_count", maximumElasticWorkerCount) d.Set("per_site_scaling", props.PerSiteScaling) d.Set("reserved", props.Reserved) diff --git a/azurerm/internal/services/web/tests/app_service_environment_data_source_test.go b/azurerm/internal/services/web/tests/app_service_environment_data_source_test.go new file mode 100644 index 000000000000..a38c32a878ed --- /dev/null +++ b/azurerm/internal/services/web/tests/app_service_environment_data_source_test.go @@ -0,0 +1,40 @@ +package tests + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" +) + +func TestAccDataSourceAzureRMAppServiceEnvironment_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, + CheckDestroy: testCheckAzureRMAppServiceEnvironmentDestroy, + 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/app_service_environment_resource_test.go b/azurerm/internal/services/web/tests/app_service_environment_resource_test.go new file mode 100644 index 000000000000..e70014f11122 --- /dev/null +++ b/azurerm/internal/services/web/tests/app_service_environment_resource_test.go @@ -0,0 +1,271 @@ +package tests + +import ( + "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/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMAppServiceEnvironment_basic(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_requiresImport(t *testing.T) { + if !features.ShouldResourcesBeImported() { + t.Skip("Skipping since resources aren't required to be imported") + return + } + + 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), + ), + }, + data.RequiresImportErrorStep(testAccAzureRMAppServiceEnvironment_requiresImport), + }, + }) +} + +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_tierAndScaleFactor(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), + resource.TestCheckResourceAttrPair(data.ResourceName, "id", aspData.ResourceName, "app_service_environment_id"), + ), + }, + data.ImportStep(), + }, + }) +} + +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 testAccAzureRMAppServiceEnvironment_basic(data acceptance.TestData) string { + template := testAccAzureRMAppServiceEnvironment_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_app_service_environment" "test" { + name = "acctest-ase-%d" + subnet_id = azurerm_subnet.ase.id +} +`, template, data.RandomInteger) +} + +func testAccAzureRMAppServiceEnvironment_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMAppServiceEnvironment_basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_app_service_environment" "import" { + name = azurerm_app_service_environment.test.name + subnet_id = azurerm_app_service_environment.test.subnet_id +} +`, template) +} + +func testAccAzureRMAppServiceEnvironment_tierAndScaleFactor(data acceptance.TestData) string { + template := testAccAzureRMAppServiceEnvironment_template(data) + return fmt.Sprintf(` +%s + +resource "azurerm_app_service_environment" "test" { + name = "acctest-ase-%d" + subnet_id = azurerm_subnet.ase.id + pricing_tier = "I2" + front_end_scale_factor = 10 +} +`, template, data.RandomInteger) +} + +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 = "Isolated" + size = "I1" + capacity = 1 + } +} +`, template, data.RandomInteger) +} + +func testAccAzureRMAppServiceEnvironment_template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestRG-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctest-vnet-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + address_space = ["10.0.0.0/16"] +} + +resource "azurerm_subnet" "ase" { + name = "asesubnet" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.0.1.0/24" +} + +resource "azurerm_subnet" "gateway" { + name = "gatewaysubnet" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.0.2.0/24" +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/web/tests/data_source_app_service_plan_test.go b/azurerm/internal/services/web/tests/data_source_app_service_plan_test.go index 0b6d291c9d01..83076a2e4399 100644 --- a/azurerm/internal/services/web/tests/data_source_app_service_plan_test.go +++ b/azurerm/internal/services/web/tests/data_source_app_service_plan_test.go @@ -14,8 +14,9 @@ func TestAccDataSourceAzureRMAppServicePlan_basic(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service_plan", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServicePlanDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppServicePlan_basic(data), @@ -35,8 +36,9 @@ func TestAccDataSourceAzureRMAppServicePlan_complete(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service_plan", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServicePlanDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppServicePlan_complete(data), @@ -57,8 +59,9 @@ func TestAccDataSourceAzureRMAppServicePlan_premiumSKU(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service_plan", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServicePlanDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppServicePlan_premiumSKU(data), @@ -78,8 +81,9 @@ func TestAccDataSourceAzureRMAppServicePlan_basicWindowsContainer(t *testing.T) data := acceptance.BuildTestData(t, "data.azurerm_app_service_plan", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServicePlanDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppServicePlan_basicWindowsContainer(data), diff --git a/azurerm/internal/services/web/tests/data_source_app_service_test.go b/azurerm/internal/services/web/tests/data_source_app_service_test.go index b94041087df6..bc4f75c25aba 100644 --- a/azurerm/internal/services/web/tests/data_source_app_service_test.go +++ b/azurerm/internal/services/web/tests/data_source_app_service_test.go @@ -12,8 +12,9 @@ func TestAccDataSourceAzureRMAppService_basic(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_basic(data), @@ -32,8 +33,9 @@ func TestAccDataSourceAzureRMAppService_tags(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_tags(data), @@ -50,8 +52,9 @@ func TestAccDataSourceAzureRMAppService_clientAppAffinityDisabled(t *testing.T) data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_clientAffinityDisabled(data), @@ -67,8 +70,9 @@ func TestAccDataSourceAzureRMAppService_32Bit(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_32Bit(data), @@ -84,8 +88,9 @@ func TestAccDataSourceAzureRMAppService_appSettings(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_appSettings(data), @@ -101,8 +106,9 @@ func TestAccDataSourceAzureRMAppService_connectionString(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_connectionStrings(data), @@ -123,8 +129,9 @@ func TestAccDataSourceAzureRMAppService_ipRestriction(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_ipRestriction(data), @@ -140,8 +147,9 @@ func TestAccDataSourceAzureRMAppService_http2Enabled(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_http2Enabled(data), @@ -157,8 +165,9 @@ func TestAccDataSourceAzureRMAppService_minTls(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_minTls(data), @@ -174,8 +183,9 @@ func TestAccDataSourceAzureRMAppService_basicWindowsContainer(t *testing.T) { data := acceptance.BuildTestData(t, "data.azurerm_app_service", "test") resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, + PreCheck: func() { acceptance.PreCheck(t) }, + Providers: acceptance.SupportedProviders, + CheckDestroy: testCheckAzureRMAppServiceDestroy, Steps: []resource.TestStep{ { Config: testAccDataSourceAppService_basicWindowsContainer(data), diff --git a/azurerm/internal/services/web/validate/app_service_environment.go b/azurerm/internal/services/web/validate/app_service_environment.go new file mode 100644 index 000000000000..9e7b63222213 --- /dev/null +++ b/azurerm/internal/services/web/validate/app_service_environment.go @@ -0,0 +1,33 @@ +package validate + +import ( + "fmt" + "regexp" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/web/parse" +) + +func AppServiceEnvironmentID(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 := parse.AppServiceEnvironmentID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} + +func AppServiceEnvironmentName(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 +} diff --git a/azurerm/internal/services/web/validate/app_service_environment_test.go b/azurerm/internal/services/web/validate/app_service_environment_test.go new file mode 100644 index 000000000000..9f6b1d44b4c8 --- /dev/null +++ b/azurerm/internal/services/web/validate/app_service_environment_test.go @@ -0,0 +1,40 @@ +package validate + +import "testing" + +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 := AppServiceEnvironmentID(tc.ID, "test") + valid := len(errors) == 0 + + if tc.Valid != valid { + t.Fatalf("Expected %t but got %t", tc.Valid, valid) + } + } +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 450524c095bb..e6e2164200c2 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -83,11 +83,15 @@
  • - azurerm_app_service_plan + azurerm_app_service_certificate
  • - azurerm_app_service_certificate + azurerm_app_service_environment +
  • + +
  • + azurerm_app_service_plan
  • @@ -695,6 +699,10 @@ azurerm_app_service_custom_hostname_binding
  • +
  • + azurerm_app_service_environment +
  • +
  • azurerm_app_service_plan
  • 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..4f9a554b62cc --- /dev/null +++ b/website/docs/r/app_service_environment.html.markdown @@ -0,0 +1,76 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_app_service_environment" +description: |- + Manages an App Service Environment. + +--- + +# azurerm_app_service_environment + +Manages an App Service Environment. + +## 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"] +} + +resource "azurerm_subnet" "ase" { + name = "asesubnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefix = "10.0.1.0/24" +} + +resource "azurerm_subnet" "gateway" { + name = "gatewaysubnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefix = "10.0.2.0/24" +} + +resource "azurerm_app_service_environment" "example" { + name = "example-ase" + subnet_id = azurerm_subnet.ase.id + pricing_tier = "I2" + front_end_scale_factor = 10 +} + +``` + +## Argument Reference + +* `name` - (Required) The name of the App Service Environment. Changing this forces a new resource to be created. + +* `subnet_id` - (Required) The ID of the Subnet which the App Service Environment should be connected to. Changing this forces a new resource to be created. + +~> **NOTE** a /24 or larger CIDR is required. Once associated with an ASE this size cannot be changed. + +* `pricing_tier` - (Optional) Pricing tier for the front end instances. Possible values are `I1`, `I2` and `I3`. Defaults to `I1`. + +* `front_end_scale_factor` - (Optional) Scale factor for front end instances. Possible values are between `5` and `15`. Defaults to `15`. + +## Attribute Reference + +* `id` - The ID of the App Service Environment. + +* `resource_group_name` - The name of the Resource Group where the App Service Environment exists. + +* `location` - The location where the App Service Environment exists. + +## 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 diff --git a/website/docs/r/app_service_plan.html.markdown b/website/docs/r/app_service_plan.html.markdown index 061859390bf0..5a0ce52bd049 100644 --- a/website/docs/r/app_service_plan.html.markdown +++ b/website/docs/r/app_service_plan.html.markdown @@ -143,10 +143,10 @@ The following attributes are exported: The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: -* `create` - (Defaults to 30 minutes) Used when creating the App Service Plan. -* `update` - (Defaults to 30 minutes) Used when updating the App Service Plan. +* `create` - (Defaults to 60 minutes) Used when creating the App Service Plan. +* `update` - (Defaults to 60 minutes) Used when updating the App Service Plan. * `read` - (Defaults to 5 minutes) Used when retrieving the App Service Plan. -* `delete` - (Defaults to 30 minutes) Used when deleting the App Service Plan. +* `delete` - (Defaults to 60 minutes) Used when deleting the App Service Plan. ## Import