From 13c64c676c8e0263734ff1b07292a83a5e6681e4 Mon Sep 17 00:00:00 2001 From: Steve <11830746+jackofallops@users.noreply.github.com> Date: Tue, 4 May 2021 19:05:19 +0100 Subject: [PATCH] New Resource: `azurerm_app_service_environment_v3` (#11174) Co-authored-by: kt This PR introduces the preview resource, App Service Environment V3. SDK/API coverage is incomplete, so two notable items are missing, but will be added if/when they are supported. (These are Host group deployment, and upgrade preference.) supersedes #10304 Resolves #9420 --- azurerm/internal/provider/services.go | 1 + .../app_service_environment_v3_data_source.go | 111 ++++++ ...service_environment_v3_data_source_test.go | 37 ++ .../app_service_environment_v3_resource.go | 336 ++++++++++++++++++ ...pp_service_environment_v3_resource_test.go | 237 ++++++++++++ azurerm/internal/services/web/registration.go | 18 + azurerm/utils/int.go | 21 ++ .../app_service_environment_v3.html.markdown | 58 +++ .../app_service_environment_v3.html.markdown | 126 +++++++ 9 files changed, 945 insertions(+) create mode 100644 azurerm/internal/services/web/app_service_environment_v3_data_source.go create mode 100644 azurerm/internal/services/web/app_service_environment_v3_data_source_test.go create mode 100644 azurerm/internal/services/web/app_service_environment_v3_resource.go create mode 100644 azurerm/internal/services/web/app_service_environment_v3_resource_test.go create mode 100644 azurerm/utils/int.go create mode 100644 website/docs/d/app_service_environment_v3.html.markdown create mode 100644 website/docs/r/app_service_environment_v3.html.markdown diff --git a/azurerm/internal/provider/services.go b/azurerm/internal/provider/services.go index 852cd624040c..522e88180cbf 100644 --- a/azurerm/internal/provider/services.go +++ b/azurerm/internal/provider/services.go @@ -102,6 +102,7 @@ func SupportedTypedServices() []sdk.TypedServiceRegistration { eventhub.Registration{}, loadbalancer.Registration{}, resource.Registration{}, + web.Registration{}, } } diff --git a/azurerm/internal/services/web/app_service_environment_v3_data_source.go b/azurerm/internal/services/web/app_service_environment_v3_data_source.go new file mode 100644 index 000000000000..c178cdf12bb3 --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_v3_data_source.go @@ -0,0 +1,111 @@ +package web + +import ( + "context" + "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/location" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/sdk" + "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" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type AppServiceEnvironmentV3DataSource struct{} + +var _ sdk.DataSource = AppServiceEnvironmentV3DataSource{} + +func (r AppServiceEnvironmentV3DataSource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validate.AppServiceEnvironmentName, + }, + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + } +} + +func (r AppServiceEnvironmentV3DataSource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + + "cluster_setting": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + + "value": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + + "tags": tags.SchemaDataSource(), + } +} + +func (r AppServiceEnvironmentV3DataSource) ModelObject() interface{} { + return AppServiceEnvironmentV3Model{} +} + +func (r AppServiceEnvironmentV3DataSource) ResourceType() string { + return "azurerm_app_service_environment_v3" +} + +func (r AppServiceEnvironmentV3DataSource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Web.AppServiceEnvironmentsClient + id, err := parse.AppServiceEnvironmentID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + existing, err := client.Get(ctx, id.ResourceGroup, id.HostingEnvironmentName) + if err != nil { + if utils.ResponseWasNotFound(existing.Response) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + model := AppServiceEnvironmentV3Model{ + Name: id.HostingEnvironmentName, + ResourceGroup: id.ResourceGroup, + Location: location.NormalizeNilable(existing.Location), + } + + if props := existing.AppServiceEnvironment; props != nil { + if props.VirtualNetwork != nil { + model.SubnetId = utils.NormalizeNilableString(props.VirtualNetwork.ID) + } + + model.PricingTier = utils.NormalizeNilableString(props.MultiSize) + + model.ClusterSetting = flattenClusterSettingsModel(props.ClusterSettings) + } + + model.Tags = tags.Flatten(existing.Tags) + + return metadata.Encode(&model) + }, + } +} diff --git a/azurerm/internal/services/web/app_service_environment_v3_data_source_test.go b/azurerm/internal/services/web/app_service_environment_v3_data_source_test.go new file mode 100644 index 000000000000..c9a5b702f05d --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_v3_data_source_test.go @@ -0,0 +1,37 @@ +package web_test + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" +) + +type AppServiceEnvironmentV3DataSource struct{} + +func TestAccAppServiceEnvironmentV3DataSource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "data.azurerm_app_service_environment_v3", "test") + + data.DataSourceTest(t, []resource.TestStep{ + { + Config: AppServiceEnvironmentV3DataSource{}.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Key("cluster_setting.#").HasValue("2"), + check.That(data.ResourceName).Key("tags.#").HasValue("2"), + ), + }, + }) +} + +func (AppServiceEnvironmentV3DataSource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +data "azurerm_app_service_environment_v3" "test" { + name = azurerm_app_service_environment_v3.test.name + resource_group_name = azurerm_app_service_environment_v3.test.resource_group_name +} +`, AppServiceEnvironmentV3Resource{}.complete(data)) +} diff --git a/azurerm/internal/services/web/app_service_environment_v3_resource.go b/azurerm/internal/services/web/app_service_environment_v3_resource.go new file mode 100644 index 000000000000..2b502bfacfb2 --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_v3_resource.go @@ -0,0 +1,336 @@ +package web + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/response" + + "github.com/Azure/azure-sdk-for-go/services/web/mgmt/2020-06-01/web" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "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/location" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/sdk" + 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" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +const KindASEV3 = "ASEV3" + +type ClusterSettingModel struct { + Name string `tfschema:"name"` + Value string `tfschema:"value"` +} + +type AppServiceEnvironmentV3Model struct { + Name string `tfschema:"name"` + ResourceGroup string `tfschema:"resource_group_name"` + SubnetId string `tfschema:"subnet_id"` + ClusterSetting []ClusterSettingModel `tfschema:"cluster_setting"` + PricingTier string `tfschema:"pricing_tier"` + Location string `tfschema:"location"` + Tags map[string]interface{} `tfschema:"tags"` +} + +// (@jackofallops) - Two important properties are missing from the SDK / Swagger that will need to be added later +// these are `dedicated_host_count` https://docs.microsoft.com/en-gb/azure/app-service/environment/creation#dedicated-hosts +// and `upgrade_preference` https://docs.microsoft.com/en-us/azure/app-service/environment/using#upgrade-preference + +type AppServiceEnvironmentV3Resource struct{} + +var _ sdk.Resource = AppServiceEnvironmentV3Resource{} +var _ sdk.ResourceWithUpdate = AppServiceEnvironmentV3Resource{} + +func (r AppServiceEnvironmentV3Resource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.AppServiceEnvironmentName, + }, + + "resource_group_name": azure.SchemaResourceGroupName(), + + "subnet_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: networkValidate.SubnetID, + }, + + "cluster_setting": { + Type: schema.TypeList, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "value": { + Type: schema.TypeString, + Required: true, + }, + }, + }, + }, + + "tags": tags.ForceNewSchema(), + } +} + +func (r AppServiceEnvironmentV3Resource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "pricing_tier": { + Type: schema.TypeString, + Computed: true, + }, + + "location": { + Type: schema.TypeString, + Computed: true, + }, + } +} + +func (r AppServiceEnvironmentV3Resource) ModelObject() interface{} { + return AppServiceEnvironmentV3Model{} +} + +func (r AppServiceEnvironmentV3Resource) ResourceType() string { + return "azurerm_app_service_environment_v3" +} + +func (r AppServiceEnvironmentV3Resource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 6 * time.Hour, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Web.AppServiceEnvironmentsClient + networksClient := metadata.Client.Network.VnetClient + subscriptionId := metadata.Client.Account.SubscriptionId + + var model AppServiceEnvironmentV3Model + if err := metadata.Decode(&model); err != nil { + return fmt.Errorf("decoding %+v", err) + } + + subnet, err := networkParse.SubnetID(model.SubnetId) + if err != nil { + return err + } + + vnet, err := networksClient.Get(ctx, subnet.ResourceGroup, subnet.VirtualNetworkName, "") + if err != nil { + return fmt.Errorf("retrieving Virtual Network %q (Resource Group %q): %+v", subnet.VirtualNetworkName, subnet.ResourceGroup, err) + } + + vnetLoc := location.NormalizeNilable(vnet.Location) + if vnetLoc == "" { + return fmt.Errorf("determining Location from Virtual Network %q (Resource Group %q): `location` was missing", subnet.VirtualNetworkName, subnet.ResourceGroup) + } + + id := parse.NewAppServiceEnvironmentID(subscriptionId, model.ResourceGroup, model.Name) + existing, err := client.Get(ctx, id.ResourceGroup, id.HostingEnvironmentName) + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + } + if !utils.ResponseWasNotFound(existing.Response) { + return metadata.ResourceRequiresImport(r.ResourceType(), id) + } + + envelope := web.AppServiceEnvironmentResource{ + Kind: utils.String(KindASEV3), + Location: utils.String(vnetLoc), + AppServiceEnvironment: &web.AppServiceEnvironment{ + Name: utils.String(id.HostingEnvironmentName), + VirtualNetwork: &web.VirtualNetworkProfile{ + ID: utils.String(model.SubnetId), + Subnet: utils.String(subnet.Name), + }, + ClusterSettings: expandClusterSettingsModel(model.ClusterSetting), + }, + Tags: tags.Expand(model.Tags), + } + + if _, err = client.CreateOrUpdate(ctx, id.ResourceGroup, id.HostingEnvironmentName, envelope); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + createWait := resource.StateChangeConf{ + Pending: []string{ + string(web.ProvisioningStateInProgress), + }, + Target: []string{ + string(web.ProvisioningStateSucceeded), + }, + MinTimeout: 1 * time.Minute, + Refresh: appServiceEnvironmentRefresh(ctx, client, id.ResourceGroup, id.HostingEnvironmentName), + } + + timeout, _ := ctx.Deadline() + createWait.Timeout = time.Until(timeout) + + if _, err := createWait.WaitForState(); err != nil { + return fmt.Errorf("waiting for the creation of %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (r AppServiceEnvironmentV3Resource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Web.AppServiceEnvironmentsClient + id, err := parse.AppServiceEnvironmentID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + existing, err := client.Get(ctx, id.ResourceGroup, id.HostingEnvironmentName) + if err != nil { + if utils.ResponseWasNotFound(existing.Response) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", id, err) + } + + model := AppServiceEnvironmentV3Model{ + Name: id.HostingEnvironmentName, + ResourceGroup: id.ResourceGroup, + Location: location.NormalizeNilable(existing.Location), + } + + if props := existing.AppServiceEnvironment; props != nil { + if props.VirtualNetwork != nil { + model.SubnetId = utils.NormalizeNilableString(props.VirtualNetwork.ID) + } + + model.PricingTier = utils.NormalizeNilableString(props.MultiSize) + model.ClusterSetting = flattenClusterSettingsModel(props.ClusterSettings) + } + + model.Tags = tags.Flatten(existing.Tags) + + return metadata.Encode(&model) + }, + } +} + +func (r AppServiceEnvironmentV3Resource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 6 * time.Hour, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.Web.AppServiceEnvironmentsClient + + id, err := parse.AppServiceEnvironmentID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + future, err := client.Delete(ctx, id.ResourceGroup, id.HostingEnvironmentName, utils.Bool(false)) + if err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } + + if err := future.WaitForCompletionRef(ctx, client.Client); err != nil { + // This future can return a 404 for the polling check if the ASE is successfully deleted but this raises an error in the SDK + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("waiting for removal of %s: %+v", id, err) + } + } + + return nil + }, + } +} + +func (r AppServiceEnvironmentV3Resource) IDValidationFunc() schema.SchemaValidateFunc { + return validate.AppServiceEnvironmentID +} + +func (r AppServiceEnvironmentV3Resource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 6 * time.Hour, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + id, err := parse.AppServiceEnvironmentID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + metadata.Logger.Info("Decoding state...") + var state AppServiceEnvironmentV3Model + if err := metadata.Decode(&state); err != nil { + return err + } + + metadata.Logger.Infof("updating %s", id) + client := metadata.Client.Web.AppServiceEnvironmentsClient + + patch := web.AppServiceEnvironmentPatchResource{ + AppServiceEnvironment: &web.AppServiceEnvironment{}, + } + + if metadata.ResourceData.HasChange("cluster_setting") { + patch.AppServiceEnvironment.ClusterSettings = expandClusterSettingsModel(state.ClusterSetting) + } + + if _, err = client.Update(ctx, id.ResourceGroup, id.HostingEnvironmentName, patch); err != nil { + return fmt.Errorf("updating %s: %+v", id, err) + } + + return nil + }, + } +} + +func flattenClusterSettingsModel(input *[]web.NameValuePair) []ClusterSettingModel { + var output []ClusterSettingModel + if input == nil || len(*input) == 0 { + return output + } + + for _, v := range *input { + if v.Name == nil { + continue + } + + output = append(output, ClusterSettingModel{ + Name: *v.Name, + Value: utils.NormalizeNilableString(v.Value), + }) + } + return output +} + +func expandClusterSettingsModel(input []ClusterSettingModel) *[]web.NameValuePair { + var clusterSettings []web.NameValuePair + if input == nil { + return &clusterSettings + } + + for _, v := range input { + clusterSettings = append(clusterSettings, web.NameValuePair{ + Name: utils.String(v.Name), + Value: utils.String(v.Value), + }) + } + return &clusterSettings +} diff --git a/azurerm/internal/services/web/app_service_environment_v3_resource_test.go b/azurerm/internal/services/web/app_service_environment_v3_resource_test.go new file mode 100644 index 000000000000..cfa97a80a16c --- /dev/null +++ b/azurerm/internal/services/web/app_service_environment_v3_resource_test.go @@ -0,0 +1,237 @@ +package web_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/acceptance/check" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/web/parse" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +type AppServiceEnvironmentV3Resource struct{} + +func TestAccAppServiceEnvironmentV3_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment_v3", "test") + r := AppServiceEnvironmentV3Resource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("pricing_tier").Exists(), + check.That(data.ResourceName).Key("location").HasValue(data.Locations.Primary), + ), + }, + data.ImportStep(), + }) +} + +func TestAccAppServiceEnvironmentV3_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment_v3", "test") + r := AppServiceEnvironmentV3Resource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("pricing_tier").Exists(), + check.That(data.ResourceName).Key("location").HasValue(data.Locations.Primary), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func TestAccAppServiceEnvironmentV3_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_app_service_environment_v3", "test") + r := AppServiceEnvironmentV3Resource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("pricing_tier").Exists(), + check.That(data.ResourceName).Key("location").HasValue(data.Locations.Primary), + check.That(data.ResourceName).Key("cluster_setting.#").HasValue("2"), + ), + }, + data.ImportStep(), + { + Config: r.completeUpdate(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("pricing_tier").Exists(), + check.That(data.ResourceName).Key("location").HasValue(data.Locations.Primary), + check.That(data.ResourceName).Key("cluster_setting.#").HasValue("3"), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("pricing_tier").Exists(), + check.That(data.ResourceName).Key("location").HasValue(data.Locations.Primary), + check.That(data.ResourceName).Key("cluster_setting.#").HasValue("2"), + ), + }, + data.ImportStep(), + }) +} + +func (AppServiceEnvironmentV3Resource) Exists(ctx context.Context, client *clients.Client, state *terraform.InstanceState) (*bool, error) { + id, err := parse.AppServiceEnvironmentID(state.ID) + if err != nil { + return nil, err + } + + resp, err := client.Web.AppServiceEnvironmentsClient.Get(ctx, id.ResourceGroup, id.HostingEnvironmentName) + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return utils.Bool(false), nil + } + return nil, fmt.Errorf("retrieving App Service Environment V3 %q (Resource Group %q): %+v", id.HostingEnvironmentName, id.ResourceGroup, err) + } + + return utils.Bool(true), nil +} + +func (r AppServiceEnvironmentV3Resource) basic(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%s +resource "azurerm_app_service_environment_v3" "test" { + name = "acctest-ase-%d" + resource_group_name = azurerm_resource_group.test.name + subnet_id = azurerm_subnet.outbound.id +} +`, template, data.RandomInteger) +} + +func (r AppServiceEnvironmentV3Resource) complete(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%s +resource "azurerm_app_service_environment_v3" "test" { + name = "acctest-ase-%d" + resource_group_name = azurerm_resource_group.test2.name + subnet_id = azurerm_subnet.outbound.id + + cluster_setting { + name = "InternalEncryption" + value = "true" + } + + cluster_setting { + name = "DisableTls1.0" + value = "1" + } + + tags = { + accTest = "1" + env = "Test" + } +} +`, template, data.RandomInteger) +} + +func (r AppServiceEnvironmentV3Resource) completeUpdate(data acceptance.TestData) string { + template := r.template(data) + return fmt.Sprintf(` +%s +resource "azurerm_app_service_environment_v3" "test" { + name = "acctest-ase-%d" + resource_group_name = azurerm_resource_group.test2.name + subnet_id = azurerm_subnet.outbound.id + + cluster_setting { + name = "InternalEncryption" + value = "true" + } + + cluster_setting { + name = "DisableTls1.0" + value = "1" + } + + cluster_setting { + name = "FrontEndSSLCipherSuiteOrder" + value = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256" + } + + tags = { + accTest = "1" + env = "Test" + } +} +`, template, data.RandomInteger) +} + +func (r AppServiceEnvironmentV3Resource) requiresImport(data acceptance.TestData) string { + template := r.basic(data) + return fmt.Sprintf(` +%s + +resource "azurerm_app_service_environment_v3" "import" { + name = azurerm_app_service_environment_v3.test.name + resource_group_name = azurerm_app_service_environment_v3.test.resource_group_name + subnet_id = azurerm_app_service_environment_v3.test.subnet_id +} +`, template) +} + +func (r AppServiceEnvironmentV3Resource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-ase-%d" + location = "%s" +} + +resource "azurerm_resource_group" "test2" { + name = "acctestRG2-ase-%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" "inbound" { + name = "inbound" + 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" "outbound" { + name = "outbound" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.0.2.0/24" + delegation { + name = "asedelegation" + service_delegation { + name = "Microsoft.Web/hostingEnvironments" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.Locations.Primary, data.RandomInteger) +} diff --git a/azurerm/internal/services/web/registration.go b/azurerm/internal/services/web/registration.go index f03ca3eb0fd0..e3ae37c7e495 100644 --- a/azurerm/internal/services/web/registration.go +++ b/azurerm/internal/services/web/registration.go @@ -2,6 +2,7 @@ package web import ( "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/sdk" ) type Registration struct{} @@ -52,3 +53,20 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_function_app_slot": resourceFunctionAppSlot(), } } + +// PackagePath is the relative path to this package +func (r Registration) PackagePath() string { + return "TODO: do we need this?" +} + +func (r Registration) DataSources() []sdk.DataSource { + return []sdk.DataSource{ + AppServiceEnvironmentV3DataSource{}, + } +} + +func (r Registration) Resources() []sdk.Resource { + return []sdk.Resource{ + AppServiceEnvironmentV3Resource{}, + } +} diff --git a/azurerm/utils/int.go b/azurerm/utils/int.go new file mode 100644 index 000000000000..ed4779d3be14 --- /dev/null +++ b/azurerm/utils/int.go @@ -0,0 +1,21 @@ +package utils + +// NormaliseNilableInt takes a pointer to an int and returns a zero value or +// the real value if present +func NormaliseNilableInt(input *int) int { + if input == nil { + return 0 + } + + return *input +} + +// NormaliseNilableInt32 takes a pointer to an int32 and returns a zero value or +// the real value if present +func NormaliseNilableInt32(input *int32) int32 { + if input == nil { + return 0 + } + + return *input +} diff --git a/website/docs/d/app_service_environment_v3.html.markdown b/website/docs/d/app_service_environment_v3.html.markdown new file mode 100644 index 000000000000..75feddcdaae2 --- /dev/null +++ b/website/docs/d/app_service_environment_v3.html.markdown @@ -0,0 +1,58 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: Data Source: azurerm_app_service_environment_v3" +description: |- + Gets information about an existing 3rd Generation (v3) App Service Environment. +--- + +# Data Source: azurerm_app_service_environment_v3 + +Use this data source to access information about an existing 3rd Generation (v3) App Service Environment. + +## Example Usage + +```hcl +data "azurerm_app_service_environment_v3" "example" { + name = "example-ASE" + resource_group_name = "example-resource-group" +} + +output "id" { + value = data.azurerm_app_service_environment_v3.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name of this v3 App Service Environment. + +* `resource_group_name` - (Required) The name of the Resource Group where the v3 App Service Environment exists. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the v3 App Service Environment. + +* `cluster_setting` - A `cluster_setting` block as defined below. + +* `subnet_id` - The ID of the v3 App Service Environment Subnet. + +* `tags` - A mapping of tags assigned to the v3 App Service Environment. + +--- + +A `cluster_setting` block exports the following: + +* `name` - The name of the Cluster Setting. + +* `value` - The value for the Cluster Setting. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `read` - (Defaults to 5 minutes) Used when retrieving the 3rd Generation (v3) App Service Environment. \ No newline at end of file diff --git a/website/docs/r/app_service_environment_v3.html.markdown b/website/docs/r/app_service_environment_v3.html.markdown new file mode 100644 index 000000000000..830f9e05e943 --- /dev/null +++ b/website/docs/r/app_service_environment_v3.html.markdown @@ -0,0 +1,126 @@ +--- +subcategory: "App Service (Web Apps)" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_app_service_environment_v3" +description: |- + Manages a 3rd Generation (v3) App Service Environment. + +--- + +# azurerm_app_service_environment + +Manages a 3rd Generation (v3) App Service Environment. + +~> **NOTE:** App Service Environment V3 is currently in Preview. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "exampleRG1" + location = "West Europe" +} + +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" "inbound" { + name = "inbound" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.0.1.0/24"] +} + +resource "azurerm_subnet" "outbound" { + name = "outbound" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.0.2.0/24"] + + service_delegation { + name = "Microsoft.Web/hostingEnvironments" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } +} + +resource "azurerm_app_service_environment_v3" "example" { + name = "example-asev3" + resource_group_name = azurerm_resource_group.example.name + subnet_id = azurerm_subnet.outbound.id + + cluster_setting { + name = "DisableTls1.0" + value = "1" + } + + cluster_setting { + name = "InternalEncryption" + value = "true" + } + + cluster_setting { + name = "FrontEndSSLCipherSuiteOrder" + value = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384_P256,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384_P256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256_P256,TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA_P256,TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA_P256" + } + + tags = { + env = "production" + terraformed = "true" + } + +} + +``` + +## Argument Reference + +* `name` - (Required) The name of the App Service Environment. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the Resource Group where the App Service Environment exists. Defaults to the Resource Group of the Subnet (specified by `subnet_id`). + +* `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. + +~> **NOTE:** This is the "outbound" Subnet which is required to have a delegation to `Microsoft.Web/hostingEnvironments` as detailed in the example above. Additionally, an "inbound" subnet is required in the Virtual Network which must not have `enforce_private_link_endpoint_network_policies` enabled. + +* `cluster_setting` - (Optional) Zero or more `cluster_setting` blocks as defined below. + +* `tags` - (Optional) A mapping of tags to assign to the resource. + +--- + +A `cluster_setting` block supports the following: + +* `name` - (Required) The name of the Cluster Setting. + +* `value` - (Required) The value for the Cluster Setting. + +## Attribute Reference + +* `id` - The ID of the App Service Environment. + +* `location` - The location where the App Service Environment exists. + +* `pricing_tier` - Pricing tier for the front end instances. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/docs/configuration/resources.html#timeouts) for certain actions: + +* `create` - (Defaults to 6 hours) Used when creating the 3rd Generation (v3) App Service Environment. +* `update` - (Defaults to 6 hours) Used when updating the 3rd Generation (v3) App Service Environment. +* `read` - (Defaults to 5 minutes) Used when retrieving the 3rd Generation (v3) App Service Environment. +* `delete` - (Defaults to 6 hours) Used when deleting the 3rd Generation (v3) App Service Environment. + +## Import + +A 3rd Generation (v3) App Service Environment can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_app_service_environment.myAppServiceEnv /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myResourceGroup/providers/Microsoft.Web/hostingEnvironments/myAppServiceEnv +```