From c3e143f97321d58bcfcc0263a80a5485af9fcb6d Mon Sep 17 00:00:00 2001 From: Yun Liu Date: Mon, 9 Jan 2023 16:42:24 -0800 Subject: [PATCH] New Resource: `azurerm_private_endpoint_application_security_group_association` (#19825) Co-authored-by: kt Fixes https://github.com/hashicorp/terraform-provider-azurerm/issues/17665 --- ...plication_security_group_association_id.go | 74 ++++ ...tion_security_group_association_id_test.go | 82 +++++ ..._application_security_group_association.go | 317 ++++++++++++++++++ ...ication_security_group_association_test.go | 297 ++++++++++++++++ internal/services/network/registration.go | 1 + ...n_security_group_association.html.markdown | 145 ++++++++ 6 files changed, 916 insertions(+) create mode 100644 internal/services/network/parse/private_endpoint_application_security_group_association_id.go create mode 100644 internal/services/network/parse/private_endpoint_application_security_group_association_id_test.go create mode 100644 internal/services/network/private_endpoint_application_security_group_association.go create mode 100644 internal/services/network/private_endpoint_application_security_group_association_test.go create mode 100644 website/docs/r/private_endpoint_application_security_group_association.html.markdown diff --git a/internal/services/network/parse/private_endpoint_application_security_group_association_id.go b/internal/services/network/parse/private_endpoint_application_security_group_association_id.go new file mode 100644 index 000000000000..7b6eb301d6f5 --- /dev/null +++ b/internal/services/network/parse/private_endpoint_application_security_group_association_id.go @@ -0,0 +1,74 @@ +package parse + +import ( + "fmt" + "strings" + + "github.com/hashicorp/go-azure-helpers/resourcemanager/resourceids" +) + +var _ resourceids.Id = PrivateEndpointApplicationSecurityGroupAssociationId{} + +type PrivateEndpointApplicationSecurityGroupAssociationId struct { + PrivateEndpointId PrivateEndpointId + ApplicationSecurityGroupId ApplicationSecurityGroupId +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationId) ID() string { + return fmt.Sprintf("%s|%s", p.PrivateEndpointId.ID(), p.ApplicationSecurityGroupId.ID()) +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationId) String() string { + components := []string{ + fmt.Sprintf("PrivateEndpointId %s", p.PrivateEndpointId.ID()), + fmt.Sprintf("ApplicationSecurityGroupId %s", p.ApplicationSecurityGroupId.ID()), + } + return fmt.Sprintf("Private Endpoint Application Security Group Association: %s", strings.Join(components, " / ")) +} + +func NewPrivateEndpointApplicationSecurityGroupAssociationId(endpointId PrivateEndpointId, securityGroupId ApplicationSecurityGroupId) PrivateEndpointApplicationSecurityGroupAssociationId { + return PrivateEndpointApplicationSecurityGroupAssociationId{ + PrivateEndpointId: endpointId, + ApplicationSecurityGroupId: securityGroupId, + } +} + +func PrivateEndpointApplicationSecurityGroupAssociationID(input string) (PrivateEndpointApplicationSecurityGroupAssociationId, error) { + splitId := strings.Split(input, "|") + if len(splitId) != 2 { + return PrivateEndpointApplicationSecurityGroupAssociationId{}, fmt.Errorf("expected ID to be in the format {PrivateEndpointId}|{ApplicationSecurityGroupId} but got %q", input) + } + + endpointId, err := PrivateEndpointID(splitId[0]) + if err != nil { + return PrivateEndpointApplicationSecurityGroupAssociationId{}, err + } + + securityGroupId, err := ApplicationSecurityGroupID(splitId[1]) + if err != nil { + return PrivateEndpointApplicationSecurityGroupAssociationId{}, err + } + + if endpointId == nil || securityGroupId == nil { + return PrivateEndpointApplicationSecurityGroupAssociationId{}, fmt.Errorf("parse error, both PrivateEndpointId and ApplicationSecurityGroupId should not be nil") + } + + return PrivateEndpointApplicationSecurityGroupAssociationId{ + PrivateEndpointId: *endpointId, + ApplicationSecurityGroupId: *securityGroupId, + }, nil +} + +func PrivateEndpointApplicationSecurityGroupAssociationIDValidation(input interface{}, key string) (warnings []string, errors []error) { + v, ok := input.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected %q to be a string", key)) + return + } + + if _, err := PrivateEndpointApplicationSecurityGroupAssociationID(v); err != nil { + errors = append(errors, err) + } + + return +} diff --git a/internal/services/network/parse/private_endpoint_application_security_group_association_id_test.go b/internal/services/network/parse/private_endpoint_application_security_group_association_id_test.go new file mode 100644 index 000000000000..20d3a5b322ba --- /dev/null +++ b/internal/services/network/parse/private_endpoint_application_security_group_association_id_test.go @@ -0,0 +1,82 @@ +package parse + +import ( + "testing" +) + +func TestPrivateEndpointApplicationSecurityGroupAssociationID(t *testing.T) { + testData := []struct { + Name string + Input string + Expect *PrivateEndpointApplicationSecurityGroupAssociationId + Error bool + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "One Segment", + Input: "hello", + Error: true, + }, + { + Name: "Two Segments Invalid ID's", + Input: "hello|world", + Error: true, + }, + { + Name: "Missing ASG Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/privateEndpoints/endpoints1", + Error: true, + }, + { + Name: "Private Endpoint Id", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/privateEndpoints/endpoints1", + Error: true, + }, + { + Name: "Application Security Group ID", + Input: "/subscriptions/12345678-1234-9876-4563-123456789012/resourceGroups/group1/providers/Microsoft.Network/applicationSecurityGroups/securityGroup1", + Error: true, + }, + { + Name: "Nat Gateway / Public IP Association ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/privateEndpoints/endpoints1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/applicationSecurityGroups/securityGroup1", + Error: false, + Expect: &PrivateEndpointApplicationSecurityGroupAssociationId{ + ApplicationSecurityGroupId: ApplicationSecurityGroupId{ + ResourceGroup: "mygroup1", + SubscriptionId: "00000000-0000-0000-0000-000000000000", + Name: "securityGroup1", + }, + PrivateEndpointId: PrivateEndpointId{ + ResourceGroup: "group1", + SubscriptionId: "00000000-0000-0000-0000-000000000000", + Name: "endpoints1", + }, + }, + }, + } + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := PrivateEndpointApplicationSecurityGroupAssociationID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.PrivateEndpointId.Name != v.Expect.PrivateEndpointId.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.PrivateEndpointId.Name, actual.PrivateEndpointId.Name) + } + + if actual.ApplicationSecurityGroupId.ResourceGroup != v.Expect.ApplicationSecurityGroupId.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.ApplicationSecurityGroupId.ResourceGroup, actual.ApplicationSecurityGroupId.ResourceGroup) + } + } +} diff --git a/internal/services/network/private_endpoint_application_security_group_association.go b/internal/services/network/private_endpoint_application_security_group_association.go new file mode 100644 index 000000000000..20390d908a14 --- /dev/null +++ b/internal/services/network/private_endpoint_application_security_group_association.go @@ -0,0 +1,317 @@ +package network + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/hashicorp/terraform-provider-azurerm/internal/locks" + "github.com/hashicorp/terraform-provider-azurerm/internal/sdk" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/services/network/validate" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" + "github.com/tombuildsstuff/kermit/sdk/network/2022-05-01/network" +) + +type PrivateEndpointApplicationSecurityGroupAssociationResource struct { +} + +var ( + _ sdk.Resource = PrivateEndpointApplicationSecurityGroupAssociationResource{} +) + +type PrivateEndpointApplicationSecurityGroupAssociationModel struct { + PrivateEndpointId string `tfschema:"private_endpoint_id"` + ApplicationSecurityGroupId string `tfschema:"application_security_group_id"` +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) Arguments() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{ + "private_endpoint_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.PrivateEndpointID, + }, + "application_security_group_id": { + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validate.ApplicationSecurityGroupID, + }, + } +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) Attributes() map[string]*pluginsdk.Schema { + return map[string]*pluginsdk.Schema{} +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) ModelObject() interface{} { + return &PrivateEndpointApplicationSecurityGroupAssociationModel{} +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) ResourceType() string { + return "azurerm_private_endpoint_application_security_group_association" +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) Create() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var state PrivateEndpointApplicationSecurityGroupAssociationModel + if err := metadata.Decode(&state); err != nil { + return err + } + + privateEndpointClient := metadata.Client.Network.PrivateEndpointClient + privateEndpointId, err := parse.PrivateEndpointID(state.PrivateEndpointId) + if err != nil { + return err + } + + locks.ByName(privateEndpointId.Name, "azurerm_private_endpoint") + defer locks.UnlockByName(privateEndpointId.Name, "azurerm_private_endpoint") + + ASGClient := metadata.Client.Network.ApplicationSecurityGroupsClient + ASGId, err := parse.ApplicationSecurityGroupID(state.ApplicationSecurityGroupId) + if err != nil { + return err + } + + locks.ByName(ASGId.Name, "azurerm_application_security_group") + defer locks.UnlockByName(ASGId.Name, "azurerm_application_security_group") + + existingPrivateEndpoint, err := privateEndpointClient.Get(ctx, privateEndpointId.ResourceGroup, privateEndpointId.Name, "") + if err != nil && !utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", privateEndpointId, err) + } + + if utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("PrivateEndpoint %q does not exsits", privateEndpointId) + } + + existingASG, err := ASGClient.Get(ctx, ASGId.ResourceGroup, ASGId.Name) + if err != nil && !utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("checking for the presence of existingPrivateEndpoint %q: %+v", ASGId, err) + } + + if utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("ApplicationSecurityGroup %q does not exsits", ASGId) + } + + resourceId := parse.NewPrivateEndpointApplicationSecurityGroupAssociationId(*privateEndpointId, *ASGId) + + input := existingPrivateEndpoint + ASGList := existingPrivateEndpoint.ApplicationSecurityGroups + + // flag: application security group exists in private endpoint configuration + ASGInPE := false + + if input.PrivateEndpointProperties != nil && input.PrivateEndpointProperties.ApplicationSecurityGroups != nil { + for _, value := range *ASGList { + if value.ID != nil && *value.ID == ASGId.ID() { + ASGInPE = true + break + } + } + } + + if ASGInPE { + return fmt.Errorf("A resource with the ID %q already exists - to be managed via Terraform this resource needs to be imported into the State. Please see the resource documentation for %q for more information.", resourceId.ID(), "azurerm_private_endpoint_application_security_group_association") + } + + if ASGList != nil { + *ASGList = append(*ASGList, existingASG) + input.ApplicationSecurityGroups = ASGList + } else { + input.ApplicationSecurityGroups = &[]network.ApplicationSecurityGroup{ + existingASG, + } + } + + future, err := privateEndpointClient.CreateOrUpdate(ctx, privateEndpointId.ResourceGroup, privateEndpointId.Name, input) + + if err != nil { + return fmt.Errorf("creating %s: %+v", privateEndpointId, err) + } + + if err := future.WaitForCompletionRef(ctx, privateEndpointClient.Client); err != nil { + return fmt.Errorf("waiting for creation of %s: %+v", privateEndpointId, err) + } + + metadata.SetID(resourceId) + return nil + }, + Timeout: 30 * time.Minute, + } +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) Read() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + resourceId, err := parse.PrivateEndpointApplicationSecurityGroupAssociationID(metadata.ResourceData.Id()) + if err != nil { + return err + } + + privateEndpointClient := metadata.Client.Network.PrivateEndpointClient + + privateEndpointId, err := parse.PrivateEndpointID(resourceId.PrivateEndpointId.ID()) + if err != nil { + return err + } + + locks.ByName(privateEndpointId.Name, "azurerm_private_endpoint") + defer locks.UnlockByName(privateEndpointId.Name, "azurerm_private_endpoint") + + ASGClient := metadata.Client.Network.ApplicationSecurityGroupsClient + + ASGId, err := parse.ApplicationSecurityGroupID(resourceId.ApplicationSecurityGroupId.ID()) + if err != nil { + return err + } + + locks.ByName(ASGId.Name, "azurerm_application_security_group") + defer locks.UnlockByName(ASGId.Name, "azurerm_application_security_group") + + existingPrivateEndpoint, err := privateEndpointClient.Get(ctx, privateEndpointId.ResourceGroup, privateEndpointId.Name, "") + if err != nil && !utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", privateEndpointId, err) + } + + if utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("PrivateEndpoint %q does not exsits", privateEndpointId) + } + + existingASG, err := ASGClient.Get(ctx, ASGId.ResourceGroup, ASGId.Name) + if err != nil && !utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("checking for the presence of existingPrivateEndpoint %q: %+v", ASGId, err) + } + + if utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("ApplicationSecurityGroup %q does not exsits", ASGId) + } + + // flag: application security group exists in private endpoint configuration + ASGInPE := false + + input := existingPrivateEndpoint + if input.PrivateEndpointProperties != nil && input.PrivateEndpointProperties.ApplicationSecurityGroups != nil { + ASGList := *input.PrivateEndpointProperties.ApplicationSecurityGroups + for _, value := range ASGList { + if value.ID != nil && *value.ID == ASGId.ID() { + ASGInPE = true + break + } + } + } + if !ASGInPE { + log.Printf("ApplicationSecurityGroup %q does not exsits in %q, removing from state.", ASGId, privateEndpointId) + err := metadata.MarkAsGone(resourceId) + if err != nil { + return err + } + } + + state := PrivateEndpointApplicationSecurityGroupAssociationModel{ + ApplicationSecurityGroupId: ASGId.ID(), + PrivateEndpointId: privateEndpointId.ID(), + } + + return metadata.Encode(&state) + }, + Timeout: 5 * time.Minute, + } +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) Delete() sdk.ResourceFunc { + return sdk.ResourceFunc{ + Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error { + var state PrivateEndpointApplicationSecurityGroupAssociationModel + if err := metadata.Decode(&state); err != nil { + return err + } + + privateEndpointClient := metadata.Client.Network.PrivateEndpointClient + + privateEndpointId, err := parse.PrivateEndpointID(state.PrivateEndpointId) + if err != nil { + return err + } + + locks.ByName(privateEndpointId.Name, "azurerm_private_endpoint") + defer locks.UnlockByName(privateEndpointId.Name, "azurerm_private_endpoint") + + ASGClient := metadata.Client.Network.ApplicationSecurityGroupsClient + + ASGId, err := parse.ApplicationSecurityGroupID(state.ApplicationSecurityGroupId) + if err != nil { + return err + } + + locks.ByName(ASGId.Name, "azurerm_application_security_group") + defer locks.UnlockByName(ASGId.Name, "azurerm_application_security_group") + + existingPrivateEndpoint, err := privateEndpointClient.Get(ctx, privateEndpointId.ResourceGroup, privateEndpointId.Name, "") + if err != nil && !utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", privateEndpointId, err) + } + + if utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("PrivateEndpoint %q does not exsits", privateEndpointId) + } + + existingASG, err := ASGClient.Get(ctx, ASGId.ResourceGroup, ASGId.Name) + if err != nil && !utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("checking for the presence of existingPrivateEndpoint %q: %+v", ASGId, err) + } + + if utils.ResponseWasNotFound(existingASG.Response) { + return fmt.Errorf("ApplicationSecurityGroup %q does not exsits", ASGId) + } + + resourceId := parse.NewPrivateEndpointApplicationSecurityGroupAssociationId(*privateEndpointId, *ASGId) + + // flag: application security group exists in private endpoint configuration + ASGInPE := false + + input := existingPrivateEndpoint + if input.PrivateEndpointProperties != nil && input.PrivateEndpointProperties.ApplicationSecurityGroups != nil { + ASGList := *input.PrivateEndpointProperties.ApplicationSecurityGroups + newASGList := make([]network.ApplicationSecurityGroup, 0) + for idx, value := range ASGList { + if value.ID != nil && *value.ID == ASGId.ID() { + newASGList = append(newASGList, ASGList[:idx]...) + newASGList = append(newASGList, ASGList[idx+1:]...) + ASGInPE = true + break + } + } + if ASGInPE { + input.PrivateEndpointProperties.ApplicationSecurityGroups = &newASGList + } else { + return fmt.Errorf("deletion failed, ApplicationSecurityGroup %q does not linked with PrivateEndpoint %q", ASGId, privateEndpointId) + } + } + + future, err := privateEndpointClient.CreateOrUpdate(ctx, privateEndpointId.ResourceGroup, privateEndpointId.Name, input) + + if err != nil { + return fmt.Errorf("creating %s: %+v", privateEndpointId, err) + } + + if err := future.WaitForCompletionRef(ctx, privateEndpointClient.Client); err != nil { + return fmt.Errorf("waiting for creation of %s: %+v", privateEndpointId, err) + } + + metadata.SetID(resourceId) + return nil + }, + Timeout: 30 * time.Minute, + } +} + +func (p PrivateEndpointApplicationSecurityGroupAssociationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc { + return parse.PrivateEndpointApplicationSecurityGroupAssociationIDValidation +} diff --git a/internal/services/network/private_endpoint_application_security_group_association_test.go b/internal/services/network/private_endpoint_application_security_group_association_test.go new file mode 100644 index 000000000000..9637b7a14fd9 --- /dev/null +++ b/internal/services/network/private_endpoint_application_security_group_association_test.go @@ -0,0 +1,297 @@ +package network_test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "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/services/network/parse" + "github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azurerm/utils" + "github.com/tombuildsstuff/kermit/sdk/network/2022-05-01/network" +) + +type PrivateEndpointApplicationSecurityGroupAssociationResource struct { +} + +func TestAccPrivateEndpointApplicationSecurityGroupAssociationResource_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_endpoint_application_security_group_association", "test") + r := PrivateEndpointApplicationSecurityGroupAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + // intentional as this is a Virtual Resource + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + data.ImportStep(), + }) +} + +func TestAccPrivateEndpointApplicationSecurityGroupAssociationResource_requiresImport(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_endpoint_application_security_group_association", "test") + r := PrivateEndpointApplicationSecurityGroupAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + // intentional as this is a Virtual Resource + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + { + Config: r.requiresImport(data), + ExpectError: acceptance.RequiresImportError("azurerm_private_endpoint_application_security_group_association"), + }, + }) +} + +func TestAccPrivateEndpointApplicationSecurityGroupAssociationResource_deleted(t *testing.T) { + data := acceptance.BuildTestData(t, "azurerm_private_endpoint_application_security_group_association", "test") + r := PrivateEndpointApplicationSecurityGroupAssociationResource{} + data.ResourceTest(t, r, []acceptance.TestStep{ + // intentionally not using a DisappearsStep as this is a Virtual Resource + { + Config: r.basic(data), + Check: acceptance.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + data.CheckWithClient(r.destroy), + ), + ExpectNonEmptyPlan: true, + }, + }) +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) Exists(ctx context.Context, client *clients.Client, state *pluginsdk.InstanceState) (*bool, error) { + splitId := strings.Split(state.ID, "|") + + exists := false + + if len(splitId) != 2 { + return &exists, fmt.Errorf("expected ID to be in the format {PrivateEndpointId}|{ApplicationSecurityGroupId} but got %q", state.ID) + } + + endpointId, err := parse.PrivateEndpointID(splitId[0]) + if err != nil { + return &exists, err + } + + securityGroupId, err := parse.ApplicationSecurityGroupID(splitId[1]) + if err != nil { + return &exists, err + } + + if endpointId == nil || securityGroupId == nil { + return &exists, fmt.Errorf("parse error, both PrivateEndpointId and ApplicationSecurityGroupId should not be nil") + } + + privateEndpointClient := client.Network.PrivateEndpointClient + existingPrivateEndpoint, err := privateEndpointClient.Get(ctx, endpointId.ResourceGroup, endpointId.Name, "") + if err != nil && !utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return &exists, fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", endpointId, err) + } + + if utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return &exists, fmt.Errorf("PrivateEndpoint %q does not exsits", endpointId) + } + + input := existingPrivateEndpoint + ASGList := existingPrivateEndpoint.ApplicationSecurityGroups + if input.PrivateEndpointProperties != nil && input.PrivateEndpointProperties.ApplicationSecurityGroups != nil { + for _, value := range *ASGList { + if value.ID != nil && *value.ID == securityGroupId.ID() { + exists = true + break + } + } + } + return &exists, nil +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) basic(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_private_endpoint" "test" { + name = "acctest-privatelink-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + subnet_id = azurerm_subnet.endpoint.id + + private_service_connection { + name = azurerm_private_link_service.test.name + is_manual_connection = false + private_connection_resource_id = azurerm_private_link_service.test.id + } +} + +resource "azurerm_application_security_group" "test" { + name = "acctest-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name +} + +resource "azurerm_private_endpoint_application_security_group_association" "test" { + private_endpoint_id = azurerm_private_endpoint.test.id + application_security_group_id = azurerm_application_security_group.test.id +} +`, r.template(data, r.serviceAutoApprove(data)), data.RandomInteger, data.RandomInteger) +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) serviceAutoApprove(data acceptance.TestData) string { + return fmt.Sprintf(` + +resource "azurerm_private_link_service" "test" { + name = "acctestPLS-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + auto_approval_subscription_ids = [data.azurerm_subscription.current.subscription_id] + visibility_subscription_ids = [data.azurerm_subscription.current.subscription_id] + + nat_ip_configuration { + name = "primaryIpConfiguration-%d" + primary = true + subnet_id = azurerm_subnet.service.id + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] +} +`, data.RandomInteger, data.RandomInteger) +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) template(data acceptance.TestData, seviceCfg string) string { + return fmt.Sprintf(` +provider "azurerm" { + features {} +} + +data "azurerm_subscription" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-PEASGAsso-%d" + location = "%s" +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvnet-%d" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + address_space = ["10.5.0.0/16"] +} + +resource "azurerm_subnet" "service" { + name = "acctestsnetservice-%d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.5.1.0/24"] + + enforce_private_link_service_network_policies = true +} + +resource "azurerm_subnet" "endpoint" { + name = "acctestsnetendpoint-%d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefixes = ["10.5.2.0/24"] + + enforce_private_link_endpoint_network_policies = true +} + +resource "azurerm_public_ip" "test" { + name = "acctestpip-%d" + sku = "Standard" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + allocation_method = "Static" +} + +resource "azurerm_lb" "test" { + name = "acctestlb-%d" + sku = "Standard" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + frontend_ip_configuration { + name = azurerm_public_ip.test.name + public_ip_address_id = azurerm_public_ip.test.id + } +} + +%s +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, data.RandomInteger, seviceCfg) +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) requiresImport(data acceptance.TestData) string { + return fmt.Sprintf(` +%s + +resource "azurerm_private_endpoint_application_security_group_association" "import" { + private_endpoint_id = azurerm_private_endpoint.test.id + application_security_group_id = azurerm_application_security_group.test.id +} +`, r.basic(data)) +} + +func (r PrivateEndpointApplicationSecurityGroupAssociationResource) destroy(ctx context.Context, client *clients.Client, state *terraform.InstanceState) error { + endpointId, err := parse.PrivateEndpointID(state.Attributes["private_endpoint_id"]) + if err != nil { + return err + } + + securityGroupId, err := parse.ApplicationSecurityGroupID(state.Attributes["application_security_group_id"]) + if err != nil { + return err + } + + privateEndpointClient := client.Network.PrivateEndpointClient + + existingPrivateEndpoint, err := privateEndpointClient.Get(ctx, endpointId.ResourceGroup, endpointId.Name, "") + if err != nil && !utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("checking for the presence of existing PrivateEndpoint %q: %+v", endpointId, err) + } + + if utils.ResponseWasNotFound(existingPrivateEndpoint.Response) { + return fmt.Errorf("PrivateEndpoint %q does not exsits", endpointId) + } + + // flag: application security group exists in private endpoint configuration + ASGInPE := false + + input := existingPrivateEndpoint + if input.PrivateEndpointProperties != nil && input.PrivateEndpointProperties.ApplicationSecurityGroups != nil { + ASGList := *input.PrivateEndpointProperties.ApplicationSecurityGroups + newASGList := make([]network.ApplicationSecurityGroup, 0) + for idx, value := range ASGList { + if value.ID != nil && *value.ID == securityGroupId.ID() { + newASGList = append(newASGList, ASGList[:idx]...) + newASGList = append(newASGList, ASGList[idx+1:]...) + ASGInPE = true + break + } + } + if ASGInPE { + input.PrivateEndpointProperties.ApplicationSecurityGroups = &newASGList + } else { + return fmt.Errorf("deletion failed, ApplicationSecurityGroup %q does not linked with PrivateEndpoint %q", securityGroupId, endpointId) + } + } + + future, err := privateEndpointClient.CreateOrUpdate(ctx, endpointId.ResourceGroup, endpointId.Name, input) + + if err != nil { + return fmt.Errorf("creating %s: %+v", endpointId, err) + } + + if err := future.WaitForCompletionRef(ctx, privateEndpointClient.Client); err != nil { + return fmt.Errorf("waiting for creation of %s: %+v", endpointId, err) + } + + return nil +} diff --git a/internal/services/network/registration.go b/internal/services/network/registration.go index e47a3815b5fd..e0743d5ae58e 100644 --- a/internal/services/network/registration.go +++ b/internal/services/network/registration.go @@ -35,6 +35,7 @@ func (r Registration) DataSources() []sdk.DataSource { func (r Registration) Resources() []sdk.Resource { return []sdk.Resource{ ManagerResource{}, + PrivateEndpointApplicationSecurityGroupAssociationResource{}, ManagerNetworkGroupResource{}, ManagerSubscriptionConnectionResource{}, RouteMapResource{}, diff --git a/website/docs/r/private_endpoint_application_security_group_association.html.markdown b/website/docs/r/private_endpoint_application_security_group_association.html.markdown new file mode 100644 index 000000000000..454ab2a98599 --- /dev/null +++ b/website/docs/r/private_endpoint_application_security_group_association.html.markdown @@ -0,0 +1,145 @@ +--- +subcategory: "Network" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_private_endpoint_application_security_group_association" +description: |- + Manages an association between Private Endpoint and Application Security Group. + +--- + +# azurerm_private_endpoint_application_security_group_association + +Manages an association between Private Endpoint and Application Security Group. + +## Example Usage + +```hcl +provider "azurerm" { + features {} +} + +data "azurerm_subscription" "current" {} + +resource "azurerm_resource_group" "example" { + name = "example-PEASGAsso" + location = "West Europe" +} + +resource "azurerm_virtual_network" "example" { + name = "examplevnet" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + address_space = ["10.5.0.0/16"] +} + +resource "azurerm_subnet" "service" { + name = "examplenetservice" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.5.1.0/24"] + + enforce_private_link_service_network_policies = true +} + +resource "azurerm_subnet" "endpoint" { + name = "examplenetendpoint" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefixes = ["10.5.2.0/24"] + + enforce_private_link_endpoint_network_policies = true +} + +resource "azurerm_public_ip" "example" { + name = "examplepip" + sku = "Standard" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + allocation_method = "Static" +} + +resource "azurerm_lb" "example" { + name = "examplelb" + sku = "Standard" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + frontend_ip_configuration { + name = azurerm_public_ip.example.name + public_ip_address_id = azurerm_public_ip.example.id + } +} + +resource "azurerm_private_link_service" "example" { + name = "examplePLS" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + auto_approval_subscription_ids = [data.azurerm_subscription.current.subscription_id] + visibility_subscription_ids = [data.azurerm_subscription.current.subscription_id] + + nat_ip_configuration { + name = "primaryIpConfiguration" + primary = true + subnet_id = azurerm_subnet.service.id + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.example.frontend_ip_configuration.0.id + ] +} + +resource "azurerm_private_endpoint" "example" { + name = "example-privatelink" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + subnet_id = azurerm_subnet.endpoint.id + + private_service_connection { + name = azurerm_private_link_service.example.name + is_manual_connection = false + private_connection_resource_id = azurerm_private_link_service.example.id + } +} + +resource "azurerm_application_security_group" "example" { + name = "example" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name +} + +resource "azurerm_private_endpoint_application_security_group_association" "example" { + private_endpoint_id = azurerm_private_endpoint.example.id + application_security_group_id = azurerm_application_security_group.example.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `application_security_group_id` - (Required) The id of application security group to associate. Changing this forces a new resource to be created. + +* `private_endpoint_id` - (Required) The id of private endpoint to associate. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The (Terraform specific) ID of the association between Private Endpoint and Application Security Group. + +## 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 association between Private Endpoint and Application Security Group. +* `read` - (Defaults to 5 minutes) Used when retrieving the association between Private Endpoint and Application Security Group. +* `delete` - (Defaults to 30 minutes) Used when deleting the association between Private Endpoint and Application Security Group. + +## Import + +Associations between Private Endpoint and Application Security Group can be imported using the `resource id`, e.g. + +```shell +terraform import azurerm_private_endpoint_application_security_group_association.association1 "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/privateEndpoints/endpoints1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/applicationSecurityGroups/securityGroup1", +``` + +-> **NOTE:** This ID is specific to Terraform - and is of the format `{privateEndpointId}|{applicationSecurityGroupId}`.