diff --git a/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go b/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go index e1a5e229ce27..3263425bc566 100644 --- a/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go +++ b/azurerm/internal/services/network/nat_gateway_public_ip_association_resource.go @@ -3,11 +3,11 @@ package network import ( "fmt" "log" + "strings" "time" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-03-01/network" "github.com/hashicorp/terraform-plugin-sdk/helper/schema" - "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" @@ -18,14 +18,14 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" ) -func resourceArmNatGatewayPublicIpAssociation() *schema.Resource { +func resourceArmNATGatewayPublicIpAssociation() *schema.Resource { return &schema.Resource{ - Create: resourceArmNatGatewayPublicIpAssociationCreate, - Read: resourceArmNatGatewayPublicIpAssociationRead, - Delete: resourceArmNatGatewayPublicIpAssociationDelete, + Create: resourceArmNATGatewayPublicIpAssociationCreate, + Read: resourceArmNATGatewayPublicIpAssociationRead, + Delete: resourceArmNATGatewayPublicIpAssociationDelete, Importer: azSchema.ValidateResourceIDPriorToImport(func(id string) error { - _, err := parse.NatGatewayID(id) + _, err := parse.NatGatewayPublicIPAddressAssociationID(id) return err }), @@ -47,18 +47,18 @@ func resourceArmNatGatewayPublicIpAssociation() *schema.Resource { Type: schema.TypeString, Required: true, ForceNew: true, - ValidateFunc: azure.ValidateResourceID, + ValidateFunc: validate.PublicIPAddressID, }, }, } } -func resourceArmNatGatewayPublicIpAssociationCreate(d *schema.ResourceData, meta interface{}) error { +func resourceArmNATGatewayPublicIpAssociationCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.NatGatewayClient ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() - log.Printf("[INFO] preparing arguments for Nat Gateway <-> Public Ip Association creation.") + log.Printf("[INFO] preparing arguments for NAT Gateway <-> Public IP Association creation.") natGatewayId := d.Get("nat_gateway_id").(string) publicIpAddressId := d.Get("public_ip_address_id").(string) parsedNatGatewayId, err := parse.NatGatewayID(natGatewayId) @@ -72,21 +72,24 @@ func resourceArmNatGatewayPublicIpAssociationCreate(d *schema.ResourceData, meta natGateway, err := client.Get(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, "") if err != nil { if utils.ResponseWasNotFound(natGateway.Response) { - return fmt.Errorf("Nat Gateway %q (Resource Group %q) was not found.", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup) + return fmt.Errorf("NAT Gateway %q (Resource Group %q) was not found.", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup) } - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) + return fmt.Errorf("failed to retrieve NAT Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) } + id := fmt.Sprintf("%s|%s", *natGateway.ID, publicIpAddressId) publicIpAddresses := make([]network.SubResource, 0) if natGateway.PublicIPAddresses != nil { for _, existingPublicIPAddress := range *natGateway.PublicIPAddresses { - if existingPublicIPAddress.ID != nil { - if *existingPublicIPAddress.ID == publicIpAddressId { - return tf.ImportAsExistsError("azurerm_nat_gateway_public_ip_association", *natGateway.ID) - } + if existingPublicIPAddress.ID == nil { + continue + } - publicIpAddresses = append(publicIpAddresses, existingPublicIPAddress) + if strings.EqualFold(*existingPublicIPAddress.ID, publicIpAddressId) { + return tf.ImportAsExistsError("azurerm_nat_gateway_public_ip_association", id) } + + publicIpAddresses = append(publicIpAddresses, existingPublicIPAddress) } } @@ -97,98 +100,119 @@ func resourceArmNatGatewayPublicIpAssociationCreate(d *schema.ResourceData, meta future, err := client.CreateOrUpdate(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, natGateway) if err != nil { - return fmt.Errorf("failed to update Public IP Association for Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) + return fmt.Errorf("failed to update Public IP Association for NAT Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("failed to wait for completion of Public IP Association for Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) + return fmt.Errorf("failed to wait for completion of Public IP Association for NAT Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) } - resp, err := client.Get(ctx, parsedNatGatewayId.ResourceGroup, parsedNatGatewayId.Name, "") - if err != nil { - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", parsedNatGatewayId.Name, parsedNatGatewayId.ResourceGroup, err) - } - d.SetId(*resp.ID) + d.SetId(id) - return resourceArmNatGatewayPublicIpAssociationRead(d, meta) + return resourceArmNATGatewayPublicIpAssociationRead(d, meta) } -func resourceArmNatGatewayPublicIpAssociationRead(d *schema.ResourceData, meta interface{}) error { +func resourceArmNATGatewayPublicIpAssociationRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.NatGatewayClient ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := parse.NatGatewayID(d.Id()) + id, err := parse.NatGatewayPublicIPAddressAssociationID(d.Id()) if err != nil { return err } - natGateway, err := client.Get(ctx, id.ResourceGroup, id.Name, "") + natGateway, err := client.Get(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, "") if err != nil { if utils.ResponseWasNotFound(natGateway.Response) { - log.Printf("[DEBUG] Nat Gateway %q (Resource Group %q) could not be found - removing from state!", id.Name, id.ResourceGroup) + log.Printf("[DEBUG] NAT Gateway %q (Resource Group %q) could not be found - removing from state!", id.NatGateway.Name, id.NatGateway.ResourceGroup) d.SetId("") return nil } - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("failed to retrieve NAT Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) + } + + if natGateway.NatGatewayPropertiesFormat == nil { + return fmt.Errorf("`properties` was nil for NAT Gateway %q (Resource Group %q)", id.NatGateway.Name, id.NatGateway.ResourceGroup) + } + props := *natGateway.NatGatewayPropertiesFormat + + if props.PublicIPAddresses == nil { + log.Printf("[DEBUG] NAT Gateway %q (Resource Group %q) doesn't have any Public IP's - removing from state!", id.NatGateway.Name, id.NatGateway.ResourceGroup) + d.SetId("") + return nil + } + + publicIPAddressId := "" + for _, pip := range *props.PublicIPAddresses { + if pip.ID == nil { + continue + } + + if strings.EqualFold(*pip.ID, id.PublicIPAddressID) { + publicIPAddressId = *pip.ID + break + } } - if natGateway.PublicIPAddresses == nil { - log.Printf("[DEBUG] Nat Gateway %q (Resource Group %q) doesn't have a Public IP - removing from state!", id.Name, id.ResourceGroup) + if publicIPAddressId == "" { + log.Printf("[DEBUG] Association between NAT Gateway %q (Resource Group %q) and Public IP Address %q was not found - removing from state", id.NatGateway.Name, id.NatGateway.ResourceGroup, id.PublicIPAddressID) d.SetId("") return nil } d.Set("nat_gateway_id", natGateway.ID) + d.Set("public_ip_address_id", publicIPAddressId) return nil } -func resourceArmNatGatewayPublicIpAssociationDelete(d *schema.ResourceData, meta interface{}) error { +func resourceArmNATGatewayPublicIpAssociationDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.NatGatewayClient ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := parse.NatGatewayID(d.Id()) + id, err := parse.NatGatewayPublicIPAddressAssociationID(d.Id()) if err != nil { return err } - publicIpAddressId := d.Get("public_ip_address_id").(string) - - locks.ByName(id.Name, natGatewayResourceName) - defer locks.UnlockByName(id.Name, natGatewayResourceName) + locks.ByName(id.NatGateway.Name, natGatewayResourceName) + defer locks.UnlockByName(id.NatGateway.Name, natGatewayResourceName) - natGateway, err := client.Get(ctx, id.ResourceGroup, id.Name, "") + natGateway, err := client.Get(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, "") if err != nil { if utils.ResponseWasNotFound(natGateway.Response) { - return fmt.Errorf("Nat Gateway %q (Resource Group %q) was not found.", id.Name, id.ResourceGroup) + return fmt.Errorf("NAT Gateway %q (Resource Group %q) was not found", id.NatGateway.Name, id.NatGateway.ResourceGroup) } - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("retrieving NAT Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) + } + if natGateway.NatGatewayPropertiesFormat == nil { + return fmt.Errorf("retrieving NAT Gateway %q (Resource Group %q): `properties` was nil", id.NatGateway.Name, id.NatGateway.ResourceGroup) } publicIpAddresses := make([]network.SubResource, 0) - if publicIPAddresses := natGateway.PublicIPAddresses; publicIPAddresses != nil { + if publicIPAddresses := natGateway.NatGatewayPropertiesFormat.PublicIPAddresses; publicIPAddresses != nil { for _, publicIPAddress := range *publicIPAddresses { if publicIPAddress.ID == nil { continue } - if *publicIPAddress.ID != publicIpAddressId { + if !strings.EqualFold(*publicIPAddress.ID, id.PublicIPAddressID) { publicIpAddresses = append(publicIpAddresses, publicIPAddress) } } } - natGateway.PublicIPAddresses = &publicIpAddresses + natGateway.NatGatewayPropertiesFormat.PublicIPAddresses = &publicIpAddresses - future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, natGateway) + future, err := client.CreateOrUpdate(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, natGateway) if err != nil { - return fmt.Errorf("failed to remove Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("removing association between NAT Gateway %q (Resource Group %q) and Public IP Address %q: %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, id.PublicIPAddressID, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("failed to wait for removal of Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("waiting for association between Public IP ID %q for NAT Gateway %q (Resource Group %q) to be removed: %+v", id.PublicIPAddressID, id.NatGateway.Name, id.NatGateway.ResourceGroup, err) } return nil diff --git a/azurerm/internal/services/network/nat_gateway_resource.go b/azurerm/internal/services/network/nat_gateway_resource.go index cc9783747c88..35ba4f42240f 100644 --- a/azurerm/internal/services/network/nat_gateway_resource.go +++ b/azurerm/internal/services/network/nat_gateway_resource.go @@ -12,7 +12,8 @@ import ( "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/azure" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/helpers/tf" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/clients" - "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/features" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/locks" + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/tags" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/timeouts" "github.com/terraform-providers/terraform-provider-azurerm/azurerm/utils" @@ -22,9 +23,9 @@ var natGatewayResourceName = "azurerm_nat_gateway" func resourceArmNatGateway() *schema.Resource { return &schema.Resource{ - Create: resourceArmNatGatewayCreateUpdate, + Create: resourceArmNatGatewayCreate, Read: resourceArmNatGatewayRead, - Update: resourceArmNatGatewayCreateUpdate, + Update: resourceArmNatGatewayUpdate, Delete: resourceArmNatGatewayDelete, Timeouts: &schema.ResourceTimeout{ @@ -65,7 +66,8 @@ func resourceArmNatGateway() *schema.Resource { Type: schema.TypeString, ValidateFunc: azure.ValidateResourceID, }, - Deprecated: "Deprecated in favor of `azurerm_nat_gateway_public_ip_association`. The dependency relation between `azurerm_nat_gateway` and `azurerm_public_ip` isn't detected by implicit dependency. So `azurerm_nat_gateway_public_ip_association` is added to resolve this issue.", + // TODO: remove in 3.0 + Deprecated: "Inline Public IP Address ID Deprecations have been deprecated in favour of the `azurerm_nat_gateway_public_ip_association` resource. This field will be removed in the next major version of the Azure Provider.", }, "public_ip_prefix_ids": { @@ -98,25 +100,26 @@ func resourceArmNatGateway() *schema.Resource { } } -func resourceArmNatGatewayCreateUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceArmNatGatewayCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.NatGatewayClient - ctx, cancel := timeouts.ForCreateUpdate(meta.(*clients.Client).StopContext, d) + ctx, cancel := timeouts.ForCreate(meta.(*clients.Client).StopContext, d) defer cancel() name := d.Get("name").(string) resourceGroup := d.Get("resource_group_name").(string) - if features.ShouldResourcesBeImported() && d.IsNewResource() { - resp, err := client.Get(ctx, resourceGroup, name, "") - if err != nil { - if !utils.ResponseWasNotFound(resp.Response) { - return fmt.Errorf("Error checking for present of existing NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) - } - } - if resp.ID != nil && *resp.ID != "" { - return tf.ImportAsExistsError("azurerm_nat_gateway", *resp.ID) + locks.ByName(name, natGatewayResourceName) + defer locks.UnlockByName(name, natGatewayResourceName) + + resp, err := client.Get(ctx, resourceGroup, name, "") + if err != nil { + if !utils.ResponseWasNotFound(resp.Response) { + return fmt.Errorf("Error checking for present of existing NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) } } + if resp.ID != nil && *resp.ID != "" { + return tf.ImportAsExistsError("azurerm_nat_gateway", *resp.ID) + } location := azure.NormalizeLocation(d.Get("location").(string)) idleTimeoutInMinutes := d.Get("idle_timeout_in_minutes").(int) @@ -148,7 +151,7 @@ func resourceArmNatGatewayCreateUpdate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error waiting for creation of NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) } - resp, err := client.Get(ctx, resourceGroup, name, "") + resp, err = client.Get(ctx, resourceGroup, name, "") if err != nil { return fmt.Errorf("Error retrieving NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) } @@ -160,31 +163,110 @@ func resourceArmNatGatewayCreateUpdate(d *schema.ResourceData, meta interface{}) return resourceArmNatGatewayRead(d, meta) } +func resourceArmNatGatewayUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*clients.Client).Network.NatGatewayClient + ctx, cancel := timeouts.ForUpdate(meta.(*clients.Client).StopContext, d) + defer cancel() + + id, err := parse.NatGatewayID(d.Id()) + if err != nil { + return err + } + + locks.ByName(id.Name, natGatewayResourceName) + defer locks.UnlockByName(id.Name, natGatewayResourceName) + + existing, err := client.Get(ctx, id.ResourceGroup, id.Name, "") + if err != nil { + if utils.ResponseWasNotFound(existing.Response) { + return fmt.Errorf("NAT Gateway %q (Resource Group %q) was not found!", id.Name, id.ResourceGroup) + } + + return fmt.Errorf("retrieving NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + if existing.NatGatewayPropertiesFormat == nil { + return fmt.Errorf("retrieving NAT Gateway %q (Resource Group %q): `properties` was nil", id.Name, id.ResourceGroup) + } + props := *existing.NatGatewayPropertiesFormat + + // intentionally building a new object rather than reusing due to the additional read-only fields + parameters := network.NatGateway{ + Location: existing.Location, + NatGatewayPropertiesFormat: &network.NatGatewayPropertiesFormat{ + IdleTimeoutInMinutes: props.IdleTimeoutInMinutes, + PublicIPAddresses: props.PublicIPAddresses, // note: these can be managed via the separate resource + PublicIPPrefixes: props.PublicIPPrefixes, + }, + Sku: existing.Sku, + Tags: existing.Tags, + Zones: existing.Zones, + } + + if d.HasChange("idle_timeout_in_minutes") { + timeout := d.Get("idle_timeout_in_minutes").(int) + parameters.NatGatewayPropertiesFormat.IdleTimeoutInMinutes = utils.Int32(int32(timeout)) + } + + if d.HasChange("sku_name") { + skuName := d.Get("sku_name").(string) + parameters.Sku = &network.NatGatewaySku{ + Name: network.NatGatewaySkuName(skuName), + } + } + + if d.HasChange("public_ip_address_ids") { + publicIpAddressIds := d.Get("public_ip_address_ids").(*schema.Set).List() + parameters.NatGatewayPropertiesFormat.PublicIPAddresses = expandArmNatGatewaySubResourceID(publicIpAddressIds) + } + + if d.HasChange("public_ip_prefix_ids") { + publicIpPrefixIds := d.Get("public_ip_prefix_ids").(*schema.Set).List() + parameters.NatGatewayPropertiesFormat.PublicIPPrefixes = expandArmNatGatewaySubResourceID(publicIpPrefixIds) + } + + if d.HasChange("tags") { + t := d.Get("tags").(map[string]interface{}) + parameters.Tags = tags.Expand(t) + } + + future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, parameters) + if err != nil { + return fmt.Errorf("updating NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { + return fmt.Errorf("waiting for update of NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + } + + return resourceArmNatGatewayRead(d, meta) +} + func resourceArmNatGatewayRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*clients.Client).Network.NatGatewayClient ctx, cancel := timeouts.ForRead(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.NatGatewayID(d.Id()) if err != nil { return err } - resourceGroup := id.ResourceGroup - name := id.Path["natGateways"] - resp, err := client.Get(ctx, resourceGroup, name, "") + resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "") if err != nil { if utils.ResponseWasNotFound(resp.Response) { - log.Printf("[INFO] NAT Gateway %q does not exist - removing from state", d.Id()) + log.Printf("[DEBUG] NAT Gateway %q does not exist - removing from state", d.Id()) d.SetId("") return nil } - return fmt.Errorf("Error reading NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("Error reading NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } d.Set("name", resp.Name) - d.Set("sku_name", resp.Sku.Name) - d.Set("resource_group_name", resourceGroup) + d.Set("resource_group_name", id.ResourceGroup) + + if sku := resp.Sku; sku != nil { + d.Set("sku_name", sku.Name) + } + if location := resp.Location; location != nil { d.Set("location", azure.NormalizeLocation(*location)) } @@ -214,24 +296,25 @@ func resourceArmNatGatewayDelete(d *schema.ResourceData, meta interface{}) error ctx, cancel := timeouts.ForDelete(meta.(*clients.Client).StopContext, d) defer cancel() - id, err := azure.ParseAzureResourceID(d.Id()) + id, err := parse.NatGatewayID(d.Id()) if err != nil { return err } - resourceGroup := id.ResourceGroup - name := id.Path["natGateways"] - future, err := client.Delete(ctx, resourceGroup, name) + locks.ByName(id.Name, natGatewayResourceName) + defer locks.UnlockByName(id.Name, natGatewayResourceName) + + future, err := client.Delete(ctx, id.ResourceGroup, id.Name) if err != nil { if response.WasNotFound(future.Response()) { return nil } - return fmt.Errorf("Error deleting NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("Error deleting NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { if !response.WasNotFound(future.Response()) { - return fmt.Errorf("Error waiting for deleting NAT Gateway %q (Resource Group %q): %+v", name, resourceGroup, err) + return fmt.Errorf("Error waiting for deleting NAT Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) } } diff --git a/azurerm/internal/services/network/parse/nat_gateway_public_ip_association.go b/azurerm/internal/services/network/parse/nat_gateway_public_ip_association.go new file mode 100644 index 000000000000..5ea4f18fa583 --- /dev/null +++ b/azurerm/internal/services/network/parse/nat_gateway_public_ip_association.go @@ -0,0 +1,34 @@ +package parse + +import ( + "fmt" + "strings" +) + +type NatGatewayPublicIPAddressAssociationId struct { + NatGateway NatGatewayId + PublicIPAddressID string +} + +func NatGatewayPublicIPAddressAssociationID(input string) (*NatGatewayPublicIPAddressAssociationId, error) { + segments := strings.Split(input, "|") + if len(segments) != 2 { + return nil, fmt.Errorf("Expected an ID in the format `{natGatewayID}|{publicIPAddressID} but got %q", input) + } + + natGatewayId, err := NatGatewayID(segments[0]) + if err != nil { + return nil, fmt.Errorf("parsing NAT Gateway ID %q: %+v", segments[0], err) + } + + // whilst we need the Resource ID, we may as well validate it + publicIPAddress := segments[1] + if _, err := PublicIPAddressID(publicIPAddress); err != nil { + return nil, fmt.Errorf("parsing Public IP Address ID %q: %+v", publicIPAddress, err) + } + + return &NatGatewayPublicIPAddressAssociationId{ + NatGateway: *natGatewayId, + PublicIPAddressID: publicIPAddress, + }, nil +} diff --git a/azurerm/internal/services/network/parse/nat_gateway_public_ip_association_test.go b/azurerm/internal/services/network/parse/nat_gateway_public_ip_association_test.go new file mode 100644 index 000000000000..44d9d4458258 --- /dev/null +++ b/azurerm/internal/services/network/parse/nat_gateway_public_ip_association_test.go @@ -0,0 +1,78 @@ +package parse + +import ( + "testing" +) + +func TestNatGatewayPublicIPAddressAssociationID(t *testing.T) { + testData := []struct { + Name string + Input string + Error bool + Expect *NatGatewayPublicIPAddressAssociationId + }{ + { + Name: "Empty", + Input: "", + Error: true, + }, + { + Name: "One Segment", + Input: "hello", + Error: true, + }, + { + Name: "Two Segments Invalid ID's", + Input: "hello|world", + Error: true, + }, + { + Name: "Missing Nat Gateway Value", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways", + Error: true, + }, + { + Name: "Nat Gateway ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1", + Error: true, + }, + { + Name: "Public IP Address ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/publicIPAddresses/myPublicIpAddress1", + Error: true, + }, + { + Name: "Nat Gateway / Public IP Association ID", + Input: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/publicIPAddresses/myPublicIpAddress1", + Error: false, + Expect: &NatGatewayPublicIPAddressAssociationId{ + NatGateway: NatGatewayId{ + Name: "gateway1", + ResourceGroup: "group1", + }, + PublicIPAddressID: "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/publicIPAddresses/myPublicIpAddress1", + }, + }, + } + + for _, v := range testData { + t.Logf("[DEBUG] Testing %q", v.Name) + + actual, err := NatGatewayPublicIPAddressAssociationID(v.Input) + if err != nil { + if v.Error { + continue + } + + t.Fatalf("Expected a value but got an error: %s", err) + } + + if actual.NatGateway.Name != v.Expect.NatGateway.Name { + t.Fatalf("Expected %q but got %q for Name", v.Expect.NatGateway.Name, actual.NatGateway.Name) + } + + if actual.NatGateway.ResourceGroup != v.Expect.NatGateway.ResourceGroup { + t.Fatalf("Expected %q but got %q for Resource Group", v.Expect.NatGateway.ResourceGroup, actual.NatGateway.ResourceGroup) + } + } +} diff --git a/azurerm/internal/services/network/registration.go b/azurerm/internal/services/network/registration.go index c6bdf4c1e225..4f65a2f771d3 100644 --- a/azurerm/internal/services/network/registration.go +++ b/azurerm/internal/services/network/registration.go @@ -86,7 +86,7 @@ func (r Registration) SupportedResources() map[string]*schema.Resource { "azurerm_private_endpoint": resourceArmPrivateEndpoint(), "azurerm_private_link_service": resourceArmPrivateLinkService(), "azurerm_public_ip": resourceArmPublicIp(), - "azurerm_nat_gateway_public_ip_association": resourceArmNatGatewayPublicIpAssociation(), + "azurerm_nat_gateway_public_ip_association": resourceArmNATGatewayPublicIpAssociation(), "azurerm_public_ip_prefix": resourceArmPublicIpPrefix(), "azurerm_network_security_group": resourceArmNetworkSecurityGroup(), "azurerm_network_security_rule": resourceArmNetworkSecurityRule(), diff --git a/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go b/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go index 39aa699e72ce..34a0235a5696 100644 --- a/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go +++ b/azurerm/internal/services/network/tests/nat_gateway_public_ip_association_resource_test.go @@ -2,6 +2,7 @@ package tests import ( "fmt" + "strings" "testing" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2020-03-01/network" @@ -26,13 +27,12 @@ func TestAccAzureRMNatGatewayPublicIpAssociation_basic(t *testing.T) { testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName), ), }, - // `public_ip_address_id` cannot be retrieved in read function while importing. - data.ImportStep("public_ip_address_id"), + data.ImportStep(), }, }) } -func TestAccAzureRMNatGatewayPublicIpAssociation_requiresImport(t *testing.T) { +func TestAccAzureRMNatGatewayPublicIpAssociation_updateNatGateway(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -46,31 +46,19 @@ func TestAccAzureRMNatGatewayPublicIpAssociation_requiresImport(t *testing.T) { testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName), ), }, - data.RequiresImportErrorStep(testAccAzureRMNatGatewayPublicIpAssociation_requiresImport), - }, - }) -} - -func TestAccAzureRMNatGatewayPublicIpAssociation_complete(t *testing.T) { - data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test") - resource.ParallelTest(t, resource.TestCase{ - PreCheck: func() { acceptance.PreCheck(t) }, - Providers: acceptance.SupportedProviders, - // intentional as this is a Virtual Resource - CheckDestroy: testCheckAzureRMNatGatewayDestroy, - Steps: []resource.TestStep{ + data.ImportStep(), { - Config: testAccAzureRMNatGatewayPublicIpAssociation_complete(data), + Config: testAccAzureRMNatGatewayPublicIpAssociation_updateNatGateway(data), Check: resource.ComposeTestCheckFunc( testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName), ), }, - data.ImportStep("public_ip_address_id"), + data.ImportStep(), }, }) } -func TestAccAzureRMNatGatewayPublicIpAssociation_update(t *testing.T) { +func TestAccAzureRMNatGatewayPublicIpAssociation_requiresImport(t *testing.T) { data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_association", "test") resource.ParallelTest(t, resource.TestCase{ PreCheck: func() { acceptance.PreCheck(t) }, @@ -84,14 +72,7 @@ func TestAccAzureRMNatGatewayPublicIpAssociation_update(t *testing.T) { testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName), ), }, - data.ImportStep("public_ip_address_id"), - { - Config: testAccAzureRMNatGatewayPublicIpAssociation_update(data), - Check: resource.ComposeTestCheckFunc( - testCheckAzureRMNatGatewayPublicIpAssociationExists(data.ResourceName), - ), - }, - data.ImportStep("public_ip_address_id"), + data.RequiresImportErrorStep(testAccAzureRMNatGatewayPublicIpAssociation_requiresImport), }, }) } @@ -117,18 +98,6 @@ func TestAccAzureRMNatGatewayPublicIpAssociation_deleted(t *testing.T) { }) } -func testAccAzureRMNatGatewayPublicIpAssociation_requiresImport(data acceptance.TestData) string { - template := testAccAzureRMNatGatewayPublicIpAssociation_basic(data) - return fmt.Sprintf(` -%s - -resource "azurerm_nat_gateway_public_ip_association" "import" { - nat_gateway_id = azurerm_nat_gateway_public_ip_association.test.nat_gateway_id - public_ip_address_id = azurerm_nat_gateway_public_ip_association.test.public_ip_address_id -} -`, template) -} - func testCheckAzureRMNatGatewayPublicIpAssociationExists(resourceName string) resource.TestCheckFunc { return func(s *terraform.State) error { client := acceptance.AzureProvider.Meta().(*clients.Client).Network.NatGatewayClient @@ -140,26 +109,25 @@ func testCheckAzureRMNatGatewayPublicIpAssociationExists(resourceName string) re return fmt.Errorf("Not found: %s", resourceName) } - id, err := parse.NatGatewayID(rs.Primary.ID) + id, err := parse.NatGatewayPublicIPAddressAssociationID(rs.Primary.ID) if err != nil { return err } - publicIpAddressId := rs.Primary.Attributes["public_ip_address_id"] - resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "") + resp, err := client.Get(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, "") if err != nil { - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) } if publicIpAddresses := resp.PublicIPAddresses; publicIpAddresses != nil { for _, publicIpAddress := range *publicIpAddresses { - if *publicIpAddress.ID == publicIpAddressId { + if strings.EqualFold(*publicIpAddress.ID, id.PublicIPAddressID) { return nil } } } - return fmt.Errorf("Association between Nat Gateway %q and Public Ip %q was not found.", id.Name, publicIpAddressId) + return fmt.Errorf("Association between Nat Gateway %q and Public IP %q was not found.", id.NatGateway.Name, id.PublicIPAddressID) } } @@ -174,34 +142,33 @@ func testCheckAzureRMNatGatewayPublicIpAssociationDisappears(resourceName string return fmt.Errorf("Not found: %s", resourceName) } - id, err := parse.NatGatewayID(rs.Primary.ID) + id, err := parse.NatGatewayPublicIPAddressAssociationID(rs.Primary.ID) if err != nil { return err } - publicIpAddressId := rs.Primary.Attributes["public_ip_address_id"] - resp, err := client.Get(ctx, id.ResourceGroup, id.Name, "") + resp, err := client.Get(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, "") if err != nil { - return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("failed to retrieve Nat Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) } updatedAddresses := make([]network.SubResource, 0) if publicIpAddresses := resp.PublicIPAddresses; publicIpAddresses != nil { for _, publicIpAddress := range *publicIpAddresses { - if *publicIpAddress.ID != publicIpAddressId { + if !strings.EqualFold(*publicIpAddress.ID, id.PublicIPAddressID) { updatedAddresses = append(updatedAddresses, publicIpAddress) } } } resp.PublicIPAddresses = &updatedAddresses - future, err := client.CreateOrUpdate(ctx, id.ResourceGroup, id.Name, resp) + future, err := client.CreateOrUpdate(ctx, id.NatGateway.ResourceGroup, id.NatGateway.Name, resp) if err != nil { - return fmt.Errorf("failed to remove Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("failed to remove Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) } if err = future.WaitForCompletionRef(ctx, client.Client); err != nil { - return fmt.Errorf("failed to wait for removal of Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.Name, id.ResourceGroup, err) + return fmt.Errorf("failed to wait for removal of Nat Gateway Public Ip Association for Nat Gateway %q (Resource Group %q): %+v", id.NatGateway.Name, id.NatGateway.ResourceGroup, err) } return nil @@ -213,52 +180,49 @@ func testAccAzureRMNatGatewayPublicIpAssociation_basic(data acceptance.TestData) return fmt.Sprintf(` %s +resource "azurerm_nat_gateway" "test" { + name = "acctest-NatGateway-%d" + location = azurerm_resource_group.test.location + resource_group_name = azurerm_resource_group.test.name + sku_name = "Standard" +} + resource "azurerm_nat_gateway_public_ip_association" "test" { nat_gateway_id = azurerm_nat_gateway.test.id public_ip_address_id = azurerm_public_ip.test.id } -`, template) +`, template, data.RandomInteger) } -func testAccAzureRMNatGatewayPublicIpAssociation_complete(data acceptance.TestData) string { - template := testAccAzureRMNatGatewayPublicIpAssociation_template(data) +func testAccAzureRMNatGatewayPublicIpAssociation_requiresImport(data acceptance.TestData) string { + template := testAccAzureRMNatGatewayPublicIpAssociation_basic(data) return fmt.Sprintf(` %s -resource "azurerm_nat_gateway_public_ip_association" "test" { - nat_gateway_id = azurerm_nat_gateway.test.id - public_ip_address_id = azurerm_public_ip.test.id -} - -resource "azurerm_public_ip" "test2" { - name = "acctest-PIP2-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - allocation_method = "Static" - sku = "Standard" -} - -resource "azurerm_nat_gateway_public_ip_association" "test2" { - nat_gateway_id = azurerm_nat_gateway.test.id - public_ip_address_id = azurerm_public_ip.test2.id +resource "azurerm_nat_gateway_public_ip_association" "import" { + nat_gateway_id = azurerm_nat_gateway_public_ip_association.test.nat_gateway_id + public_ip_address_id = azurerm_nat_gateway_public_ip_association.test.public_ip_address_id } -`, template, data.RandomInteger) +`, template) } -func testAccAzureRMNatGatewayPublicIpAssociation_update(data acceptance.TestData) string { +func testAccAzureRMNatGatewayPublicIpAssociation_updateNatGateway(data acceptance.TestData) string { template := testAccAzureRMNatGatewayPublicIpAssociation_template(data) return fmt.Sprintf(` %s -resource "azurerm_nat_gateway" "test2" { - name = "acctest-NatGateway2-%d" +resource "azurerm_nat_gateway" "test" { + name = "acctest-NatGateway-%d" location = azurerm_resource_group.test.location resource_group_name = azurerm_resource_group.test.name sku_name = "Standard" + tags = { + Hello = "World" + } } resource "azurerm_nat_gateway_public_ip_association" "test" { - nat_gateway_id = azurerm_nat_gateway.test2.id + nat_gateway_id = azurerm_nat_gateway.test.id public_ip_address_id = azurerm_public_ip.test.id } `, template, data.RandomInteger) @@ -282,12 +246,5 @@ resource "azurerm_public_ip" "test" { allocation_method = "Static" sku = "Standard" } - -resource "azurerm_nat_gateway" "test" { - name = "acctest-NatGateway-%d" - location = azurerm_resource_group.test.location - resource_group_name = azurerm_resource_group.test.name - sku_name = "Standard" -} -`, data.RandomInteger, data.Locations.Primary, data.RandomInteger, data.RandomInteger) +`, data.RandomInteger, data.Locations.Primary, data.RandomInteger) } diff --git a/azurerm/internal/services/network/validate/public_ip_address.go b/azurerm/internal/services/network/validate/public_ip_address.go new file mode 100644 index 000000000000..6c195be9784f --- /dev/null +++ b/azurerm/internal/services/network/validate/public_ip_address.go @@ -0,0 +1,22 @@ +package validate + +import ( + "fmt" + + "github.com/terraform-providers/terraform-provider-azurerm/azurerm/internal/services/network/parse" +) + +func PublicIPAddressID(i interface{}, k string) (warnings []string, errors []error) { + v, ok := i.(string) + if !ok { + errors = append(errors, fmt.Errorf("expected type of %q to be string", k)) + return + } + + if _, err := parse.PublicIPAddressID(v); err != nil { + errors = append(errors, fmt.Errorf("Can not parse %q as a resource id: %v", k, err)) + return + } + + return warnings, errors +} diff --git a/website/docs/r/nat_gateway_public_ip_association.html.markdown b/website/docs/r/nat_gateway_public_ip_association.html.markdown index 238523351371..1e2d40b02ee5 100644 --- a/website/docs/r/nat_gateway_public_ip_association.html.markdown +++ b/website/docs/r/nat_gateway_public_ip_association.html.markdown @@ -14,10 +14,6 @@ Manages the association between a Nat Gateway and a Public IP. ## Example Usage ```hcl -provider "azurerm" { - features {} -} - resource "azurerm_resource_group" "example" { name = "example-resources" location = "West Europe" @@ -71,5 +67,7 @@ The `timeouts` block allows you to specify [timeouts](https://www.terraform.io/d Associations between Nat Gateway and Public IP Addresses can be imported using the `resource id`, e.g. ```shell -terraform import azurerm_nat_gateway_public_ip_association.example /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1 +terraform import azurerm_nat_gateway_public_ip_association.example "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/group1/providers/Microsoft.Network/natGateways/gateway1|/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.Network/publicIPAddresses/myPublicIpAddress1" ``` + +-> **Note:** This is a Terraform Specific ID in the format `{natGatewayID}|{publicIPAddressID}`