This is an automated email from the ASF dual-hosted git repository.

sureshanaparti pushed a commit to branch main
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-terraform-provider.git


The following commit(s) were added to refs/heads/main by this push:
     new 969683c  Fix cloudstack_ipaddress delete silently succeeding on 
source-NAT IPs (#331)
969683c is described below

commit 969683cf5d4a0dc52a8790aceb2fa91d32f6e54f
Author: Manoj Kumar <[email protected]>
AuthorDate: Thu Sep 3 19:56:27 2026 +0530

    Fix cloudstack_ipaddress delete silently succeeding on source-NAT IPs (#331)
    
    * 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.
    
    * 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 | 34 ++++++++++++++++-------------
 website/docs/r/ipaddress.html.markdown      |  6 +++++
 2 files changed, 25 insertions(+), 15 deletions(-)

diff --git a/cloudstack/resource_cloudstack_ipaddress.go 
b/cloudstack/resource_cloudstack_ipaddress.go
index 1af251f..b1c0b81 100644
--- a/cloudstack/resource_cloudstack_ipaddress.go
+++ b/cloudstack/resource_cloudstack_ipaddress.go
@@ -276,23 +276,27 @@ 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)
+
+       // 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)
+               // 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)
        }
 
        return nil
diff --git a/website/docs/r/ipaddress.html.markdown 
b/website/docs/r/ipaddress.html.markdown
index 19c1898..28552d5 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.*

Reply via email to