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 +
  • + azurerm_private_link_service_endpoint_connections +
  • + +
  • + azurerm_private_link_service +
  • +
  • azurerm_virtual_network_gateway
  • @@ -1691,6 +1699,10 @@ azurerm_web_application_firewall_policy +
  • + azurerm_private_link_service +
  • + > azurerm_virtual_wan diff --git a/website/docs/d/loadbalancer.html.markdown b/website/docs/d/loadbalancer.html.markdown index ecfcada622f9..12e404b3e8d0 100644 --- a/website/docs/d/loadbalancer.html.markdown +++ b/website/docs/d/loadbalancer.html.markdown @@ -54,6 +54,7 @@ The following attributes are exported: A `frontend_ip_configuration` block exports the following: * `name` - The name of the Frontend IP Configuration. +* `id` - The id of the Frontend IP Configuration. * `subnet_id` - The ID of the Subnet which is associated with the IP Configuration. * `private_ip_address` - Private IP Address to assign to the Load Balancer. * `private_ip_address_allocation` - The allocation method for the Private IP Address used by this Load Balancer. diff --git a/website/docs/d/private_link_service.html.markdown b/website/docs/d/private_link_service.html.markdown new file mode 100644 index 000000000000..cc4cda255f43 --- /dev/null +++ b/website/docs/d/private_link_service.html.markdown @@ -0,0 +1,72 @@ +--- +subcategory: "" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_private_link_service" +sidebar_current: "docs-azurerm-datasource-private-link-service" +description: |- + Use this data source to access information about an existing Private Link Service. +--- + +# Data Source: azurerm_private_link_service + +Use this data source to access information about an existing Private Link Service. + + +## Private Link Service Usage + +```hcl +data "azurerm_private_link_service" "example" { + name = "myPrivateLinkService" + resource_group_name = "PrivateLinkServiceRG" +} + +output "private_link_service_id" { + value = "${data.azurerm_private_link_service.example.id}" +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the private link service. + +* `resource_group_name` - (Required) The name of the resource group in which the private link service resides. + + +## Attributes Reference + +The following attributes are exported: + +* `location` - The supported Azure location where the resource exists. + +* `alias` - The alias is a globally unique name for your private link service which Azure generates for you. Your can use this alias to request a connection to your private link service. + +* `auto_approval_subscription_ids` - The list of subscription(s) globally unique identifiers that will be auto approved to use the private link service. + +* `visibility_subscription_ids` - The list of subscription(s) globally unique identifiers(GUID) that will be able to see the private link service. + +* `nat_ip_configuration` - The `nat_ip_configuration` block as defined below. + +* `load_balancer_frontend_ip_configuration_ids` - The list of Standard Load Balancer(SLB) resource IDs. The Private Link service is tied to the frontend IP address of a SLB. All traffic destined for the private link service will reach the frontend of the SLB. You can configure SLB rules to direct this traffic to appropriate backend pools where your applications are running. + +* `network_interfaces` - The list of network interface resource ids that are being used by the service. + +* `tags` - A mapping of tags to assign to the resource. + + +--- + +The `nat_ip_configuration` block exports the following: + +* `name` - The name of private link service NAT IP configuration. + +* `private_ip_address` - The private IP address of the NAT IP configuration. + +* `private_ip_address_version` - The ip address version of the `ip_configuration`. + +* `subnet_id` - The resource ID of the subnet to be used by the service. + +* `primary` - Value that indicates if the IP configuration is the primary configuration or not. + diff --git a/website/docs/d/private_link_service_endpoint_connections.html.markdown b/website/docs/d/private_link_service_endpoint_connections.html.markdown new file mode 100644 index 000000000000..e4f62be9a06e --- /dev/null +++ b/website/docs/d/private_link_service_endpoint_connections.html.markdown @@ -0,0 +1,54 @@ +--- +subcategory: "" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_private_link_service_endpoint_connections" +sidebar_current: "docs-azurerm-datasource-private-link-service-endpoint-connections" +description: |- + Use this data source to access endpoint connection information about an existing Private Link Service. +--- + +# Data Source: azurerm_private_link_service_endpoint_connections + +Use this data source to access endpoint connection information about an existing Private Link Service. + + +## Private Link Service Endpoint Connections Usage + +```hcl +data "azurerm_private_link_service_endpoint_connections" "example" { + name = azurerm_private_link_service.example.name + resource_group_name = azurerm_resource_group.example.name +} + +output "private_endpoint_status" { + value = data.azurerm_private_link_service_endpoint_connections.example.private_endpoint_connections.0.status +} +``` + + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the private link service. + +* `resource_group_name` - (Required) The name of the resource group in which the private link service resides. + + +## Attributes Reference + +The `private_endpoint_connections` block exports the following: + +* `connection_id` - The resource id of the private link service connection between the private link service and the private link endpoint. + +* `connection_name` - The name of the connection between the private link service and the private link endpoint. + +* `private_endpoint_id` - The resource id of the private link endpoint. + +* `private_endpoint_name` - The name of the private link endpoint. + +* `action_required` - A message indicating if changes on the service provider require any updates or not. + +* `description` - The request for approval message or the reason for rejection message. + +* `status` - Indicates the state of the connection between the private link service and the private link endpoint, possible values are `Pending`, `Approved` or `Rejected`. diff --git a/website/docs/d/subnet.html.markdown b/website/docs/d/subnet.html.markdown index 323347081057..780b0775f4b1 100644 --- a/website/docs/d/subnet.html.markdown +++ b/website/docs/d/subnet.html.markdown @@ -35,6 +35,7 @@ output "subnet_id" { * `id` - The ID of the Subnet. * `address_prefix` - The address prefix used for the subnet. +* `enforce_private_link_service_network_policies` - Enable or Disable network policies on private link service in the subnet. * `network_security_group_id` - The ID of the Network Security Group associated with the subnet. * `route_table_id` - The ID of the Route Table associated with this subnet. * `ip_configurations` - The collection of IP Configurations with IPs within this subnet. diff --git a/website/docs/r/loadbalancer.html.markdown b/website/docs/r/loadbalancer.html.markdown index d86f5875cb8c..cbde51cfee7b 100644 --- a/website/docs/r/loadbalancer.html.markdown +++ b/website/docs/r/loadbalancer.html.markdown @@ -69,6 +69,7 @@ The following attributes are exported: * `id` - The Load Balancer ID. * `private_ip_address` - The first private IP address assigned to the load balancer in `frontend_ip_configuration` blocks, if any. * `private_ip_addresses` - The list of private IP address assigned to the load balancer in `frontend_ip_configuration` blocks, if any. +* `id` - The id of the Frontend IP Configuration. ## Import diff --git a/website/docs/r/private_link_service.html.markdown b/website/docs/r/private_link_service.html.markdown new file mode 100644 index 000000000000..6748dcd74053 --- /dev/null +++ b/website/docs/r/private_link_service.html.markdown @@ -0,0 +1,138 @@ +--- +subcategory: "" +layout: "azurerm" +page_title: "Azure Resource Manager: azurerm_private_link_service" +sidebar_current: "docs-azurerm-resource-private-link-service" +description: |- + Manages an Azure Private Link Service. +--- + +# azurerm_private_link_service + +Manages an Azure Private Link Service. + + +## Private Link Service Usage + +```hcl +resource "azurerm_resource_group" "example" { + name = "exampleRG" + location = "Eastus2" +} + +resource "azurerm_virtual_network" "example" { + name = "example-avn" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + address_space = ["10.5.0.0/16"] +} + +resource "azurerm_subnet" "example" { + name = "example-snet" + resource_group_name = azurerm_resource_group.example.name + virtual_network_name = azurerm_virtual_network.example.name + address_prefix = "10.5.1.0/24" + enforce_private_link_service_network_policies = true +} + +resource "azurerm_public_ip" "example" { + name = "example-api" + sku = "Standard" + location = azurerm_resource_group.example.location + resource_group_name = azurerm_resource_group.example.name + allocation_method = "Static" +} + +resource "azurerm_lb" "example" { + name = "example-lb" + 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 = "myPrivateLinkService" + resource_group_name = azurerm_resource_group.example.name + location = azurerm_resource_group.example.location + + auto_approval_subscription_ids = ["00000000-0000-0000-0000-000000000000"] + visibility_subscription_ids = ["00000000-0000-0000-0000-000000000000"] + load_balancer_frontend_ip_configuration_ids = [azurerm_lb.example.frontend_ip_configuration.0.id] + + nat_ip_configuration { + name = "primaryIpConfiguration" + private_ip_address = "10.5.1.17" + private_ip_address_version = "IPv4" + subnet_id = azurerm_subnet.example.id + primary = true + } + + nat_ip_configuration { + name = "secondaryIpConfiguration" + private_ip_address = "10.5.1.18" + private_ip_address_version = "IPv4" + subnet_id = azurerm_subnet.example.id + primary = false + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `name` - (Required) The name of the private link service. Changing this forces a new resource to be created. + +* `resource_group_name` - (Required) The name of the resource group in which the private link service resides. Changing this forces a new resource to be created. + +* `location` - (Required) Specifies the supported Azure location where the resource exists. Changing this forces a new resource to be created. + +* `auto_approval_subscription_ids` - (Optional) A list of subscription globally unique identifiers(GUID) that will be automatically be able to use this service. + +* `visibility_subscription_ids` - (Optional) A list of subscription globally unique identifiers(GUID) that will be able to see this service. If left undefined all Azure subscriptions will be able to see this service. + +* `nat_ip_configuration` - (Required) A `nat_ip_configuration` block as defined below. + +* `load_balancer_frontend_ip_configuration_ids` - (Required) A list of Standard Load Balancer(SLB) resource IDs. The Private Link service is tied to the frontend IP address of a SLB. All traffic destined for the private link service will reach the frontend of the SLB. You can configure SLB rules to direct this traffic to appropriate backend pools where your applications are running. + +* `tags` - (Optional) A mapping of tags to assign to the resource. Changing this forces a new resource to be created. + +--- + +The `nat_ip_configuration` block supports the following: + +* `name` - (Required) The name of primary private link service NAT IP configuration. Changing this forces a new resource to be created. + +* `private_ip_address` - (Optional) The private IP address of the NAT IP configuration. + +* `private_ip_address_version` - (Optional) The ip address version of the `ip_configuration`, the supported value is `IPv4`. Defaults to `IPv4`. + +-> **NOTE:** Private Link Service Supports `IPv4` traffic only. + +* `subnet_id` - (Required) The resource ID of the subnet to be used by the service. + +-> **NOTE:** Verify that the subnets `enforce_private_link_service_network_policies` attribute is set to `true`. + +* `primary` - (Required) Specifies if the `nat_ip_configuration` block is the primary ip configuration for the service or not. Valid values are `true` or `false`. Changing this forces a new resource to be created. + +## Attributes Reference + +The following attributes are exported: + +* `alias` - The alias is a globally unique name for your private link service which Azure generates for you. Your can use this alias to request a connection to your private link service. + +* `network_interfaces` - A list of network interface resource ids that are being used by the service. + + +## Import + +Private Link Service can be imported using the `resource id`, e.g. + +```shell +$ terraform import azurerm_private_link_service.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/acctestRG/providers/Microsoft.Network/privateLinkServices/privatelinkservicename +``` diff --git a/website/docs/r/subnet.html.markdown b/website/docs/r/subnet.html.markdown index 9d0e3e2d3cb3..9e2dfd05318a 100644 --- a/website/docs/r/subnet.html.markdown +++ b/website/docs/r/subnet.html.markdown @@ -60,6 +60,10 @@ The following arguments are supported: * `address_prefix` - (Required) The address prefix to use for the subnet. +* `enforce_private_link_service_network_policies` - (Optional) Enable or Disable network policies on the `private link service` in the subnet. Default is `false`. + +-> **NOTE:** Network policies like network security groups (NSG) are not supported for the private link service. In order to deploy a private link service on a given subnet, an explicit disable setting is required on that subnet(e.g. `enforce_private_link_service_network_policies` = `true`). This setting is only applicable for the private link service. For other resources in the subnet, access is controlled based on Network Security Groups (NSG) security rules definition. + * `network_security_group_id` - (Optional / **Deprecated**) The ID of the Network Security Group to associate with the subnet. -> **NOTE:** At this time Subnet `<->` Network Security Group associations need to be configured both using this field (which is now Deprecated) and using the `azurerm_subnet_network_security_group_association` resource. This field is deprecated and will be removed in favour of that resource in the next major version (2.0) of the AzureRM Provider.