diff --git a/azurerm/data_source_loadbalancer.go b/azurerm/data_source_loadbalancer.go index 1f8f0ef12d4d..cee6def390a9 100644 --- a/azurerm/data_source_loadbalancer.go +++ b/azurerm/data_source_loadbalancer.go @@ -69,6 +69,11 @@ func dataSourceArmLoadBalancer() *schema.Resource { }, "zones": azure.SchemaZonesComputed(), + + "id": { + Type: schema.TypeString, + Computed: true, + }, }, }, }, @@ -158,6 +163,10 @@ func flattenLoadBalancerDataSourceFrontendIpConfiguration(ipConfigs *[]network.F ipConfig["name"] = *config.Name } + if config.ID != nil { + ipConfig["id"] = *config.ID + } + zones := make([]string, 0) if zs := config.Zones; zs != nil { zones = *zs diff --git a/azurerm/data_source_private_link_service.go b/azurerm/data_source_private_link_service.go new file mode 100644 index 000000000000..a9e760989905 --- /dev/null +++ b/azurerm/data_source_private_link_service.go @@ -0,0 +1,168 @@ +package azurerm + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + aznet "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmPrivateLinkService() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmPrivateLinkServiceRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: aznet.ValidatePrivateLinkServiceName, + }, + + "location": azure.SchemaLocationForDataSource(), + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + + "auto_approval_subscription_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "visibility_subscription_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + // currently not implemented yet, timeline unknown, exact purpose unknown, maybe coming to a future API near you + // "fqdns": { + // Type: schema.TypeList, + // Computed: true, + // Elem: &schema.Schema{ + // Type: schema.TypeString, + // }, + // }, + + "nat_ip_configuration": { + Type: schema.TypeList, + Computed: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address": { + Type: schema.TypeString, + Computed: true, + }, + "private_ip_address_version": { + Type: schema.TypeString, + Computed: true, + }, + "subnet_id": { + Type: schema.TypeString, + Computed: true, + }, + "primary": { + Type: schema.TypeBool, + Computed: true, + }, + }, + }, + }, + + "load_balancer_frontend_ip_configuration_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "alias": { + Type: schema.TypeString, + Computed: true, + }, + + "network_interface_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Set: schema.HashString, + }, + + "tags": tags.SchemaDataSource(), + }, + } +} + +func dataSourceArmPrivateLinkServiceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.PrivateLinkServiceClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + resp, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: Private Link Service %q (Resource Group %q) was not found", name, resourceGroup) + } + return fmt.Errorf("Error reading Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if resp.ID == nil { + return fmt.Errorf("Cannot read ID for Private Link Service %q (Resource Group %q)", name, resourceGroup) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + d.Set("location", azure.NormalizeLocation(*resp.Location)) + + if props := resp.PrivateLinkServiceProperties; props != nil { + d.Set("alias", props.Alias) + if props.AutoApproval.Subscriptions != nil { + if err := d.Set("auto_approval_subscription_ids", utils.FlattenStringSlice(props.AutoApproval.Subscriptions)); err != nil { + return fmt.Errorf("Error setting `auto_approval_subscription_ids`: %+v", err) + } + } + if props.Visibility.Subscriptions != nil { + if err := d.Set("visibility_subscription_ids", utils.FlattenStringSlice(props.Visibility.Subscriptions)); err != nil { + return fmt.Errorf("Error setting `visibility_subscription_ids`: %+v", err) + } + } + // currently not implemented yet, timeline unknown, exact purpose unknown, maybe coming to a future API near you + // if props.Fqdns != nil { + // if err := d.Set("fqdns", utils.FlattenStringSlice(props.Fqdns)); err != nil { + // return fmt.Errorf("Error setting `fqdns`: %+v", err) + // } + // } + if props.IPConfigurations != nil { + if err := d.Set("nat_ip_configuration", flattenArmPrivateLinkServiceIPConfiguration(props.IPConfigurations)); err != nil { + return fmt.Errorf("Error setting `nat_ip_configuration`: %+v", err) + } + } + if props.LoadBalancerFrontendIPConfigurations != nil { + if err := d.Set("load_balancer_frontend_ip_configuration_ids", flattenArmPrivateLinkServiceFrontendIPConfiguration(props.LoadBalancerFrontendIPConfigurations)); err != nil { + return fmt.Errorf("Error setting `load_balancer_frontend_ip_configuration_ids`: %+v", err) + } + } + if props.NetworkInterfaces != nil { + if err := d.Set("network_interface_ids", flattenArmPrivateLinkServiceInterface(props.NetworkInterfaces)); err != nil { + return fmt.Errorf("Error setting `network_interface_ids`: %+v", err) + } + } + } + + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("API returns a nil/empty id on Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + d.SetId(*resp.ID) + + return tags.FlattenAndSet(d, resp.Tags) +} diff --git a/azurerm/data_source_private_link_service_endpoint_connections.go b/azurerm/data_source_private_link_service_endpoint_connections.go new file mode 100644 index 000000000000..b551c391a03f --- /dev/null +++ b/azurerm/data_source_private_link_service_endpoint_connections.go @@ -0,0 +1,149 @@ +package azurerm + +import ( + "fmt" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + aznet "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func dataSourceArmPrivateLinkServiceEndpointConnections() *schema.Resource { + return &schema.Resource{ + Read: dataSourceArmPrivateLinkServiceEndpointConnectionsRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ValidateFunc: aznet.ValidatePrivateLinkServiceName, + }, + + "location": azure.SchemaLocationForDataSource(), + + "resource_group_name": azure.SchemaResourceGroupNameForDataSource(), + + "private_endpoint_connections": { + Type: schema.TypeList, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "connection_id": { + Type: schema.TypeString, + Computed: true, + }, + "connection_name": { + Type: schema.TypeString, + Computed: true, + }, + "private_endpoint_id": { + Type: schema.TypeString, + Computed: true, + }, + "private_endpoint_name": { + Type: schema.TypeString, + Computed: true, + }, + "action_required": { + Type: schema.TypeString, + Computed: true, + }, + "description": { + Type: schema.TypeString, + Computed: true, + }, + "status": { + Type: schema.TypeString, + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataSourceArmPrivateLinkServiceEndpointConnectionsRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.PrivateLinkServiceClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + resp, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error: Private Link Service %q (Resource Group %q) was not found", name, resourceGroup) + } + return fmt.Errorf("Error reading Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("API returns a nil/empty id on Private Link Service Endpoint Connection Status %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + d.Set("location", azure.NormalizeLocation(*resp.Location)) + + if props := resp.PrivateLinkServiceProperties; props != nil { + if ip := props.PrivateEndpointConnections; ip != nil { + if err := d.Set("private_endpoint_connections", flattenArmPrivateLinkServicePrivateEndpointConnections(ip)); err != nil { + return fmt.Errorf("Error setting `private_endpoint_connections`: %+v", err) + } + } + } + + d.SetId(*resp.ID) + + return nil +} + +func flattenArmPrivateLinkServicePrivateEndpointConnections(input *[]network.PrivateEndpointConnection) []interface{} { + results := make([]interface{}, 0) + if input == nil { + return results + } + + for _, item := range *input { + v := make(map[string]interface{}) + if id := item.ID; id != nil { + v["connection_id"] = *id + } + if name := item.Name; name != nil { + v["connection_name"] = *name + } + + if props := item.PrivateEndpointConnectionProperties; props != nil { + if p := props.PrivateEndpoint; p != nil { + if id := p.ID; id != nil { + v["private_endpoint_id"] = *id + + id, _ := azure.ParseAzureResourceID(*id) + name := id.Path["privateEndpoints"] + if name != "" { + v["private_endpoint_name"] = name + } + } + } + if s := props.PrivateLinkServiceConnectionState; s != nil { + if a := s.ActionsRequired; a != nil { + v["action_required"] = *a + } else { + v["action_required"] = "none" + } + if d := s.Description; d != nil { + v["description"] = *d + } + if t := s.Status; t != nil { + v["status"] = *t + } + } + } + + results = append(results, v) + } + + return results +} diff --git a/azurerm/data_source_private_link_service_test.go b/azurerm/data_source_private_link_service_test.go new file mode 100644 index 000000000000..700177adefec --- /dev/null +++ b/azurerm/data_source_private_link_service_test.go @@ -0,0 +1,45 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" +) + +func TestAccDataSourceAzureRMPrivateLinkService_complete(t *testing.T) { + dataSourceName := "data.azurerm_private_link_service.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccDataSourcePrivateLinkService_complete(ri, location), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(dataSourceName, "nat_ip_configuration.#", "2"), + resource.TestCheckResourceAttr(dataSourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.40"), + resource.TestCheckResourceAttr(dataSourceName, "nat_ip_configuration.0.private_ip_address_version", "IPv4"), + resource.TestCheckResourceAttr(dataSourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.41"), + resource.TestCheckResourceAttr(dataSourceName, "nat_ip_configuration.1.private_ip_address_version", "IPv4"), + resource.TestCheckResourceAttr(dataSourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + ), + }, + }, + }) +} + +func testAccDataSourcePrivateLinkService_complete(rInt int, location string) string { + config := testAccAzureRMPrivateLinkService_complete(rInt, location) + return fmt.Sprintf(` +%s + +data "azurerm_private_link_service" "test" { + name = azurerm_private_link_service.test.name + resource_group_name = azurerm_private_link_service.test.resource_group_name +} +`, config) +} diff --git a/azurerm/data_source_subnet.go b/azurerm/data_source_subnet.go index 2c977abdb636..234a48ff94bd 100644 --- a/azurerm/data_source_subnet.go +++ b/azurerm/data_source_subnet.go @@ -2,6 +2,7 @@ package azurerm import ( "fmt" + "strings" "time" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" @@ -63,6 +64,11 @@ func dataSourceArmSubnet() *schema.Resource { Type: schema.TypeString, }, }, + + "enforce_private_link_service_network_policies": { + Type: schema.TypeBool, + Computed: true, + }, }, } } @@ -92,6 +98,10 @@ func dataSourceArmSubnetRead(d *schema.ResourceData, meta interface{}) error { if props := resp.SubnetPropertiesFormat; props != nil { d.Set("address_prefix", props.AddressPrefix) + if p := props.PrivateLinkServiceNetworkPolicies; p != nil { + d.Set("enforce_private_link_service_network_policies", strings.EqualFold("Disabled", *p)) + } + if props.NetworkSecurityGroup != nil { d.Set("network_security_group_id", props.NetworkSecurityGroup.ID) } else { diff --git a/azurerm/helpers/validate/uuid.go b/azurerm/helpers/validate/uuid.go index 93bba3ab3723..0556385244ea 100644 --- a/azurerm/helpers/validate/uuid.go +++ b/azurerm/helpers/validate/uuid.go @@ -23,6 +23,9 @@ func UUID(i interface{}, k string) (warnings []string, errors []error) { return warnings, errors } +func GUID(i interface{}, k string) (warnings []string, errors []error) { + return UUID(i, k) +} func UUIDOrEmpty(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { diff --git a/azurerm/internal/services/network/client.go b/azurerm/internal/services/network/client.go index 28bf8e46b831..1a4f87591ff2 100644 --- a/azurerm/internal/services/network/client.go +++ b/azurerm/internal/services/network/client.go @@ -35,6 +35,7 @@ type Client struct { VirtualHubClient *network.VirtualHubsClient WatcherClient *network.WatchersClient WebApplicationFirewallPoliciesClient *network.WebApplicationFirewallPoliciesClient + PrivateLinkServiceClient *network.PrivateLinkServicesClient } func BuildClient(o *common.ClientOptions) *Client { @@ -92,6 +93,9 @@ func BuildClient(o *common.ClientOptions) *Client { PublicIPPrefixesClient := network.NewPublicIPPrefixesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&PublicIPPrefixesClient.Client, o.ResourceManagerAuthorizer) + PrivateLinkServiceClient := network.NewPrivateLinkServicesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) + o.ConfigureClient(&PrivateLinkServiceClient.Client, o.ResourceManagerAuthorizer) + RoutesClient := network.NewRoutesClientWithBaseURI(o.ResourceManagerEndpoint, o.SubscriptionId) o.ConfigureClient(&RoutesClient.Client, o.ResourceManagerAuthorizer) @@ -155,5 +159,6 @@ func BuildClient(o *common.ClientOptions) *Client { VirtualHubClient: &VirtualHubClient, WatcherClient: &WatcherClient, WebApplicationFirewallPoliciesClient: &WebApplicationFirewallPoliciesClient, + PrivateLinkServiceClient: &PrivateLinkServiceClient, } } diff --git a/azurerm/internal/services/network/validate.go b/azurerm/internal/services/network/validate.go index 3accbecee501..d4b538ab7618 100644 --- a/azurerm/internal/services/network/validate.go +++ b/azurerm/internal/services/network/validate.go @@ -3,8 +3,103 @@ package network import ( "fmt" "regexp" + + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) +func ValidatePrivateLinkNatIpConfiguration(d *schema.ResourceDiff) error { + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + ipConfigurations := d.Get("nat_ip_configuration").([]interface{}) + + for i, item := range ipConfigurations { + v := item.(map[string]interface{}) + p := fmt.Sprintf("nat_ip_configuration.%d.private_ip_address", i) + s := fmt.Sprintf("nat_ip_configuration.%d.subnet_id", i) + isPrimary := v["primary"].(bool) + in := v["name"].(string) + + if d.HasChange(p) { + o, n := d.GetChange(p) + if o != "" && n == "" { + return fmt.Errorf("Private Link Service %q (Resource Group %q) nat_ip_configuration %q private_ip_address once assigned can not be removed", name, resourceGroup, in) + } + } + + if isPrimary && d.HasChange(s) { + o, _ := d.GetChange(s) + if o != "" { + return fmt.Errorf("Private Link Service %q (Resource Group %q) nat_ip_configuration %q primary subnet_id once assigned can not be changed", name, resourceGroup, in) + } + } + } + + return nil +} + +func ValidatePrivateLinkServiceName(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + return nil, append(errors, fmt.Errorf("expected type of %s to be string", k)) + } + + // The name attribute rules per the Nat Gateway service team are (Friday, October 18, 2019 4:20 PM): + // 1. Must not be empty. + // 2. Must be between 1 and 80 characters. + // 3. The attribute must: + // a) begin with a letter or number + // b) end with a letter, number or underscore + // c) may contain only letters, numbers, underscores, periods, or hyphens. + + if len(v) == 1 { + if m, _ := validate.RegExHelper(i, k, `^([a-zA-Z\d])`); !m { + errors = append(errors, fmt.Errorf("%s must begin with a letter or number", k)) + } + } else { + if m, _ := validate.RegExHelper(i, k, `^([a-zA-Z\d])([a-zA-Z\d-\_\.]{0,78})([a-zA-Z\d\_])$`); !m { + errors = append(errors, fmt.Errorf("%s must be between 1 - 80 characters long, begin with a letter or number, end with a letter, number or underscore, and may contain only letters, numbers, underscores, periods, or hyphens", k)) + } + } + + return nil, errors +} + +func ValidatePrivateLinkServiceSubsciptionFqdn(i interface{}, k string) (_ []string, errors []error) { + v, ok := i.(string) + if !ok { + return nil, append(errors, fmt.Errorf("expected type of %q to be string", k)) + } + + if m, _ := validate.RegExHelper(i, k, `^(([a-zA-Z\d]|[a-zA-Z\d][a-zA-Z\d\-]*[a-zA-Z\d])\.){1,}([a-zA-Z\d]|[a-zA-Z\d][a-zA-Z\d\-]*[a-zA-Z\d\.]){1,}$`); !m { + errors = append(errors, fmt.Errorf(`%q is an invalid FQDN`, v)) + } + + // I use 255 here because the string contains the upto three . characters in it + if len(v) > 255 { + errors = append(errors, fmt.Errorf(`FQDNs can not be longer than 255 characters in length, got %d characters`, len(v))) + } + + segments := utils.SplitRemoveEmptyEntries(v, ".", false) + index := 0 + + for _, label := range segments { + index++ + if index == len(segments) { + if len(label) < 2 { + errors = append(errors, fmt.Errorf(`the last label of an FQDN must be at least 2 characters, got 1 character`)) + } + } else { + if len(label) > 63 { + errors = append(errors, fmt.Errorf(`FQDN labels must not be longer than 63 characters, got %d characters`, len(label))) + } + } + } + + return nil, errors +} + func ValidateVirtualHubName(v interface{}, k string) (warnings []string, errors []error) { value := v.(string) diff --git a/azurerm/provider.go b/azurerm/provider.go index 7433c0988216..4466a7e1699c 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -52,107 +52,109 @@ func Provider() terraform.ResourceProvider { } dataSources := map[string]*schema.Resource{ - "azurerm_api_management": dataSourceApiManagementService(), - "azurerm_api_management_api": dataSourceApiManagementApi(), - "azurerm_api_management_group": dataSourceApiManagementGroup(), - "azurerm_api_management_product": dataSourceApiManagementProduct(), - "azurerm_api_management_user": dataSourceArmApiManagementUser(), - "azurerm_app_service_plan": dataSourceAppServicePlan(), - "azurerm_app_service_certificate": dataSourceAppServiceCertificate(), - "azurerm_app_service": dataSourceArmAppService(), - "azurerm_app_service_certificate_order": dataSourceArmAppServiceCertificateOrder(), - "azurerm_application_insights": dataSourceArmApplicationInsights(), - "azurerm_application_security_group": dataSourceArmApplicationSecurityGroup(), - "azurerm_automation_account": dataSourceArmAutomationAccount(), - "azurerm_automation_variable_bool": dataSourceArmAutomationVariableBool(), - "azurerm_automation_variable_datetime": dataSourceArmAutomationVariableDateTime(), - "azurerm_automation_variable_int": dataSourceArmAutomationVariableInt(), - "azurerm_automation_variable_string": dataSourceArmAutomationVariableString(), - "azurerm_availability_set": dataSourceArmAvailabilitySet(), - "azurerm_azuread_application": dataSourceArmAzureADApplication(), - "azurerm_azuread_service_principal": dataSourceArmActiveDirectoryServicePrincipal(), - "azurerm_batch_account": dataSourceArmBatchAccount(), - "azurerm_batch_certificate": dataSourceArmBatchCertificate(), - "azurerm_batch_pool": dataSourceArmBatchPool(), - "azurerm_builtin_role_definition": dataSourceArmBuiltInRoleDefinition(), - "azurerm_cdn_profile": dataSourceArmCdnProfile(), - "azurerm_client_config": dataSourceArmClientConfig(), - "azurerm_kubernetes_service_versions": dataSourceArmKubernetesServiceVersions(), - "azurerm_container_registry": dataSourceArmContainerRegistry(), - "azurerm_cosmosdb_account": dataSourceArmCosmosDbAccount(), - "azurerm_data_factory": dataSourceArmDataFactory(), - "azurerm_data_lake_store": dataSourceArmDataLakeStoreAccount(), - "azurerm_dev_test_lab": dataSourceArmDevTestLab(), - "azurerm_dev_test_virtual_network": dataSourceArmDevTestVirtualNetwork(), - "azurerm_dns_zone": dataSourceArmDnsZone(), - "azurerm_eventhub_namespace": dataSourceEventHubNamespace(), - "azurerm_express_route_circuit": dataSourceArmExpressRouteCircuit(), - "azurerm_firewall": dataSourceArmFirewall(), - "azurerm_image": dataSourceArmImage(), - "azurerm_hdinsight_cluster": dataSourceArmHDInsightSparkCluster(), - "azurerm_healthcare_service": dataSourceArmHealthcareService(), - "azurerm_maps_account": dataSourceArmMapsAccount(), - "azurerm_key_vault_access_policy": dataSourceArmKeyVaultAccessPolicy(), - "azurerm_key_vault_key": dataSourceArmKeyVaultKey(), - "azurerm_key_vault_secret": dataSourceArmKeyVaultSecret(), - "azurerm_key_vault": dataSourceArmKeyVault(), - "azurerm_kubernetes_cluster": dataSourceArmKubernetesCluster(), - "azurerm_lb": dataSourceArmLoadBalancer(), - "azurerm_lb_backend_address_pool": dataSourceArmLoadBalancerBackendAddressPool(), - "azurerm_log_analytics_workspace": dataSourceLogAnalyticsWorkspace(), - "azurerm_logic_app_workflow": dataSourceArmLogicAppWorkflow(), - "azurerm_managed_disk": dataSourceArmManagedDisk(), - "azurerm_management_group": dataSourceArmManagementGroup(), - "azurerm_monitor_action_group": dataSourceArmMonitorActionGroup(), - "azurerm_monitor_diagnostic_categories": dataSourceArmMonitorDiagnosticCategories(), - "azurerm_monitor_log_profile": dataSourceArmMonitorLogProfile(), - "azurerm_mssql_elasticpool": dataSourceArmMsSqlElasticpool(), - "azurerm_netapp_account": dataSourceArmNetAppAccount(), - "azurerm_netapp_pool": dataSourceArmNetAppPool(), - "azurerm_network_ddos_protection_plan": dataSourceNetworkDDoSProtectionPlan(), - "azurerm_network_interface": dataSourceArmNetworkInterface(), - "azurerm_network_security_group": dataSourceArmNetworkSecurityGroup(), - "azurerm_network_watcher": dataSourceArmNetworkWatcher(), - "azurerm_notification_hub_namespace": dataSourceNotificationHubNamespace(), - "azurerm_notification_hub": dataSourceNotificationHub(), - "azurerm_platform_image": dataSourceArmPlatformImage(), - "azurerm_policy_definition": dataSourceArmPolicyDefinition(), - "azurerm_postgresql_server": dataSourcePostgreSqlServer(), - "azurerm_proximity_placement_group": dataSourceArmProximityPlacementGroup(), - "azurerm_public_ip": dataSourceArmPublicIP(), - "azurerm_public_ips": dataSourceArmPublicIPs(), - "azurerm_public_ip_prefix": dataSourceArmPublicIpPrefix(), - "azurerm_recovery_services_vault": dataSourceArmRecoveryServicesVault(), - "azurerm_recovery_services_protection_policy_vm": dataSourceArmRecoveryServicesProtectionPolicyVm(), - "azurerm_redis_cache": dataSourceArmRedisCache(), - "azurerm_resources": dataSourceArmResources(), - "azurerm_resource_group": dataSourceArmResourceGroup(), - "azurerm_role_definition": dataSourceArmRoleDefinition(), - "azurerm_route_table": dataSourceArmRouteTable(), - "azurerm_scheduler_job_collection": dataSourceArmSchedulerJobCollection(), - "azurerm_servicebus_namespace": dataSourceArmServiceBusNamespace(), - "azurerm_servicebus_namespace_authorization_rule": dataSourceArmServiceBusNamespaceAuthorizationRule(), - "azurerm_shared_image_gallery": dataSourceArmSharedImageGallery(), - "azurerm_shared_image_version": dataSourceArmSharedImageVersion(), - "azurerm_shared_image": dataSourceArmSharedImage(), - "azurerm_snapshot": dataSourceArmSnapshot(), - "azurerm_sql_server": dataSourceSqlServer(), - "azurerm_sql_database": dataSourceSqlDatabase(), - "azurerm_stream_analytics_job": dataSourceArmStreamAnalyticsJob(), - "azurerm_storage_account_blob_container_sas": dataSourceArmStorageAccountBlobContainerSharedAccessSignature(), - "azurerm_storage_account_sas": dataSourceArmStorageAccountSharedAccessSignature(), - "azurerm_storage_account": dataSourceArmStorageAccount(), - "azurerm_storage_management_policy": dataSourceArmStorageManagementPolicy(), - "azurerm_subnet": dataSourceArmSubnet(), - "azurerm_subscription": dataSourceArmSubscription(), - "azurerm_subscriptions": dataSourceArmSubscriptions(), - "azurerm_traffic_manager_geographical_location": dataSourceArmTrafficManagerGeographicalLocation(), - "azurerm_user_assigned_identity": dataSourceArmUserAssignedIdentity(), - "azurerm_virtual_hub": dataSourceArmVirtualHub(), - "azurerm_virtual_machine": dataSourceArmVirtualMachine(), - "azurerm_virtual_network_gateway": dataSourceArmVirtualNetworkGateway(), - "azurerm_virtual_network_gateway_connection": dataSourceArmVirtualNetworkGatewayConnection(), - "azurerm_virtual_network": dataSourceArmVirtualNetwork(), + "azurerm_api_management": dataSourceApiManagementService(), + "azurerm_api_management_api": dataSourceApiManagementApi(), + "azurerm_api_management_group": dataSourceApiManagementGroup(), + "azurerm_api_management_product": dataSourceApiManagementProduct(), + "azurerm_api_management_user": dataSourceArmApiManagementUser(), + "azurerm_app_service_plan": dataSourceAppServicePlan(), + "azurerm_app_service_certificate": dataSourceAppServiceCertificate(), + "azurerm_app_service": dataSourceArmAppService(), + "azurerm_app_service_certificate_order": dataSourceArmAppServiceCertificateOrder(), + "azurerm_application_insights": dataSourceArmApplicationInsights(), + "azurerm_application_security_group": dataSourceArmApplicationSecurityGroup(), + "azurerm_automation_account": dataSourceArmAutomationAccount(), + "azurerm_automation_variable_bool": dataSourceArmAutomationVariableBool(), + "azurerm_automation_variable_datetime": dataSourceArmAutomationVariableDateTime(), + "azurerm_automation_variable_int": dataSourceArmAutomationVariableInt(), + "azurerm_automation_variable_string": dataSourceArmAutomationVariableString(), + "azurerm_availability_set": dataSourceArmAvailabilitySet(), + "azurerm_azuread_application": dataSourceArmAzureADApplication(), + "azurerm_azuread_service_principal": dataSourceArmActiveDirectoryServicePrincipal(), + "azurerm_batch_account": dataSourceArmBatchAccount(), + "azurerm_batch_certificate": dataSourceArmBatchCertificate(), + "azurerm_batch_pool": dataSourceArmBatchPool(), + "azurerm_builtin_role_definition": dataSourceArmBuiltInRoleDefinition(), + "azurerm_cdn_profile": dataSourceArmCdnProfile(), + "azurerm_client_config": dataSourceArmClientConfig(), + "azurerm_kubernetes_service_versions": dataSourceArmKubernetesServiceVersions(), + "azurerm_container_registry": dataSourceArmContainerRegistry(), + "azurerm_cosmosdb_account": dataSourceArmCosmosDbAccount(), + "azurerm_data_factory": dataSourceArmDataFactory(), + "azurerm_data_lake_store": dataSourceArmDataLakeStoreAccount(), + "azurerm_dev_test_lab": dataSourceArmDevTestLab(), + "azurerm_dev_test_virtual_network": dataSourceArmDevTestVirtualNetwork(), + "azurerm_dns_zone": dataSourceArmDnsZone(), + "azurerm_eventhub_namespace": dataSourceEventHubNamespace(), + "azurerm_express_route_circuit": dataSourceArmExpressRouteCircuit(), + "azurerm_firewall": dataSourceArmFirewall(), + "azurerm_image": dataSourceArmImage(), + "azurerm_hdinsight_cluster": dataSourceArmHDInsightSparkCluster(), + "azurerm_healthcare_service": dataSourceArmHealthcareService(), + "azurerm_maps_account": dataSourceArmMapsAccount(), + "azurerm_key_vault_access_policy": dataSourceArmKeyVaultAccessPolicy(), + "azurerm_key_vault_key": dataSourceArmKeyVaultKey(), + "azurerm_key_vault_secret": dataSourceArmKeyVaultSecret(), + "azurerm_key_vault": dataSourceArmKeyVault(), + "azurerm_kubernetes_cluster": dataSourceArmKubernetesCluster(), + "azurerm_lb": dataSourceArmLoadBalancer(), + "azurerm_lb_backend_address_pool": dataSourceArmLoadBalancerBackendAddressPool(), + "azurerm_log_analytics_workspace": dataSourceLogAnalyticsWorkspace(), + "azurerm_logic_app_workflow": dataSourceArmLogicAppWorkflow(), + "azurerm_managed_disk": dataSourceArmManagedDisk(), + "azurerm_management_group": dataSourceArmManagementGroup(), + "azurerm_monitor_action_group": dataSourceArmMonitorActionGroup(), + "azurerm_monitor_diagnostic_categories": dataSourceArmMonitorDiagnosticCategories(), + "azurerm_monitor_log_profile": dataSourceArmMonitorLogProfile(), + "azurerm_mssql_elasticpool": dataSourceArmMsSqlElasticpool(), + "azurerm_netapp_account": dataSourceArmNetAppAccount(), + "azurerm_netapp_pool": dataSourceArmNetAppPool(), + "azurerm_network_ddos_protection_plan": dataSourceNetworkDDoSProtectionPlan(), + "azurerm_network_interface": dataSourceArmNetworkInterface(), + "azurerm_network_security_group": dataSourceArmNetworkSecurityGroup(), + "azurerm_network_watcher": dataSourceArmNetworkWatcher(), + "azurerm_notification_hub_namespace": dataSourceNotificationHubNamespace(), + "azurerm_notification_hub": dataSourceNotificationHub(), + "azurerm_platform_image": dataSourceArmPlatformImage(), + "azurerm_policy_definition": dataSourceArmPolicyDefinition(), + "azurerm_postgresql_server": dataSourcePostgreSqlServer(), + "azurerm_private_link_service": dataSourceArmPrivateLinkService(), + "azurerm_private_link_service_endpoint_connections": dataSourceArmPrivateLinkServiceEndpointConnections(), + "azurerm_proximity_placement_group": dataSourceArmProximityPlacementGroup(), + "azurerm_public_ip": dataSourceArmPublicIP(), + "azurerm_public_ips": dataSourceArmPublicIPs(), + "azurerm_public_ip_prefix": dataSourceArmPublicIpPrefix(), + "azurerm_recovery_services_vault": dataSourceArmRecoveryServicesVault(), + "azurerm_recovery_services_protection_policy_vm": dataSourceArmRecoveryServicesProtectionPolicyVm(), + "azurerm_redis_cache": dataSourceArmRedisCache(), + "azurerm_resources": dataSourceArmResources(), + "azurerm_resource_group": dataSourceArmResourceGroup(), + "azurerm_role_definition": dataSourceArmRoleDefinition(), + "azurerm_route_table": dataSourceArmRouteTable(), + "azurerm_scheduler_job_collection": dataSourceArmSchedulerJobCollection(), + "azurerm_servicebus_namespace": dataSourceArmServiceBusNamespace(), + "azurerm_servicebus_namespace_authorization_rule": dataSourceArmServiceBusNamespaceAuthorizationRule(), + "azurerm_shared_image_gallery": dataSourceArmSharedImageGallery(), + "azurerm_shared_image_version": dataSourceArmSharedImageVersion(), + "azurerm_shared_image": dataSourceArmSharedImage(), + "azurerm_snapshot": dataSourceArmSnapshot(), + "azurerm_sql_server": dataSourceSqlServer(), + "azurerm_sql_database": dataSourceSqlDatabase(), + "azurerm_stream_analytics_job": dataSourceArmStreamAnalyticsJob(), + "azurerm_storage_account_blob_container_sas": dataSourceArmStorageAccountBlobContainerSharedAccessSignature(), + "azurerm_storage_account_sas": dataSourceArmStorageAccountSharedAccessSignature(), + "azurerm_storage_account": dataSourceArmStorageAccount(), + "azurerm_storage_management_policy": dataSourceArmStorageManagementPolicy(), + "azurerm_subnet": dataSourceArmSubnet(), + "azurerm_subscription": dataSourceArmSubscription(), + "azurerm_subscriptions": dataSourceArmSubscriptions(), + "azurerm_traffic_manager_geographical_location": dataSourceArmTrafficManagerGeographicalLocation(), + "azurerm_user_assigned_identity": dataSourceArmUserAssignedIdentity(), + "azurerm_virtual_hub": dataSourceArmVirtualHub(), + "azurerm_virtual_machine": dataSourceArmVirtualMachine(), + "azurerm_virtual_network_gateway": dataSourceArmVirtualNetworkGateway(), + "azurerm_virtual_network_gateway_connection": dataSourceArmVirtualNetworkGatewayConnection(), + "azurerm_virtual_network": dataSourceArmVirtualNetwork(), } resources := map[string]*schema.Resource{ @@ -393,6 +395,7 @@ func Provider() terraform.ResourceProvider { "azurerm_private_dns_ptr_record": resourceArmPrivateDnsPtrRecord(), "azurerm_private_dns_srv_record": resourceArmPrivateDnsSrvRecord(), "azurerm_private_dns_zone_virtual_network_link": resourceArmPrivateDnsZoneVirtualNetworkLink(), + "azurerm_private_link_service": resourceArmPrivateLinkService(), "azurerm_proximity_placement_group": resourceArmProximityPlacementGroup(), "azurerm_public_ip": resourceArmPublicIp(), "azurerm_public_ip_prefix": resourceArmPublicIpPrefix(), diff --git a/azurerm/resource_arm_loadbalancer.go b/azurerm/resource_arm_loadbalancer.go index 2a0664d57e8e..540ccd6c2cca 100644 --- a/azurerm/resource_arm_loadbalancer.go +++ b/azurerm/resource_arm_loadbalancer.go @@ -143,6 +143,11 @@ func resourceArmLoadBalancer() *schema.Resource { }, "zones": azure.SchemaSingleZone(), + + "id": { + Type: schema.TypeString, + Computed: true, + }, }, }, }, @@ -369,6 +374,10 @@ func flattenLoadBalancerFrontendIpConfiguration(ipConfigs *[]network.FrontendIPC ipConfig["name"] = *config.Name } + if config.ID != nil { + ipConfig["id"] = *config.ID + } + zones := make([]string, 0) if zs := config.Zones; zs != nil { zones = *zs diff --git a/azurerm/resource_arm_private_link_service.go b/azurerm/resource_arm_private_link_service.go new file mode 100644 index 000000000000..6abd064bdfc1 --- /dev/null +++ b/azurerm/resource_arm_private_link_service.go @@ -0,0 +1,456 @@ +package azurerm + +import ( + "context" + "fmt" + "log" + "time" + + "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/response" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/validate" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + aznet "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func resourceArmPrivateLinkService() *schema.Resource { + return &schema.Resource{ + Create: resourceArmPrivateLinkServiceCreateUpdate, + Read: resourceArmPrivateLinkServiceRead, + Update: resourceArmPrivateLinkServiceCreateUpdate, + Delete: resourceArmPrivateLinkServiceDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznet.ValidatePrivateLinkServiceName, + }, + + "location": azure.SchemaLocation(), + + "resource_group_name": azure.SchemaResourceGroupName(), + + "auto_approval_subscription_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validate.GUID, + }, + Set: schema.HashString, + }, + + "visibility_subscription_ids": { + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validate.GUID, + }, + Set: schema.HashString, + }, + + // currently not implemented yet, timeline unknown, exact purpose unknown, maybe coming to a future API near you + // "fqdns": { + // Type: schema.TypeList, + // Optional: true, + // Elem: &schema.Schema{ + // Type: schema.TypeString, + // ValidateFunc: validate.NoEmptyStrings, + // }, + // }, + + // Required by the API you can't create the resource without at least + // one ip configuration once primary is set it is set forever unless + // you destroy the resource and recreate it. + "nat_ip_configuration": { + Type: schema.TypeList, + Required: true, + MaxItems: 8, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: aznet.ValidatePrivateLinkServiceName, + }, + "private_ip_address": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validate.IPv4Address, + }, + // Only IPv4 is supported by the API, but I am exposing this + // as they will support IPv6 in a future release. + "private_ip_address_version": { + Type: schema.TypeString, + Optional: true, + ValidateFunc: validation.StringInSlice([]string{ + string(network.IPv4), + }, false), + Default: string(network.IPv4), + }, + "subnet_id": { + Type: schema.TypeString, + Required: true, + ValidateFunc: azure.ValidateResourceID, + }, + "primary": { + Type: schema.TypeBool, + Required: true, + ForceNew: true, + }, + }, + }, + }, + + // private_endpoint_connections have been removed and placed inside the + // azurerm_private_link_service_endpoint_connections datasource. + + // Required by the API you can't create the resource without at least one load balancer id + "load_balancer_frontend_ip_configuration_ids": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: azure.ValidateResourceID, + }, + Set: schema.HashString, + }, + + "alias": { + Type: schema.TypeString, + Computed: true, + }, + + "network_interface_ids": { + Type: schema.TypeSet, + Computed: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: azure.ValidateResourceID, + }, + Set: schema.HashString, + }, + + "tags": tags.Schema(), + }, + + CustomizeDiff: func(d *schema.ResourceDiff, v interface{}) error { + if err := aznet.ValidatePrivateLinkNatIpConfiguration(d); err != nil { + return err + } + + return nil + }, + } +} + +func resourceArmPrivateLinkServiceCreateUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.PrivateLinkServiceClient + ctx := meta.(*ArmClient).StopContext + + name := d.Get("name").(string) + resourceGroup := d.Get("resource_group_name").(string) + + if features.ShouldResourcesBeImported() && d.IsNewResource() { + existing, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + if !utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("Error checking for presence of existing Private Link Service %q (Resource Group %q): %s", name, resourceGroup, err) + } + } + if existing.ID != nil && *existing.ID != "" { + return tf.ImportAsExistsError("azurerm_private_link_service", *existing.ID) + } + } + + location := azure.NormalizeLocation(d.Get("location").(string)) + autoApproval := d.Get("auto_approval_subscription_ids").(*schema.Set) + // currently not implemented yet, timeline unknown, exact purpose unknown, maybe coming to a future API near you + //fqdns := d.Get("fqdns").([]interface{}) + primaryIpConfiguration := d.Get("nat_ip_configuration").([]interface{}) + loadBalancerFrontendIpConfigurations := d.Get("load_balancer_frontend_ip_configuration_ids").(*schema.Set) + visibility := d.Get("visibility_subscription_ids").(*schema.Set) + t := d.Get("tags").(map[string]interface{}) + + parameters := network.PrivateLinkService{ + Location: utils.String(location), + PrivateLinkServiceProperties: &network.PrivateLinkServiceProperties{ + AutoApproval: &network.PrivateLinkServicePropertiesAutoApproval{ + Subscriptions: utils.ExpandStringSlice(autoApproval.List()), + }, + Visibility: &network.PrivateLinkServicePropertiesVisibility{ + Subscriptions: utils.ExpandStringSlice(visibility.List()), + }, + IPConfigurations: expandArmPrivateLinkServiceIPConfiguration(primaryIpConfiguration), + LoadBalancerFrontendIPConfigurations: expandArmPrivateLinkServiceFrontendIPConfiguration(loadBalancerFrontendIpConfigurations), + //Fqdns: utils.ExpandStringSlice(fqdns), + }, + Tags: tags.Expand(t), + } + + future, err := client.CreateOrUpdate(ctx, resourceGroup, name, parameters) + if err != nil { + return fmt.Errorf("Error creating Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("Error waiting for creation of Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + resp, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + return fmt.Errorf("Error retrieving Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + if resp.ID == nil || *resp.ID == "" { + return fmt.Errorf("API returns a nil/empty id on Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + // we can't rely on the use of the Future here due to the resource being successfully completed but now the service is applying those values. + log.Printf("[DEBUG] Waiting for Private Link Service to %q (Resource Group %q) to finish applying", name, resourceGroup) + stateConf := &resource.StateChangeConf{ + Pending: []string{"Pending", "Updating", "Creating"}, + Target: []string{"Succeeded"}, + Refresh: privateLinkServiceWaitForReadyRefreshFunc(ctx, client, resourceGroup, name), + Timeout: 60 * time.Minute, + MinTimeout: 15 * time.Second, + } + if _, err := stateConf.WaitForState(); err != nil { + return fmt.Errorf("Error waiting for Private Link Service %q (Resource Group %q) to complete: %s", name, resourceGroup, err) + } + + d.SetId(*resp.ID) + + return resourceArmPrivateLinkServiceRead(d, meta) +} + +func resourceArmPrivateLinkServiceRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.PrivateLinkServiceClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + name := id.Path["privateLinkServices"] + + resp, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + if utils.ResponseWasNotFound(resp.Response) { + log.Printf("[INFO] Private Link Service %q does not exist - removing from state", d.Id()) + d.SetId("") + return nil + } + return fmt.Errorf("Error reading Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + d.Set("name", resp.Name) + d.Set("resource_group_name", resourceGroup) + d.Set("location", azure.NormalizeLocation(*resp.Location)) + + if props := resp.PrivateLinkServiceProperties; props != nil { + d.Set("alias", props.Alias) + if err := d.Set("auto_approval_subscription_ids", utils.FlattenStringSlice(props.AutoApproval.Subscriptions)); err != nil { + return fmt.Errorf("Error setting `auto_approval_subscription_ids`: %+v", err) + } + if err := d.Set("visibility_subscription_ids", utils.FlattenStringSlice(props.Visibility.Subscriptions)); err != nil { + return fmt.Errorf("Error setting `visibility_subscription_ids`: %+v", err) + } + // currently not implemented yet, timeline unknown, exact purpose unknown, maybe coming to a future API near you + // if props.Fqdns != nil { + // if err := d.Set("fqdns", utils.FlattenStringSlice(props.Fqdns)); err != nil { + // return fmt.Errorf("Error setting `fqdns`: %+v", err) + // } + // } + if err := d.Set("nat_ip_configuration", flattenArmPrivateLinkServiceIPConfiguration(props.IPConfigurations)); err != nil { + return fmt.Errorf("Error setting `nat_ip_configuration`: %+v", err) + } + if err := d.Set("load_balancer_frontend_ip_configuration_ids", flattenArmPrivateLinkServiceFrontendIPConfiguration(props.LoadBalancerFrontendIPConfigurations)); err != nil { + return fmt.Errorf("Error setting `load_balancer_frontend_ip_configuration_ids`: %+v", err) + } + if err := d.Set("network_interface_ids", flattenArmPrivateLinkServiceInterface(props.NetworkInterfaces)); err != nil { + return fmt.Errorf("Error setting `network_interface_ids`: %+v", err) + } + } + + return tags.FlattenAndSet(d, resp.Tags) +} + +func resourceArmPrivateLinkServiceDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*ArmClient).Network.PrivateLinkServiceClient + ctx := meta.(*ArmClient).StopContext + + id, err := azure.ParseAzureResourceID(d.Id()) + if err != nil { + return err + } + resourceGroup := id.ResourceGroup + name := id.Path["privateLinkServices"] + + future, err := client.Delete(ctx, resourceGroup, name) + if err != nil { + if response.WasNotFound(future.Response()) { + return nil + } + return fmt.Errorf("Error deleting Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + if !response.WasNotFound(future.Response()) { + return fmt.Errorf("Error waiting for deleting Private Link Service %q (Resource Group %q): %+v", name, resourceGroup, err) + } + } + + return nil +} + +func expandArmPrivateLinkServiceIPConfiguration(input []interface{}) *[]network.PrivateLinkServiceIPConfiguration { + if len(input) == 0 { + return nil + } + + results := make([]network.PrivateLinkServiceIPConfiguration, 0) + + for _, item := range input { + v := item.(map[string]interface{}) + privateIpAddress := v["private_ip_address"].(string) + subnetId := v["subnet_id"].(string) + privateIpAddressVersion := v["private_ip_address_version"].(string) + name := v["name"].(string) + primary := v["primary"].(bool) + + result := network.PrivateLinkServiceIPConfiguration{ + Name: utils.String(name), + PrivateLinkServiceIPConfigurationProperties: &network.PrivateLinkServiceIPConfigurationProperties{ + PrivateIPAddress: utils.String(privateIpAddress), + PrivateIPAddressVersion: network.IPVersion(privateIpAddressVersion), + Subnet: &network.Subnet{ + ID: utils.String(subnetId), + }, + Primary: utils.Bool(primary), + }, + } + + if privateIpAddress != "" { + result.PrivateLinkServiceIPConfigurationProperties.PrivateIPAllocationMethod = network.IPAllocationMethod("Static") + } else { + result.PrivateLinkServiceIPConfigurationProperties.PrivateIPAllocationMethod = network.IPAllocationMethod("Dynamic") + } + + results = append(results, result) + } + + return &results +} + +func expandArmPrivateLinkServiceFrontendIPConfiguration(input *schema.Set) *[]network.FrontendIPConfiguration { + ids := input.List() + if len(ids) == 0 { + return nil + } + + results := make([]network.FrontendIPConfiguration, 0) + + for _, item := range ids { + result := network.FrontendIPConfiguration{ + ID: utils.String(item.(string)), + } + + results = append(results, result) + } + + return &results +} + +func flattenArmPrivateLinkServiceIPConfiguration(input *[]network.PrivateLinkServiceIPConfiguration) []interface{} { + results := make([]interface{}, 0) + if input == nil { + return results + } + + for _, item := range *input { + c := make(map[string]interface{}) + + if name := item.Name; name != nil { + c["name"] = *name + } + if props := item.PrivateLinkServiceIPConfigurationProperties; props != nil { + if v := props.PrivateIPAddress; v != nil { + c["private_ip_address"] = *v + } + c["private_ip_address_version"] = string(props.PrivateIPAddressVersion) + if v := props.Subnet; v != nil { + if i := v.ID; i != nil { + c["subnet_id"] = *i + } + } + if v := props.Primary; v != nil { + c["primary"] = *v + } + } + + results = append(results, c) + } + + return results +} + +func flattenArmPrivateLinkServiceFrontendIPConfiguration(input *[]network.FrontendIPConfiguration) *schema.Set { + results := &schema.Set{F: schema.HashString} + if input == nil { + return results + } + + for _, item := range *input { + if id := item.ID; id != nil { + results.Add(*id) + } + } + + return results +} + +func flattenArmPrivateLinkServiceInterface(input *[]network.Interface) *schema.Set { + results := &schema.Set{F: schema.HashString} + if input == nil { + return results + } + + for _, item := range *input { + if id := item.ID; id != nil { + results.Add(*id) + } + } + + return results +} + +func privateLinkServiceWaitForReadyRefreshFunc(ctx context.Context, client *network.PrivateLinkServicesClient, resourceGroupName string, name string) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + res, err := client.Get(ctx, resourceGroupName, name, "") + if err != nil { + return nil, "Error", fmt.Errorf("Error issuing read request in privateLinkServiceWaitForReadyRefreshFunc %q (Resource Group %q): %s", name, resourceGroupName, err) + } + if props := res.PrivateLinkServiceProperties; props != nil { + if state := props.ProvisioningState; state != "" { + return res, string(state), nil + } + } + + return res, "Pending", nil + } +} diff --git a/azurerm/resource_arm_private_link_service_test.go b/azurerm/resource_arm_private_link_service_test.go new file mode 100644 index 000000000000..dcc51cf4348c --- /dev/null +++ b/azurerm/resource_arm_private_link_service_test.go @@ -0,0 +1,700 @@ +package azurerm + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" +) + +func TestAccAzureRMPrivateLinkService_basic(t *testing.T) { + resourceName := "azurerm_private_link_service.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateLinkServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateLinkService_basic(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "name", fmt.Sprintf("acctestPLS-%d", ri)), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMPrivateLinkService_update(t *testing.T) { + resourceName := "azurerm_private_link_service.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateLinkServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateLinkService_basicIp(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_update(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.primary", "true"), + resource.TestCheckResourceAttr(resourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.env", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_basicIp(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMPrivateLinkService_move(t *testing.T) { + resourceName := "azurerm_private_link_service.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateLinkServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateLinkService_moveSetup(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "1"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.17"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_moveAdd(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.17"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.18"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.2.private_ip_address", "10.5.1.19"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.3.private_ip_address", "10.5.1.20"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_moveChangeOne(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.17"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.18"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.2.private_ip_address", "10.5.1.19"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.3.private_ip_address", "10.5.1.21"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_moveChangeTwo(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.17"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.20"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.2.private_ip_address", "10.5.1.19"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.3.private_ip_address", "10.5.1.21"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + Config: testAccAzureRMPrivateLinkService_moveChangeThree(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.17"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.20"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.2.private_ip_address", "10.5.1.19"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.3.private_ip_address", "10.5.1.18"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func TestAccAzureRMPrivateLinkService_complete(t *testing.T) { + resourceName := "azurerm_private_link_service.test" + ri := tf.AccRandTimeInt() + location := testLocation() + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testCheckAzureRMPrivateLinkServiceDestroy, + Steps: []resource.TestStep{ + { + Config: testAccAzureRMPrivateLinkService_complete(ri, location), + Check: resource.ComposeTestCheckFunc( + testCheckAzureRMPrivateLinkServiceExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "auto_approval_subscription_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "visibility_subscription_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.#", "2"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address", "10.5.1.40"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.0.private_ip_address_version", "IPv4"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address", "10.5.1.41"), + resource.TestCheckResourceAttr(resourceName, "nat_ip_configuration.1.private_ip_address_version", "IPv4"), + resource.TestCheckResourceAttr(resourceName, "load_balancer_frontend_ip_configuration_ids.#", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.%", "1"), + resource.TestCheckResourceAttr(resourceName, "tags.env", "test"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testCheckAzureRMPrivateLinkServiceExists(resourceName string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + if !ok { + return fmt.Errorf("Private Link Service not found: %s", resourceName) + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + client := testAccProvider.Meta().(*ArmClient).Network.PrivateLinkServiceClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + if resp, err := client.Get(ctx, resourceGroup, name, ""); err != nil { + if utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Private Link Service %q (Resource Group %q) does not exist", name, resourceGroup) + } + return fmt.Errorf("Bad: Get on network.PrivateLinkServiceClient: %+v", err) + } + + return nil + } +} + +func testCheckAzureRMPrivateLinkServiceDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*ArmClient).Network.PrivateLinkServiceClient + ctx := testAccProvider.Meta().(*ArmClient).StopContext + + for _, rs := range s.RootModule().Resources { + if rs.Type != "azurerm_private_link_service" { + continue + } + + name := rs.Primary.Attributes["name"] + resourceGroup := rs.Primary.Attributes["resource_group_name"] + + if resp, err := client.Get(ctx, resourceGroup, name, ""); err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Bad: Get on network.PrivateLinkServiceClient: %+v", err) + } + } + + return nil + } + + return nil +} + +func testAccAzureRMPrivateLinkService_basic(rInt int, location string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_private_link_service" "test" { + name = "acctestPLS-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + nat_ip_configuration { + name = "primaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + primary = true + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_basicIp(rInt int, location string) string { + return fmt.Sprintf(` +%s + +resource "azurerm_private_link_service" "test" { + name = "acctestPLS-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + nat_ip_configuration { + name = "primaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.30" + private_ip_address_version = "IPv4" + primary = true + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_update(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.30" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.22" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "thirdaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.23" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "fourtharyIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.24" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_moveSetup(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + primary = true + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_moveAdd(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.18" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "thirdaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.19" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "fourtharyIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.20" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_moveChangeOne(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.18" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "thirdaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.19" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "fourtharyIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.21" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_moveChangeTwo(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.20" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "thirdaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.19" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "fourtharyIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.21" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_moveChangeThree(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.20" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "thirdaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.19" + private_ip_address_version = "IPv4" + primary = false + } + + nat_ip_configuration { + name = "fourtharyIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.18" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkService_complete(rInt int, location string) string { + return fmt.Sprintf(` +%s + +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" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.40" + private_ip_address_version = "IPv4" + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration-%d" + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.41" + private_ip_address_version = "IPv4" + primary = false + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} +`, testAccAzureRMPrivateLinkServiceTemplate(rInt, location), rInt, rInt, rInt) +} + +func testAccAzureRMPrivateLinkServiceTemplate(rInt int, location string) string { + return fmt.Sprintf(` +data "azurerm_subscription" "current" {} + +resource "azurerm_resource_group" "test" { + name = "acctestRG-privatelinkservice-%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" "test" { + name = "acctestsnet-%d" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.5.1.0/24" + + enforce_private_link_service_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 + } +} +`, rInt, location, rInt, rInt, rInt, rInt) +} diff --git a/azurerm/resource_arm_subnet.go b/azurerm/resource_arm_subnet.go index c6e03711f172..57b01c227c9e 100644 --- a/azurerm/resource_arm_subnet.go +++ b/azurerm/resource_arm_subnet.go @@ -3,6 +3,7 @@ package azurerm import ( "fmt" "log" + "strings" "time" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-07-01/network" @@ -138,6 +139,12 @@ func resourceArmSubnet() *schema.Resource { }, }, }, + + "enforce_private_link_service_network_policies": { + Type: schema.TypeBool, + Optional: true, + Default: false, + }, }, } } @@ -175,6 +182,15 @@ func resourceArmSubnetCreateUpdate(d *schema.ResourceData, meta interface{}) err AddressPrefix: &addressPrefix, } + if v, ok := d.GetOk("enforce_private_link_service_network_policies"); ok { + // To enable private endpoints you must disable the network policies for the + // subnet because Network policies like network security groups are not + // supported by private endpoints. + if v.(bool) { + properties.PrivateLinkServiceNetworkPolicies = utils.String("Disabled") + } + } + if v, ok := d.GetOk("network_security_group_id"); ok { nsgId := v.(string) properties.NetworkSecurityGroup = &network.SecurityGroup{ @@ -272,6 +288,14 @@ func resourceArmSubnetRead(d *schema.ResourceData, meta interface{}) error { if props := resp.SubnetPropertiesFormat; props != nil { d.Set("address_prefix", props.AddressPrefix) + if p := props.PrivateLinkServiceNetworkPolicies; p != nil { + // To enable private endpoints you must disable the network policies for the + // subnet because Network policies like network security groups are not + // supported by private endpoints. + + d.Set("enforce_private_link_service_network_policies", strings.EqualFold("Disabled", *p)) + } + var securityGroupId *string if props.NetworkSecurityGroup != nil { securityGroupId = props.NetworkSecurityGroup.ID diff --git a/azurerm/utils/split.go b/azurerm/utils/split.go new file mode 100644 index 000000000000..500ab64e9f82 --- /dev/null +++ b/azurerm/utils/split.go @@ -0,0 +1,22 @@ +package utils + +import ( + "strings" +) + +func SplitRemoveEmptyEntries(input string, delimiter string, removeWhitespace bool) []string { + result := make([]string, 0) + + s := strings.Split(input, delimiter) + + for _, v := range s { + if removeWhitespace { + v = strings.TrimSpace(v) + } + if len(v) > 0 { + result = append(result, v) + } + } + + return result +} diff --git a/examples/virtual-networks/private-link-service/README.md b/examples/virtual-networks/private-link-service/README.md new file mode 100644 index 000000000000..83c9b22fb80d --- /dev/null +++ b/examples/virtual-networks/private-link-service/README.md @@ -0,0 +1,3 @@ +## Example: Private Link Service + + This example provisions a Private Link Service. \ No newline at end of file diff --git a/examples/virtual-networks/private-link-service/main.tf b/examples/virtual-networks/private-link-service/main.tf new file mode 100644 index 000000000000..9aed70b3a642 --- /dev/null +++ b/examples/virtual-networks/private-link-service/main.tf @@ -0,0 +1,59 @@ +resource "azurerm_resource_group" "test" { + name = var.resource_group_name + location = var.location +} + +resource "azurerm_virtual_network" "test" { + name = "acctestvnet" + resource_group_name = azurerm_resource_group.test.name + location = azurerm_resource_group.test.location + address_space = ["10.5.0.0/16"] +} + +resource "azurerm_subnet" "test" { + name = "acctestsnet" + resource_group_name = azurerm_resource_group.test.name + virtual_network_name = azurerm_virtual_network.test.name + address_prefix = "10.5.1.0/24" + private_link_service_network_policies = "Disabled" +} + +resource "azurerm_public_ip" "test" { + name = "acctestpip" + 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" + 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 + } +} + +resource "azurerm_private_link_service" "test" { + name = "acctestpls" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + + nat_ip_configuration { + name = azurerm_public_ip.test.name + subnet_id = azurerm_subnet.test.id + private_ip_address = "10.5.1.17" + } + + load_balancer_frontend_ip_configuration_ids = [ + azurerm_lb.test.frontend_ip_configuration.0.id + ] + + tags = { + env = "test" + } +} diff --git a/examples/virtual-networks/private-link-service/variables.tf b/examples/virtual-networks/private-link-service/variables.tf new file mode 100644 index 000000000000..9a3398bc66d8 --- /dev/null +++ b/examples/virtual-networks/private-link-service/variables.tf @@ -0,0 +1,9 @@ +variable "resource_group_name" { + description = "The name of the resource group the Private Link Service is located in." + default = "example-private-link-service" +} + +variable "location" { + description = "The Azure location where all resources in this example should be created." + default = "WestUS" +} diff --git a/website/azurerm.erb b/website/azurerm.erb index 2b41444a4fc3..dc44b2e30a49 100644 --- a/website/azurerm.erb +++ b/website/azurerm.erb @@ -442,6 +442,14 @@ azurerm_virtual_network +