From 98695a5bd6be6095d59063f8deb916c8f8c13eca Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Fri, 28 Aug 2026 15:28:02 +0530 Subject: [PATCH 1/2] Fix cloudstack_ipaddress delete silently succeeding on source-NAT IPs resourceCloudStackIPAddressDelete skipped the disassociate call entirely when is_source_nat was true and returned nil, so Terraform reported the resource destroyed while CloudStack left the IP allocated. Always call DisassociateIpAddress and propagate whatever CloudStack returns instead of swallowing it, matching every other error path in this file. Verified against a live 4.23.0.0 lab: against an implemented network with a running virtual router, CloudStack now correctly rejects the destroy (error 530, IP still in use for source NAT) instead of Terraform falsely reporting success; against a network that was never implemented, the disassociate genuinely succeeds and is now reported accurately. --- cloudstack/resource_cloudstack_ipaddress.go | 28 ++++++++++----------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/cloudstack/resource_cloudstack_ipaddress.go b/cloudstack/resource_cloudstack_ipaddress.go index 1af251f2..a0c4ea2c 100644 --- a/cloudstack/resource_cloudstack_ipaddress.go +++ b/cloudstack/resource_cloudstack_ipaddress.go @@ -276,23 +276,21 @@ func resourceCloudStackIPAddressRead(d *schema.ResourceData, meta interface{}) e } func resourceCloudStackIPAddressDelete(d *schema.ResourceData, meta interface{}) error { - if !d.Get("is_source_nat").(bool) { - cs := meta.(*cloudstack.CloudStackClient) - - // Create a new parameter struct - p := cs.Address.NewDisassociateIpAddressParams(d.Id()) - - // Disassociate the IP address - if _, err := cs.Address.DisassociateIpAddress(p); err != nil { - // This is a very poor way to be told the ID does no longer exist :( - if strings.Contains(err.Error(), fmt.Sprintf( - "Invalid parameter id value=%s due to incorrect long value format, "+ - "or entity does not exist", d.Id())) { - return nil - } + cs := meta.(*cloudstack.CloudStackClient) - return fmt.Errorf("Error disassociating IP address %s: %s", d.Id(), err) + // Create a new parameter struct + p := cs.Address.NewDisassociateIpAddressParams(d.Id()) + + // Disassociate the IP address + if _, err := cs.Address.DisassociateIpAddress(p); err != nil { + // This is a very poor way to be told the ID does no longer exist :( + if strings.Contains(err.Error(), fmt.Sprintf( + "Invalid parameter id value=%s due to incorrect long value format, "+ + "or entity does not exist", d.Id())) { + return nil } + + return fmt.Errorf("Error disassociating IP address %s: %s", d.Id(), err) } return nil From 410b8cda21e974f9aa04c30dacb53edbcbc972f4 Mon Sep 17 00:00:00 2001 From: Manoj Kumar Date: Wed, 2 Sep 2026 10:01:48 +0530 Subject: [PATCH 2/2] Treat source-NAT disassociate failure as a no-op on delete CloudStack rejects DisassociateIpAddress for a source NAT IP while its owning network/VPC still exists, so the now-unconditional disassociate call added in the previous commit fails terraform destroy on any config that manages a source NAT IP as its own cloudstack_ipaddress resource (e.g. imported directly). Deleting the network/VPC releases the IP as a side effect, so treat that specific CloudStack error as success here instead of failing the destroy. --- cloudstack/resource_cloudstack_ipaddress.go | 6 ++++++ website/docs/r/ipaddress.html.markdown | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/cloudstack/resource_cloudstack_ipaddress.go b/cloudstack/resource_cloudstack_ipaddress.go index a0c4ea2c..b1c0b815 100644 --- a/cloudstack/resource_cloudstack_ipaddress.go +++ b/cloudstack/resource_cloudstack_ipaddress.go @@ -290,6 +290,12 @@ func resourceCloudStackIPAddressDelete(d *schema.ResourceData, meta interface{}) return nil } + // A source NAT IP can't be disassociated while its network/VPC still exists; + // deleting that network/VPC releases it instead, so treat this as a no-op. + if strings.Contains(err.Error(), "used for source nat purposes and can not be disassociated") { + return nil + } + return fmt.Errorf("Error disassociating IP address %s: %s", d.Id(), err) } diff --git a/website/docs/r/ipaddress.html.markdown b/website/docs/r/ipaddress.html.markdown index 19c18983..28552d5a 100644 --- a/website/docs/r/ipaddress.html.markdown +++ b/website/docs/r/ipaddress.html.markdown @@ -120,3 +120,9 @@ When importing into a project you need to prefix the import ID with the project ```shell $ terraform import cloudstack_ipaddress.default my-project/6226ea4d-9cbe-4cc9-b30c-b9532146da5b ``` + +*NOTE: A source NAT IP cannot be released on its own while its network or VPC still +exists; CloudStack ties its lifecycle to the owning network/VPC and releases it +automatically when that network/VPC is deleted. Destroying a `cloudstack_ipaddress` +resource that manages a source NAT IP is a no-op until the owning network/VPC is +also destroyed.*