Skip to content

Commit da8672d

Browse files
ksamorayclaude
andcommitted
Fix orphaned NSX allocation on IP realization failure
When allocation_ip is unset and realization fails, Create tried to clean up via resourceNsxtPolicyIPAddressAllocationDelete before d.SetId() had ever been called, so the delete silently no-op'd on an empty ID and the allocation was leaked on NSX Manager with no Terraform state pointing to it. Set the ID temporarily so the real delete path (including cache invalidation) runs, then clear it again so Create still reports failure. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent e3ac3a0 commit da8672d

2 files changed

Lines changed: 98 additions & 6 deletions

File tree

nsxt/resource_nsxt_policy_ip_address_allocation.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,7 @@ func resourceNsxtPolicyIPAddressAllocationCreate(d *schema.ResourceData, m inter
144144
stateConf := nsxtPolicyWaitForRealizationStateConf(connector, d, m, *obj.Path, timeout)
145145
entity, err := stateConf.WaitForState()
146146
if err != nil {
147-
// Clean up NSX allocation
148-
resourceNsxtPolicyIPAddressAllocationDelete(d, m)
149-
return err
147+
return cleanupFailedIPAddressAllocation(d, m, id, err)
150148
}
151149
realizedResource := entity.(model.GenericPolicyRealizedResource)
152150
for _, attr := range realizedResource.ExtendedAttributes {
@@ -158,9 +156,7 @@ func resourceNsxtPolicyIPAddressAllocationCreate(d *schema.ResourceData, m inter
158156
return resourceNsxtPolicyIPAddressAllocationRead(d, m)
159157
}
160158
}
161-
// Clean up NSX allocation
162-
resourceNsxtPolicyIPAddressAllocationDelete(d, m)
163-
return fmt.Errorf("Failed to get realized IP for path %s", d.Get("path"))
159+
return cleanupFailedIPAddressAllocation(d, m, id, fmt.Errorf("Failed to get realized IP for path %s", d.Get("path")))
164160
}
165161

166162
d.SetId(id)
@@ -169,6 +165,20 @@ func resourceNsxtPolicyIPAddressAllocationCreate(d *schema.ResourceData, m inter
169165
return resourceNsxtPolicyIPAddressAllocationRead(d, m)
170166
}
171167

168+
// cleanupFailedIPAddressAllocation deletes an IPAddressAllocation that was already
169+
// created on NSX (via Patch) but whose realization failed, so it isn't leaked.
170+
// The ID is set temporarily so resourceNsxtPolicyIPAddressAllocationDelete can be
171+
// reused (including its cache invalidation), then cleared again since Create is
172+
// failing and no resource should end up in Terraform state.
173+
func cleanupFailedIPAddressAllocation(d *schema.ResourceData, m interface{}, id string, originalErr error) error {
174+
d.SetId(id)
175+
if deleteErr := resourceNsxtPolicyIPAddressAllocationDelete(d, m); deleteErr != nil {
176+
log.Printf("[WARNING] Failed to clean up IPAddressAllocation with ID %s after create failure: %v", id, deleteErr)
177+
}
178+
d.SetId("")
179+
return originalErr
180+
}
181+
172182
func resourceNsxtPolicyIPAddressAllocationRead(d *schema.ResourceData, m interface{}) error {
173183
connector := getPolicyConnector(m)
174184
client := cliIpAllocationsClient(getSessionContext(d, m), connector)

nsxt/utgomock_resource_nsxt_policy_ip_address_allocation_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
package nsxt
88

99
import (
10+
"errors"
1011
"testing"
1112

1213
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
@@ -18,8 +19,10 @@ import (
1819
"go.uber.org/mock/gomock"
1920

2021
ippoolsapi "github.com/vmware/terraform-provider-nsxt/api/infra/ip_pools"
22+
realizedstateapi "github.com/vmware/terraform-provider-nsxt/api/infra/realized_state"
2123
utl "github.com/vmware/terraform-provider-nsxt/api/utl"
2224
ippoolmocks "github.com/vmware/terraform-provider-nsxt/mocks/infra/ip_pools"
25+
realizedstatemocks "github.com/vmware/terraform-provider-nsxt/mocks/infra/realized_state"
2326
)
2427

2528
var (
@@ -55,6 +58,13 @@ func minimalNsxtIpAllocData() map[string]interface{} {
5558
}
5659
}
5760

61+
func minimalNsxtIpAllocDataNoIP() map[string]interface{} {
62+
data := minimalNsxtIpAllocData()
63+
delete(data, "allocation_ip")
64+
data["timeout"] = 5
65+
return data
66+
}
67+
5868
func setupNsxtIpAllocMock(t *testing.T, ctrl *gomock.Controller) (*ippoolmocks.MockIpAllocationsClient, func()) {
5969
mockSDK := ippoolmocks.NewMockIpAllocationsClient(ctrl)
6070
mockWrapper := &ippoolsapi.IpAddressAllocationClientContext{
@@ -69,6 +79,20 @@ func setupNsxtIpAllocMock(t *testing.T, ctrl *gomock.Controller) (*ippoolmocks.M
6979
return mockSDK, func() { cliIpAllocationsClient = original }
7080
}
7181

82+
func setupNsxtIpAllocRealizedMock(t *testing.T, ctrl *gomock.Controller) (*realizedstatemocks.MockRealizedEntitiesClient, func()) {
83+
mockSDK := realizedstatemocks.NewMockRealizedEntitiesClient(ctrl)
84+
mockWrapper := &realizedstateapi.RealizedEntityClientContext{
85+
Client: mockSDK,
86+
ClientType: utl.Local,
87+
}
88+
89+
original := cliRealizedEntitiesClient
90+
cliRealizedEntitiesClient = func(_ utl.SessionContext, _ vapiProtocolClient.Connector) *realizedstateapi.RealizedEntityClientContext {
91+
return mockWrapper
92+
}
93+
return mockSDK, func() { cliRealizedEntitiesClient = original }
94+
}
95+
7296
func TestMockResourceNsxtPolicyIPAddressAllocationCreate(t *testing.T) {
7397
ctrl := gomock.NewController(t)
7498
defer ctrl.Finish()
@@ -103,6 +127,64 @@ func TestMockResourceNsxtPolicyIPAddressAllocationCreate(t *testing.T) {
103127
})
104128
}
105129

130+
func TestMockResourceNsxtPolicyIPAddressAllocationCreateRealizationCleanup(t *testing.T) {
131+
ctrl := gomock.NewController(t)
132+
defer ctrl.Finish()
133+
mockSDK, restoreAlloc := setupNsxtIpAllocMock(t, ctrl)
134+
defer restoreAlloc()
135+
mockRealizedSDK, restoreRealized := setupNsxtIpAllocRealizedMock(t, ctrl)
136+
defer restoreRealized()
137+
138+
t.Run("Create cleans up and clears ID when realization fails", func(t *testing.T) {
139+
notFoundErr := vapiErrors.NotFound{}
140+
gomock.InOrder(
141+
mockSDK.EXPECT().Get(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nsxModel.IpAddressAllocation{}, notFoundErr),
142+
mockSDK.EXPECT().Patch(nsxtIpAllocPoolID, nsxtIpAllocID, gomock.Any()).Return(nil),
143+
mockSDK.EXPECT().Get(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nsxtIpAllocAPIResponse(), nil),
144+
)
145+
mockRealizedSDK.EXPECT().List(nsxtIpAllocPath, nil).Return(nsxModel.GenericPolicyRealizedResourceListResult{}, errors.New("realization query failed"))
146+
// Cleanup must delete the already-created NSX object, keyed by the
147+
// resource ID directly (not read back from d.Id(), which isn't set yet).
148+
mockSDK.EXPECT().Delete(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nil)
149+
150+
res := resourceNsxtPolicyIPAddressAllocation()
151+
d := schema.TestResourceDataRaw(t, res.Schema, minimalNsxtIpAllocDataNoIP())
152+
153+
err := resourceNsxtPolicyIPAddressAllocationCreate(d, newGoMockProviderClient())
154+
require.Error(t, err)
155+
assert.Equal(t, "", d.Id())
156+
})
157+
158+
t.Run("Create cleans up and clears ID when realized IP attribute is missing", func(t *testing.T) {
159+
notFoundErr := vapiErrors.NotFound{}
160+
realizedState := "REALIZED"
161+
otherAttrKey := "other_attr"
162+
gomock.InOrder(
163+
mockSDK.EXPECT().Get(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nsxModel.IpAddressAllocation{}, notFoundErr),
164+
mockSDK.EXPECT().Patch(nsxtIpAllocPoolID, nsxtIpAllocID, gomock.Any()).Return(nil),
165+
mockSDK.EXPECT().Get(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nsxtIpAllocAPIResponse(), nil),
166+
)
167+
mockRealizedSDK.EXPECT().List(nsxtIpAllocPath, nil).Return(nsxModel.GenericPolicyRealizedResourceListResult{
168+
Results: []nsxModel.GenericPolicyRealizedResource{
169+
{
170+
State: &realizedState,
171+
ExtendedAttributes: []nsxModel.AttributeVal{
172+
{Key: &otherAttrKey, Values: []string{"x"}},
173+
},
174+
},
175+
},
176+
}, nil)
177+
mockSDK.EXPECT().Delete(nsxtIpAllocPoolID, nsxtIpAllocID).Return(nil)
178+
179+
res := resourceNsxtPolicyIPAddressAllocation()
180+
d := schema.TestResourceDataRaw(t, res.Schema, minimalNsxtIpAllocDataNoIP())
181+
182+
err := resourceNsxtPolicyIPAddressAllocationCreate(d, newGoMockProviderClient())
183+
require.Error(t, err)
184+
assert.Equal(t, "", d.Id())
185+
})
186+
}
187+
106188
func TestMockResourceNsxtPolicyIPAddressAllocationRead(t *testing.T) {
107189
ctrl := gomock.NewController(t)
108190
defer ctrl.Finish()

0 commit comments

Comments
 (0)