diff --git a/internal/services/servicenetworking/application_load_balancer_resource_test.go b/internal/services/servicenetworking/application_load_balancer_resource_test.go index 683683f2a44a..78ac1e6f8b04 100644 --- a/internal/services/servicenetworking/application_load_balancer_resource_test.go +++ b/internal/services/servicenetworking/application_load_balancer_resource_test.go @@ -153,11 +153,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/internal/services/servicenetworking/application_load_balancer_subnet_association_resource.go b/internal/services/servicenetworking/application_load_balancer_subnet_association_resource.go new file mode 100644 index 000000000000..574c39d718eb --- /dev/null +++ b/internal/services/servicenetworking/application_load_balancer_subnet_association_resource.go @@ -0,0 +1,216 @@ +package servicenetworking + +import ( + "context" + "fmt" + "time" + + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-helpers/resourcemanager/commonids" + "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/associationsinterface" + "github.com/hashicorp/go-azure-sdk/resource-manager/servicenetworking/2023-05-01-preview/trafficcontrollerinterface" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/servicenetworking/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" +) + +type ApplicationLoadBalancerSubnetAssociationResource struct{} + +type AssociationResourceModel struct { + Name string `tfschema:"name"` + ApplicationLoadBalancerId string `tfschema:"application_load_balancer_id"` + SubnetId string `tfschema:"subnet_id"` + Tags map[string]interface{} `tfschema:"tags"` +} + +var _ sdk.ResourceWithUpdate = ApplicationLoadBalancerSubnetAssociationResource{} + +func (t ApplicationLoadBalancerSubnetAssociationResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "name": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ApplicationLoadBalancerSubnetAssociationName(), + }, + + "application_load_balancer_id": commonschema.ResourceIDReferenceRequiredForceNew(associationsinterface.TrafficControllerId{}), + + "subnet_id": commonschema.ResourceIDReferenceRequired(commonids.SubnetId{}), + + "tags": commonschema.Tags(), + } +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) ModelObject() interface{} { + return &AssociationResourceModel{} +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) ResourceType() string { + return "azurerm_application_load_balancer_subnet_association" +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return associationsinterface.ValidateAssociationID +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) 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.AssociationsInterface + + var config AssociationResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) + } + + albId, err := trafficcontrollerinterface.ParseTrafficControllerID(config.ApplicationLoadBalancerId) + if err != nil { + return err + } + + id := associationsinterface.NewAssociationID(albId.SubscriptionId, albId.ResourceGroupName, albId.TrafficControllerName, config.Name) + existing, err := client.Get(ctx, id) + if err != nil && !response.WasNotFound(existing.HttpResponse) { + return fmt.Errorf("checking for presence of exisiting %s: %+v", id, err) + } + + if !response.WasNotFound(existing.HttpResponse) { + return metadata.ResourceRequiresImport(t.ResourceType(), id) + } + + controller, err := trafficControllerClient.Get(ctx, *albId) + if err != nil { + return fmt.Errorf("retrieving parent %s: %+v", *albId, err) + } + + if controller.Model == nil { + return fmt.Errorf("retrieving parent %s: model was nil", *albId) + } + + association := associationsinterface.Association{ + Location: location.Normalize(controller.Model.Location), + Properties: &associationsinterface.AssociationProperties{ + Subnet: &associationsinterface.AssociationSubnet{ + Id: config.SubnetId, + }, + AssociationType: associationsinterface.AssociationTypeSubnets, + }, + Tags: tags.Expand(config.Tags), + } + + if err := client.CreateOrUpdateThenPoll(ctx, id, association); err != nil { + return fmt.Errorf("creating %s: %+v", id, err) + } + + metadata.SetID(id) + return nil + }, + } +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 5 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.AssociationsInterface + + id, err := associationsinterface.ParseAssociationID(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("retreiving %s: %v", *id, err) + } + + trafficControllerId := associationsinterface.NewTrafficControllerID(id.SubscriptionId, id.ResourceGroupName, id.TrafficControllerName) + state := AssociationResourceModel{ + Name: id.AssociationName, + ApplicationLoadBalancerId: trafficControllerId.ID(), + } + + if model := resp.Model; model != nil { + state.Tags = tags.Flatten(model.Tags) + + if prop := model.Properties; prop != nil { + if prop.Subnet != nil { + parsedSubnetId, err := commonids.ParseSubnetIDInsensitively(prop.Subnet.Id) + if err != nil { + return err + } + state.SubnetId = parsedSubnetId.ID() + } + } + } + + return metadata.Encode(&state) + }, + } +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) Update() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.AssociationsInterface + + var config AssociationResourceModel + if err := metadata.Decode(&config); err != nil { + return fmt.Errorf("decoding %v", err) + } + + id, err := associationsinterface.ParseAssociationID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + // Although `AssociationSubnetUpdate` defined in the SDK contains the `subnetId`, while per testing it can not be updated + // Tracked on https://github.com/Azure/azure-rest-api-specs/issues/26657 + associationUpdate := associationsinterface.AssociationUpdate{} + + if metadata.ResourceData.HasChange("tags") { + associationUpdate.Tags = tags.Expand(config.Tags) + } + + if _, err = client.Update(ctx, *id, associationUpdate); err != nil { + return fmt.Errorf("updating %s: %v", *id, err) + } + + return nil + }, + } +} + +func (t ApplicationLoadBalancerSubnetAssociationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Timeout: 30 * time.Minute, + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + client := metadata.Client.ServiceNetworking.AssociationsInterface + + id, err := associationsinterface.ParseAssociationID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + if err = client.DeleteThenPoll(ctx, *id); err != nil { + return fmt.Errorf("deleting %s: %v", *id, err) + } + + return nil + }, + } +} diff --git a/internal/services/servicenetworking/application_load_balancer_subnet_association_resource_test.go b/internal/services/servicenetworking/application_load_balancer_subnet_association_resource_test.go new file mode 100644 index 000000000000..450e00695a95 --- /dev/null +++ b/internal/services/servicenetworking/application_load_balancer_subnet_association_resource_test.go @@ -0,0 +1,195 @@ +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/associationsinterface" + "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 SubnetAssociationResource struct{} + +func (r SubnetAssociationResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + id, err := associationsinterface.ParseAssociationID(state.ID) + if err != nil { + return nil, fmt.Errorf("while parsing resource ID: %+v", err) + } + + resp, err := clients.ServiceNetworking.AssociationsInterface.Get(ctx, *id) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil + } + return nil, fmt.Errorf("while checking existence of %s: %+v", *id, err) + } + return pointer.To(resp.Model != nil), nil +} + +func TestAccApplicationLoadBalancerSubnetAssociation_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_subnet_association", "test") + + r := SubnetAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerSubnetAssociation_update(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_subnet_association", "test") + + r := SubnetAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerSubnetAssociation_complete(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_subnet_association", "test") + + r := SubnetAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + { + Config: r.complete(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplicationLoadBalancerSubnetAssociation_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_application_load_balancer_subnet_association", "test") + + r := SubnetAssociationResource{} + 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 SubnetAssociationResource) 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 +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvnet%[1]d" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_subnet" "test" { + name = "acctestsubnet%[1]d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.ServiceNetworking/trafficControllers" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +} + +`, data.RandomInteger, data.Locations.Primary) +} + +func (r SubnetAssociationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + } +} + +%s + +resource "azurerm_application_load_balancer_subnet_association" "test" { + name = "acct-%d" + application_load_balancer_id = azurerm_application_load_balancer.test.id + subnet_id = azurerm_subnet.test.id +} +`, r.template(data), data.RandomInteger) +} + +func (r SubnetAssociationResource) complete(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azurerm" { + features { + } +} + +%s + +resource "azurerm_application_load_balancer_subnet_association" "test" { + name = "acct-%d" + application_load_balancer_id = azurerm_application_load_balancer.test.id + subnet_id = azurerm_subnet.test.id + tags = { + key = "value" + } +} +`, r.template(data), data.RandomInteger) +} + +func (r SubnetAssociationResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` + %s + +resource "azurerm_application_load_balancer_subnet_association" "import" { + name = azurerm_application_load_balancer_subnet_association.test.name + application_load_balancer_id = azurerm_application_load_balancer_subnet_association.test.application_load_balancer_id + subnet_id = azurerm_application_load_balancer_subnet_association.test.subnet_id +} +`, r.basic(data)) +} diff --git a/internal/services/servicenetworking/registration.go b/internal/services/servicenetworking/registration.go index 52f9f267fee9..422104402b06 100644 --- a/internal/services/servicenetworking/registration.go +++ b/internal/services/servicenetworking/registration.go @@ -20,6 +20,7 @@ func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ ApplicationLoadBalancerResource{}, FrontendsResource{}, + ApplicationLoadBalancerSubnetAssociationResource{}, } } diff --git a/internal/services/servicenetworking/validate/application_load_balancer_subnet_association_name.go b/internal/services/servicenetworking/validate/application_load_balancer_subnet_association_name.go new file mode 100644 index 000000000000..f7de55c5e23e --- /dev/null +++ b/internal/services/servicenetworking/validate/application_load_balancer_subnet_association_name.go @@ -0,0 +1,17 @@ +package validate + +import ( + "regexp" + + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/validation" +) + +func ApplicationLoadBalancerSubnetAssociationName() pluginsdk.SchemaValidateFunc { + return validation.All( + validation.StringLenBetween(1, 64), + validation.StringMatch(regexp.MustCompile(`^[a-zA-Z0-9]`), "the name must begin with a letter or number."), + validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9]$`), "the name must end with a letter or number."), + validation.StringMatch(regexp.MustCompile(`[a-zA-Z0-9_.-]{0,64}`), "the name may contain only letters, numbers, underscores, periods, or hyphens."), + ) +} diff --git a/website/docs/r/application_load_balancer_subnet_association.html.markdown b/website/docs/r/application_load_balancer_subnet_association.html.markdown new file mode 100644 index 000000000000..d055d8f38945 --- /dev/null +++ b/website/docs/r/application_load_balancer_subnet_association.html.markdown @@ -0,0 +1,94 @@ +--- +subcategory: "Service Networking" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_application_load_balancer_subnet_association" +description: |- + Manages an association between an Application Gateway for Containers and a Subnet. +--- + +# azurerm_application_load_balancer_subnet_association + +Manages an association between an Application Gateway for Containers and a Subnet. + +## Example Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "example-rg" + location = "westeurope" +} + +resource "azurerm_application_load_balancer" "example" { + name = "example-alb" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_virtual_network" "example" { + name = "example-vnet" + address_space = ["10.0.0.0/16"] + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_subnet" "example" { + name = "example-subnet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.0.1.0/24"] + + delegation { + name = "delegation" + + service_delegation { + name = "Microsoft.ServiceNetworking/trafficControllers" + actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"] + } + } +} + +resource "azurerm_application_load_balancer_subnet_association" "example" { + name = "example" + application_load_balancer_id = azurerm_application_load_balancer.example.id + subnet_id = azurerm_subnet.example.id +} +``` + +## Arguments Reference + +The following arguments are supported: + +* `name` - (Required) The name which should be used for this Application Gateway for Containers Association. Changing this forces a new resource to be created. + +* `application_load_balancer_id` - (Required) The ID of the Application Gateway for Containers. Changing this forces a new resource to be created. + +* `subnet_id` - (Required) The ID of the subnet which the Application Gateway for Containers associated to. Changing this forces a new resource to be created. + +**Note:** The subnet to be used must have a delegation for `Microsoft.ServiceNetworking/trafficControllers` as shown in the example above. + +--- + +* `tags` - (Optional) A mapping of tags which should be assigned to the Application Gateway for Containers Association. + +## Attributes Reference + +In addition to the Arguments listed above - the following Attributes are exported: + +* `id` - The ID of the Application Gateway for Containers Association. + +## 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 Association. +* `read` - (Defaults to 5 minutes) Used when retrieving the Application Gateway for Containers Association. +* `update` - (Defaults to 30 minutes) Used when updating the Application Gateway for Containers Association. +* `delete` - (Defaults to 30 minutes) Used when deleting the Application Gateway for Containers Association. + +## Import + +Application Gateway for Containers Associations can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_application_load_balancer_subnet_association.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg1/providers/Microsoft.ServiceNetworking/trafficControllers/alb1/associations/association1 +```