diff --git a/internal/services/servicenetworking/registration.go b/internal/services/servicenetworking/registration.go index 4af6218b5f072..52f9f267fee95 100644 --- a/internal/services/servicenetworking/registration.go +++ b/internal/services/servicenetworking/registration.go @@ -19,6 +19,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ ApplicationLoadBalancerResource{}, + FrontendsResource{}, } } diff --git a/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource.go b/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource.go new file mode 100644 index 0000000000000..664b4ab08a781 --- /dev/null +++ b/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource.go @@ -0,0 +1,219 @@ +package servicenetworking + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonschema" + "github.com/hashicorp/go-azure-helpers/resourcemanager/location" + "github.com/hashicorp/go-azure-helpers/resourcemanager/tags" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicenetworking/2023-05-01-preview/frontendsinterface" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azurerm/helpers/tf" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +type FrontendsResource struct{} + +type FrontendsModel struct { + Name string `tfschema:"name"` + ApplicationLoadBalancerId string `tfschema:"application_load_balancer_id"` + Location string `tfschema:"location"` + Fqdn string `tfschema:"fully_qualified_domain_name"` + Tags map[string]interface{} `tfschema:"tags"` +} + +var _ sdk.Resource = FrontendsResource{} + +func (f FrontendsResource) Arguments() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.StringIsNotEmpty, + }, + + "application_load_balancer_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: frontendsinterface.ValidateTrafficControllerID, + }, + + "location": commonschema.Location(), + + "tags": commonschema.Tags(), + } +} + +func (f FrontendsResource) Attributes() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "fully_qualified_domain_name": { + Type: pluginsdk.TypeString, + Computed: true, + }, + } +} + +func (f FrontendsResource) ModelObject() interface{} { + return &FrontendsModel{} +} + +func (f FrontendsResource) ResourceType() string { + return "azurerm_application_load_balancer_frontend" +} + +func (f FrontendsResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return frontendsinterface.ValidateFrontendID +} + +func (f FrontendsResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.FrontendsInterface + + var config FrontendsModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) + } + + trafficControllerId, err := frontendsinterface.ParseTrafficControllerID(config.ApplicationLoadBalancerId) + if err != nil { + return err + } + + id := frontendsinterface.NewFrontendID(trafficControllerId.SubscriptionId, trafficControllerId.ResourceGroupName, trafficControllerId.TrafficControllerName, config.Name) + + resp, err := client.Get(ctx, id) + if err != nil { + if !response.WasNotFound(resp.HttpResponse) { + return fmt.Errorf("checking for presence of existing %s: %+v", id, err) + } + } + + if !response.WasNotFound(resp.HttpResponse) { + return tf.ImportAsExistsError(f.ResourceType(), id.ID()) + } + + frontend := frontendsinterface.Frontend{ + Location: location.Normalize(config.Location), + Properties: &frontendsinterface.FrontendProperties{}, + Tags: tags.Expand(config.Tags), + } + + if err := client.CreateOrUpdateThenPoll(ctx, id, frontend); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (f FrontendsResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.FrontendsInterface + + id, err := frontendsinterface.ParseFrontendID(metadata.ResourceData.Id()) + if err != nil { + return fmt.Errorf("parsing %s: %+v", metadata.ResourceData.Id(), err) + } + + resp, err := client.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return metadata.MarkAsGone(id) + } + return fmt.Errorf("retrieving %s: %+v", metadata.ResourceData.Id(), err) + } + + trafficControllerId := frontendsinterface.NewTrafficControllerID(id.SubscriptionId, id.ResourceGroupName, id.TrafficControllerName) + + state := FrontendsModel{ + Name: id.FrontendName, + ApplicationLoadBalancerId: trafficControllerId.ID(), + } + + if model := resp.Model; model != nil { + state.Location = location.NormalizeNilable(pointer.To(model.Location)) + state.Tags = tags.Flatten(model.Tags) + + if prop := model.Properties; prop != nil { + state.Fqdn = pointer.From(prop.Fqdn) + } + } + + return metadata.Encode(&state) + }, + } +} + +func (f FrontendsResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.FrontendsInterface + + id, err := frontendsinterface.ParseFrontendID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + var config FrontendsModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) + } + + resp, err := client.Get(ctx, *id) + if err != nil { + return fmt.Errorf("retiring %s: %+v", *id, err) + } + + if resp.Model == nil { + return fmt.Errorf("retiring %s: Model was nil", *id) + } + + model := *resp.Model + + if metadata.ResourceData.HasChange("tags") { + model.Tags = tags.Expand(config.Tags) + } + + if err := client.CreateOrUpdateThenPoll(ctx, *id, model); err != nil { + return fmt.Errorf("updating `azurerm_alb_frontend` %s: %+v", *id, err) + } + + return nil + }, + } +} + +func (f FrontendsResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.FrontendsInterface + + id, err := frontendsinterface.ParseFrontendID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + if err = client.DeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %q: %+v", id.ID(), err) + } + + return nil + }, + } +} diff --git a/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource_test.go b/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource_test.go new file mode 100644 index 0000000000000..4fa98be278795 --- /dev/null +++ b/internal/services/servicenetworking/service_networking_application_load_balancer_frontend_resource_test.go @@ -0,0 +1,169 @@ +package servicenetworking_test + +import ( + "context" + "fmt" + "testing" + + "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicenetworking/2023-05-01-preview/frontendsinterface" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance" + "github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azurerm/internal/clients" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type ApplicationLoadBalancerFrontendResource struct{} + +func (r ApplicationLoadBalancerFrontendResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := frontendsinterface.ParseFrontendID(state.ID) + if err != nil { + return nil, fmt.Errorf("while parsing resource ID: %+v", err) + } + + resp, err := clients.ServiceNetworking.FrontendsInterface.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil + } + return nil, fmt.Errorf("while checking existence for %q: %+v", id.String(), err) + } + return pointer.To(resp.Model != nil), nil +} + +func TestAccApplicationLoadBalancerFrontend_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_frontend", "test") + + r := ApplicationLoadBalancerFrontendResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("fully_qualified_domain_name").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerFrontend_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_frontend", "test") + + r := ApplicationLoadBalancerFrontendResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("fully_qualified_domain_name").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerFrontend_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_frontend", "test") + + r := ApplicationLoadBalancerFrontendResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("fully_qualified_domain_name").Exists(), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("fully_qualified_domain_name").Exists(), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerFrontend_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_frontend", "test") + + r := ApplicationLoadBalancerFrontendResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.RequiresImportErrorStep(r.requiresImport), + }) +} + +func (r ApplicationLoadBalancerFrontendResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azurerm_resource_group" "test" { + name = "acctestrg-alb-%[1]d" + location = "%[2]s" +} + +resource "azurerm_application_load_balancer" "test" { + name = "acctestalb-%[1]d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} +`, data.RandomInteger, data.Locations.Primary) +} + +func (r ApplicationLoadBalancerFrontendResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + } +} + + %s + +resource "azurerm_application_load_balancer_frontend" "test" { + name = "acct-frnt-%d" + application_load_balancer_id = azurerm_application_load_balancer.test.id + location = azurerm_application_load_balancer.test.location +} +`, r.template(data), data.RandomInteger) +} + +func (r ApplicationLoadBalancerFrontendResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + } +} + + %s + +resource "azurerm_application_load_balancer_frontend" "test" { + name = "acct-frnt-%d" + application_load_balancer_id = azurerm_application_load_balancer.test.id + location = azurerm_application_load_balancer.test.location + tags = { + "tag1" = "value1" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r ApplicationLoadBalancerFrontendResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` + %s + +resource "azurerm_application_load_balancer_frontend" "import" { + name = azurerm_application_load_balancer_frontend.test.name + application_load_balancer_id = azurerm_application_load_balancer_frontend.test.application_load_balancer_id + location = azurerm_application_load_balancer_frontend.test.location +} + +`, r.basic(data)) +} diff --git a/internal/services/servicenetworking/application_load_balancer_resource.go b/internal/services/servicenetworking/service_networking_application_load_balancer_resource.go similarity index 83% rename from internal/services/servicenetworking/application_load_balancer_resource.go rename to internal/services/servicenetworking/service_networking_application_load_balancer_resource.go index f90aec3770ab3..5b1516a869df4 100644 --- a/internal/services/servicenetworking/application_load_balancer_resource.go +++ b/internal/services/servicenetworking/service_networking_application_load_balancer_resource.go @@ -3,6 +3,7 @@ package servicenetworking import ( "context" "fmt" + "net/http" "regexp" "time" @@ -186,8 +187,39 @@ func (t ApplicationLoadBalancerResource) Delete() sdk.ResourceFunc { if err != nil { return err } - if err := client.DeleteThenPoll(ctx, *id); err != nil { - return fmt.Errorf("deleting %s: %+v", *id, err) + + // a workaround for that some child resources may still exist for seconds before it fully deleted. + // tracked o https://github.com/Azure/azure-rest-api-specs/issues/26000n + // it will cause the error "Can not delete resource before nested resources are deleted." + deadline, ok := ctx.Deadline() + if !ok { + return fmt.Errorf("could not retrieve context deadline for %s", id.ID()) + } + stateConf := &pluginsdk.StateChangeConf{ + Delay: 5 * time.Minute, + Pending: []string{"409"}, + Target: []string{"200", "202"}, + Refresh: func() (result interface{}, state string, err error) { + resp, err := client.Delete(ctx, *id) + if err != nil { + if resp.HttpResponse.StatusCode == http.StatusConflict { + return nil, "409", nil + } + return nil, "", err + } + return resp, "200", nil + }, + MinTimeout: 15 * time.Second, + Timeout: time.Until(deadline), + } + + if future, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("waiting for deleting of %s: %+v", id, err) + } else { + poller := future.(trafficcontrollerinterface.DeleteOperationResponse).Poller + if err := poller.PollUntilDone(ctx); err != nil { + return fmt.Errorf("deleting %s: %+v", id, err) + } } return nil diff --git a/internal/services/servicenetworking/application_load_balancer_resource_test.go b/internal/services/servicenetworking/service_networking_application_load_balancer_resource_test.go similarity index 90% rename from internal/services/servicenetworking/application_load_balancer_resource_test.go rename to internal/services/servicenetworking/service_networking_application_load_balancer_resource_test.go index 3391fb8350354..60aa9dcf1fed2 100644 --- a/internal/services/servicenetworking/application_load_balancer_resource_test.go +++ b/internal/services/servicenetworking/service_networking_application_load_balancer_resource_test.go @@ -41,7 +41,6 @@ func TestAccApplicationLoadBalancer_basic(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("configuration_endpoint.#").HasValue("1"), ), }, data.ImportStep(), @@ -57,7 +56,6 @@ func TestAccApplicationLoadBalancer_complete(t *testing.T) { Config: r.complete(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("primary_configuration_endpoint").HasValue("1"), ), }, data.ImportStep(), @@ -73,7 +71,6 @@ func TestAccApplicationLoadBalancer_update(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("configuration_endpoint.#").HasValue("1"), ), }, data.ImportStep(), @@ -81,7 +78,6 @@ func TestAccApplicationLoadBalancer_update(t *testing.T) { Config: r.complete(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("configuration_endpoint.#").HasValue("1"), ), }, data.ImportStep(), @@ -97,7 +93,6 @@ func TestAccApplicationLoadBalancer_requiresImport(t *testing.T) { Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("configuration_endpoint.#").HasValue("1"), ), }, data.RequiresImportErrorStep(r.requiresImport), @@ -152,11 +147,6 @@ resource "azurerm_application_load_balancer" "test" { func (r ApplicationLoadBalancerResource) requiresImport(data acceptance.TestData) string { return fmt.Sprintf(` -provider "azurerm" { - features { - } -} - %s resource "azurerm_application_load_balancer" "import" { diff --git a/website/docs/r/service_networking_application_load_balancer.html.markdown b/website/docs/r/service_networking_application_load_balancer.html.markdown new file mode 100644 index 0000000000000..eaa4e35e51890 --- /dev/null +++ b/website/docs/r/service_networking_application_load_balancer.html.markdown @@ -0,0 +1,62 @@ +--- +subcategory: "Service Networking" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_application_load_balancer" +description: |- + Manages an Application Gateway for Containers (ALB). +--- + +# azurerm_application_load_balancer + +Manages an Application Gateway for Containers (ALB). + +## Example Usage + +```hcl +resource "azurerm_application_load_balancer" "example" { + name = "example" + resource_group_name = "example" + location = "West Europe" +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this resource. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the Resource Group where the resource should exist. Changing this forces a new resource to be created. + +* `location` - (Required) The Azure Region where the resource should exist. Changing this forces a new resource to be created. Available regions can be found [here](https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/overview#supported-regions) + +**Note:** The available values of `location` are `northeurope` and `north central us`. + +--- + +* `tags` - (Optional) A mapping of tags which should be assigned to the Application Gateway for Containers. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the resource. + +* `configuration_endpoint` - The list of configuration endpoints for the resource. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Application Gateway for Containers resource +* `read` - (Defaults to 5 minutes) Used when retrieving the Application Gateway for Containers resource. +* `update` - (Defaults to 30 minutes) Used when updating the Application Gateway for Containers resource +* `delete` - (Defaults to 30 minutes) Used when deleting the Application Gateway for Containers resource. + +## Import + +Application Gateway for Containers (ALB) can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_application_load_balancer.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.ServiceNetworking/trafficControllers/alb1 +``` diff --git a/website/docs/r/service_networking_application_load_balancer_frontend.html.markdown b/website/docs/r/service_networking_application_load_balancer_frontend.html.markdown new file mode 100644 index 0000000000000..696012d636ba1 --- /dev/null +++ b/website/docs/r/service_networking_application_load_balancer_frontend.html.markdown @@ -0,0 +1,66 @@ +--- +subcategory: "Service Networking" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_application_load_balancer_frontend" +description: |- + Manages an Application Gateway for Containers Frontend. +--- + +# azurerm_application_load_balancer_frontend + +Manages an Application Gateway for Containers Frontend. + +## Example Usage + +```hcl +resource "azurerm_application_load_balancer" "example" { + name = "example" + resource_group_name = "example" + location = "West Europe" +} + +resource "azurerm_application_load_balancer_frontend" "example" { + name = "example" + location = "West Europe" + application_load_balancer_id = azurerm_application_load_balancer.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this Application Gateway for Containers Frontend. Changing this forces a new resource to be created. + +* `location` - (Required) The Azure Region where the Application Gateway for Containers Frontend should exist. Changing this forces a new resource to be created. Available regions can be found [here](https://learn.microsoft.com/en-us/azure/application-gateway/for-containers/overview#supported-regions). + +* `application_load_balancer_id` - (Required) The ID of the Application Gateway for Containers. Changing this forces a new resource to be created. + +--- + +* `tags` - (Optional) A mapping of tags which should be assigned to the Application Gateway for Containers Frontend. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Application Gateway for Containers Frontend. + +* `fully_qualified_domain_name` - The Fully Qualified Domain Name of the DNS record associated to an Application Gateway for Containers Frontend. + +## Timeouts + +The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/language/resources/syntax#operation-timeouts) for certain actions: + +* `create` - (Defaults to 30 minutes) Used when creating the Application Gateway for Containers Frontend. +* `read` - (Defaults to 5 minutes) Used when retrieving the Application Gateway for Containers Frontend. +* `update` - (Defaults to 30 minutes) Used when updating the Application Gateway for Containers Frontend. +* `delete` - (Defaults to 30 minutes) Used when deleting the Application Gateway for Containers Frontend. + +## Import + +Application Gateway for Containers Frontend can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_application_load_balancer_frontend.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.ServiceNetworking/trafficControllers/alb1/frontends/frontend1 +```