Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package network

import (
"context"
"fmt"
"strings"
"time"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/lang/response"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/network/2025-01-01/natgateways"
"github.com/hashicorp/terraform-provider-azurerm/internal/locks"
"github.com/hashicorp/terraform-provider-azurerm/internal/sdk"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

type NatGatewayPublicIpV6AssociationResource struct{}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should use IPv6 or ipv6, althought the SDK generated with IPAddressV6, the standard spelling is a lowercased v6 as IPv6. we should rename all other words in this PR too.

Suggested change
type NatGatewayPublicIpV6AssociationResource struct{}
type NatGatewayPublicIPv6AssociationResource struct{}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated


var _ sdk.Resource = NatGatewayPublicIpV6AssociationResource{}

type NatGatewayPublicIpV6AssociationModel struct {
NatGatewayId string `tfschema:"nat_gateway_id"`
PublicIpAddressId string `tfschema:"public_ip_address_id"`
}

func (r NatGatewayPublicIpV6AssociationResource) Arguments() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{
"nat_gateway_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: natgateways.ValidateNatGatewayID,
},

"public_ip_address_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: commonids.ValidatePublicIPAddressID,
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"nat_gateway_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: natgateways.ValidateNatGatewayID,
},
"public_ip_address_id": {
Type: pluginsdk.TypeString,
Required: true,
ForceNew: true,
ValidateFunc: commonids.ValidatePublicIPAddressID,
},
"nat_gateway_id": commonschema.ResourceIDReferenceRequiredForceNew(&natgateways.NatGatewayId{}),
"public_ip_address_id": commonschema.ResourceIDReferenceRequiredForceNew(&commonids.PublicIPAddressId{}),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

}
}

func (r NatGatewayPublicIpV6AssociationResource) Attributes() map[string]*pluginsdk.Schema {
return map[string]*pluginsdk.Schema{}
}

func (r NatGatewayPublicIpV6AssociationResource) ModelObject() interface{} {
return &NatGatewayPublicIpV6AssociationModel{}
}

func (r NatGatewayPublicIpV6AssociationResource) ResourceType() string {
return "azurerm_nat_gateway_public_ip_v6_association"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be no breaks in ipv6?

Suggested change
return "azurerm_nat_gateway_public_ip_v6_association"
return "azurerm_nat_gateway_public_ipv6_association"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

}

func (r NatGatewayPublicIpV6AssociationResource) Create() sdk.ResourceFunc {
return sdk.ResourceFunc{
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Network.NatGateways

var state NatGatewayPublicIpV6AssociationModel
if err := metadata.Decode(&state); err != nil {
return err
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return err
return fmt.Errorf("decoding: %+v", err)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

}

publicIpAddressId, err := commonids.ParsePublicIPAddressID(state.PublicIpAddressId)
if err != nil {
return err
}

natGatewayId, err := natgateways.ParseNatGatewayID(state.NatGatewayId)
if err != nil {
return err
}

locks.ByName(natGatewayId.NatGatewayName, natGatewayResourceName)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to lock by the nat gateway's ID? Will there be two gateways in different resource group but same name?

Copy link
Copy Markdown
Contributor Author

@neil-yechenwei neil-yechenwei Dec 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. API allows to create two gateways with same name in different resource groups.

defer locks.UnlockByName(natGatewayId.NatGatewayName, natGatewayResourceName)

natGateway, err := client.Get(ctx, *natGatewayId, natgateways.DefaultGetOperationOptions())
if err != nil {
if response.WasNotFound(natGateway.HttpResponse) {
return fmt.Errorf("%s was not found", natGatewayId)
}
return fmt.Errorf("retrieving %s: %+v", natGatewayId, err)
}

if natGateway.Model == nil {
return fmt.Errorf("retrieving %s: `model` was nil", natGatewayId)
}
if natGateway.Model.Properties == nil {
return fmt.Errorf("retrieving %s: `properties` was nil", natGatewayId)
}

id := commonids.NewCompositeResourceID(natGatewayId, publicIpAddressId)

publicIpAddressesV6 := make([]natgateways.SubResource, 0)
if natGateway.Model.Properties.PublicIPAddressesV6 != nil {
for _, existingPublicIPAddress := range *natGateway.Model.Properties.PublicIPAddressesV6 {
if existingPublicIPAddress.Id == nil {
continue
}

if strings.EqualFold(*existingPublicIPAddress.Id, publicIpAddressId.ID()) {
return metadata.ResourceRequiresImport(r.ResourceType(), id)
}

publicIpAddressesV6 = append(publicIpAddressesV6, existingPublicIPAddress)
}
}

publicIpAddressesV6 = append(publicIpAddressesV6, natgateways.SubResource{
Id: pointer.To(state.PublicIpAddressId),
})
natGateway.Model.Properties.PublicIPAddressesV6 = pointer.To(publicIpAddressesV6)

if err := client.CreateOrUpdateThenPoll(ctx, *natGatewayId, *natGateway.Model); err != nil {
return fmt.Errorf("updating %s: %+v", natGatewayId, err)
}

metadata.SetID(id)
return nil
},
Timeout: 30 * time.Minute,
}
}

func (r NatGatewayPublicIpV6AssociationResource) Read() sdk.ResourceFunc {
return sdk.ResourceFunc{
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Network.NatGateways

id, err := commonids.ParseCompositeResourceID(metadata.ResourceData.Id(), &natgateways.NatGatewayId{}, &commonids.PublicIPAddressId{})
if err != nil {
return err
}

natGateway, err := client.Get(ctx, *id.First, natgateways.DefaultGetOperationOptions())
if err != nil {
if response.WasNotFound(natGateway.HttpResponse) {
return metadata.MarkAsGone(id)
}
return fmt.Errorf("retrieving %s: %+v", *id.First, err)
}

if natGateway.Model == nil || natGateway.Model.Properties == nil {
return metadata.MarkAsGone(id)
}

if natGateway.Model.Properties.PublicIPAddressesV6 == nil {
return metadata.MarkAsGone(id)
}

publicIPAddressFound := false
for _, pip := range *natGateway.Model.Properties.PublicIPAddressesV6 {
if pip.Id == nil {
continue
}

if strings.EqualFold(*pip.Id, id.Second.ID()) {
publicIPAddressFound = true
break
}
}

if !publicIPAddressFound {
return metadata.MarkAsGone(id)
}

state := NatGatewayPublicIpV6AssociationModel{
NatGatewayId: id.First.ID(),
PublicIpAddressId: id.Second.ID(),
}

return metadata.Encode(&state)
},
Timeout: 5 * time.Minute,
}
}

func (r NatGatewayPublicIpV6AssociationResource) Delete() sdk.ResourceFunc {
return sdk.ResourceFunc{
Func: func(ctx context.Context, metadata sdk.ResourceMetaData) error {
client := metadata.Client.Network.NatGateways

var state NatGatewayPublicIpV6AssociationModel
if err := metadata.Decode(&state); err != nil {
return err
}

publicIpAddressId, err := commonids.ParsePublicIPAddressID(state.PublicIpAddressId)
if err != nil {
return err
}

natGatewayId, err := natgateways.ParseNatGatewayID(state.NatGatewayId)
if err != nil {
return err
}

locks.ByName(natGatewayId.NatGatewayName, natGatewayResourceName)
defer locks.UnlockByName(natGatewayId.NatGatewayName, natGatewayResourceName)

natGateway, err := client.Get(ctx, *natGatewayId, natgateways.DefaultGetOperationOptions())
if err != nil {
if response.WasNotFound(natGateway.HttpResponse) {
return fmt.Errorf("%s was not found", natGatewayId)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pointers used in fmt.Errorf() should be dereferenced, please fix similar issues in the _resource file.

Suggested change
return fmt.Errorf("%s was not found", natGatewayId)
return fmt.Errorf("%s was not found", *natGatewayId)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

}
return fmt.Errorf("retrieving %s: %+v", natGatewayId, err)
}

if natGateway.Model == nil {
return fmt.Errorf("retrieving %s: `model` was nil", natGatewayId)
}
if natGateway.Model.Properties == nil {
return fmt.Errorf("retrieving %s: `properties` was nil", natGatewayId)
}

publicIpAddressesV6 := make([]natgateways.SubResource, 0)
if publicIPAddresses := natGateway.Model.Properties.PublicIPAddressesV6; publicIPAddresses != nil {
for _, publicIPAddress := range *publicIPAddresses {
if publicIPAddress.Id == nil {
continue
}

if !strings.EqualFold(*publicIPAddress.Id, publicIpAddressId.ID()) {
publicIpAddressesV6 = append(publicIpAddressesV6, publicIPAddress)
}
}
}
natGateway.Model.Properties.PublicIPAddressesV6 = pointer.To(publicIpAddressesV6)

if err := client.CreateOrUpdateThenPoll(ctx, *natGatewayId, *natGateway.Model); err != nil {
return fmt.Errorf("removing association between %s and %s: %+v", natGatewayId, publicIpAddressId, err)
}

return nil
},
Timeout: 30 * time.Minute,
}
}

func (r NatGatewayPublicIpV6AssociationResource) IDValidationFunc() pluginsdk.SchemaValidateFunc {
return func(input interface{}, key string) (warnings []string, errors []error) {
v, ok := input.(string)
if !ok {
errors = append(errors, fmt.Errorf("expected %q to be a string", key))
return
}

if _, err := commonids.ParseCompositeResourceID(v, &natgateways.NatGatewayId{}, &commonids.PublicIPAddressId{}); err != nil {
errors = append(errors, err)
}

return
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package network_test

import (
"context"
"fmt"
"strings"
"testing"

"github.com/hashicorp/go-azure-helpers/lang/pointer"
"github.com/hashicorp/go-azure-helpers/resourcemanager/commonids"
"github.com/hashicorp/go-azure-sdk/resource-manager/network/2025-01-01/natgateways"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance"
"github.com/hashicorp/terraform-provider-azurerm/internal/acceptance/check"
"github.com/hashicorp/terraform-provider-azurerm/internal/clients"
"github.com/hashicorp/terraform-provider-azurerm/internal/tf/pluginsdk"
)

type NatGatewayPublicIpV6AssociationResource struct{}

func TestAccNatGatewayPublicIpV6Association_basic(t *testing.T) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add another test to show verify if multiple associations can work together.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_v6_association", "test")
r := NatGatewayPublicIpV6AssociationResource{}
data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.ImportStep(),
})
}

func TestAccNatGatewayPublicIpV6Association_requiresImport(t *testing.T) {
data := acceptance.BuildTestData(t, "azurerm_nat_gateway_public_ip_v6_association", "test")
r := NatGatewayPublicIpV6AssociationResource{}
data.ResourceTest(t, r, []acceptance.TestStep{
{
Config: r.basic(data),
Check: acceptance.ComposeTestCheckFunc(
check.That(data.ResourceName).ExistsInAzure(r),
),
},
data.RequiresImportErrorStep(r.requiresImport),
})
}

func (t NatGatewayPublicIpV6AssociationResource) Exists(ctx context.Context, clients *clients.Client, state *pluginsdk.InstanceState) (*bool, error) {
id, err := commonids.ParseCompositeResourceID(state.ID, &natgateways.NatGatewayId{}, &commonids.PublicIPAddressId{})
if err != nil {
return nil, err
}

resp, err := clients.Network.NatGateways.Get(ctx, *id.First, natgateways.DefaultGetOperationOptions())
if err != nil {
return nil, fmt.Errorf("retrieving %s: %+v", *id.First, err)
}

found := false
if model := resp.Model; model != nil {
if props := model.Properties; props != nil {
if props.PublicIPAddressesV6 != nil {
for _, pip := range *props.PublicIPAddressesV6 {
if pip.Id == nil {
continue
}

if strings.EqualFold(*pip.Id, id.Second.ID()) {
found = true
break
}
}
}
}
}

return pointer.To(found), nil
}

func (r NatGatewayPublicIpV6AssociationResource) basic(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

provider "azurerm" {
features {}
}

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 = "StandardV2"
zones = ["1", "2", "3"]
}

resource "azurerm_nat_gateway_public_ip_v6_association" "test" {
nat_gateway_id = azurerm_nat_gateway.test.id
public_ip_address_id = azurerm_public_ip.test.id
}
`, r.template(data), data.RandomInteger)
}

func (r NatGatewayPublicIpV6AssociationResource) requiresImport(data acceptance.TestData) string {
return fmt.Sprintf(`
%s

resource "azurerm_nat_gateway_public_ip_v6_association" "import" {
nat_gateway_id = azurerm_nat_gateway_public_ip_v6_association.test.nat_gateway_id
public_ip_address_id = azurerm_nat_gateway_public_ip_v6_association.test.public_ip_address_id
}
`, r.basic(data))
}

func (NatGatewayPublicIpV6AssociationResource) template(data acceptance.TestData) string {
return fmt.Sprintf(`
resource "azurerm_resource_group" "test" {
name = "acctestRG-ngpi-v6-%d"
location = "%s"
}

resource "azurerm_public_ip" "test" {
name = "acctest-PIPv6-%d"
location = azurerm_resource_group.test.location
resource_group_name = azurerm_resource_group.test.name
allocation_method = "Static"
sku = "StandardV2"
ip_version = "IPv6"
}
`, data.RandomInteger, data.Locations.Primary, data.RandomInteger)
}
Loading
Loading