diff --git a/internal/services/servicenetworking/application_load_balancer_frontend_resource.go b/internal/services/servicenetworking/application_load_balancer_frontend_resource.go new file mode 100644 index 000000000000..620278951795 --- /dev/null +++ b/internal/services/servicenetworking/application_load_balancer_frontend_resource.go @@ -0,0 +1,216 @@ +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/tags" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicenetworking/2023-05-01-preview/frontendsinterface" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicenetworking/2023-05-01-preview/trafficcontrollerinterface" + "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"` + 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, + }, + + "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 { + trafficControllerClient := metadata.Client.ServiceNetworking.TrafficControllerInterface + 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 + } + + controllerId := trafficcontrollerinterface.NewTrafficControllerID(trafficControllerId.SubscriptionId, trafficControllerId.ResourceGroupName, trafficControllerId.TrafficControllerName) + controller, err := trafficControllerClient.Get(ctx, controllerId) + if err != nil { + return fmt.Errorf("retrieving %s: %+v", controllerId, err) + } + + if controller.Model == nil { + return fmt.Errorf("retrieving %s: Model was nil", controllerId) + } + + loc := controller.Model.Location + + 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: loc, + 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 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.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) + } + + update := frontendsinterface.FrontendUpdate{} + + if metadata.ResourceData.HasChange("tags") { + update.Tags = tags.Expand(config.Tags) + } + if _, err := client.Update(ctx, *id, update); err != nil { + return fmt.Errorf("updating `azurerm_application_load_balancer_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/application_load_balancer_frontend_resource_test.go b/internal/services/servicenetworking/application_load_balancer_frontend_resource_test.go new file mode 100644 index 000000000000..7af3395d7903 --- /dev/null +++ b/internal/services/servicenetworking/application_load_balancer_frontend_resource_test.go @@ -0,0 +1,165 @@ +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 +} +`, 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 + 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 +} +`, r.basic(data)) +} diff --git a/internal/services/servicenetworking/application_load_balancer_resource.go b/internal/services/servicenetworking/application_load_balancer_resource.go index f90aec3770ab..0e01479f90d1 100644 --- a/internal/services/servicenetworking/application_load_balancer_resource.go +++ b/internal/services/servicenetworking/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/26000 + // 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/registration.go b/internal/services/servicenetworking/registration.go index 4af6218b5f07..52f9f267fee9 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/website/docs/r/application_load_balancer_frontend.html.markdown b/website/docs/r/application_load_balancer_frontend.html.markdown new file mode 100644 index 000000000000..696012d636ba --- /dev/null +++ b/website/docs/r/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 +```