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 f52481e  Fix test findings: storage IP range updates, service offering 
import, project display_text (#332)
f52481e is described below

commit f52481ef24aff852e090674941c485a28d1c15d6
Author: Manoj Kumar <[email protected]>
AuthorDate: Thu Sep 3 20:00:07 2026 +0530

    Fix test findings: storage IP range updates, service offering import, 
project display_text (#332)
    
    * Mark storage_network_ip_range netmask/start_ip/end_ip as ForceNew
    
    * Document ForceNew-field footgun after cloudstack_service_offering import
    
    * Add snake_case display_text field to cloudstack_project, deprecate 
displaytext
    
    * Fix cloudstack_project display_text/displaytext import and update bugs
    
    * Narrow storage_network_ip_range ForceNew docs to overlapping edits
    
    * Add ConflictsWith between cloudstack_project displaytext/display_text
---
 cloudstack/resource_cloudstack_project.go          | 51 ++++++++++++++++++----
 ...resource_cloudstack_storage_network_ip_range.go | 19 ++++----
 website/docs/r/project.html.markdown               |  5 ++-
 website/docs/r/service_offering.html.markdown      | 11 +++++
 .../docs/r/storage_network_ip_range.html.markdown  | 19 +++++++-
 5 files changed, 86 insertions(+), 19 deletions(-)

diff --git a/cloudstack/resource_cloudstack_project.go 
b/cloudstack/resource_cloudstack_project.go
index 9c02846..db004af 100644
--- a/cloudstack/resource_cloudstack_project.go
+++ b/cloudstack/resource_cloudstack_project.go
@@ -47,8 +47,18 @@ func resourceCloudStackProject() *schema.Resource {
                        },
 
                        "displaytext": {
-                               Type:     schema.TypeString,
-                               Optional: true,
+                               Type:          schema.TypeString,
+                               Optional:      true,
+                               Computed:      true,
+                               Deprecated:    "use display_text instead",
+                               ConflictsWith: []string{"display_text"},
+                       },
+
+                       "display_text": {
+                               Type:          schema.TypeString,
+                               Optional:      true,
+                               Computed:      true,
+                               ConflictsWith: []string{"displaytext"},
                        },
 
                        "domain": {
@@ -76,12 +86,32 @@ func resourceCloudStackProject() *schema.Resource {
        }
 }
 
+// projectDisplayText resolves the effective display text from the new
+// display_text field and the deprecated displaytext field. ConflictsWith
+// keeps config from setting both at once, but both fields are Computed, so
+// GetOk alone can't tell a real edit from a stale Computed value left over
+// from the last refresh - HasChange is checked first to find the one
+// actually edited. The final GetOk/displaytext fallback only matters when
+// neither changed (e.g. a fresh create, or a no-op update).
+func projectDisplayText(d *schema.ResourceData) string {
+       if d.HasChange("display_text") {
+               return d.Get("display_text").(string)
+       }
+       if d.HasChange("displaytext") {
+               return d.Get("displaytext").(string)
+       }
+       if v, ok := d.GetOk("display_text"); ok {
+               return v.(string)
+       }
+       return d.Get("displaytext").(string)
+}
+
 func resourceCloudStackProjectCreate(d *schema.ResourceData, meta any) error {
        cs := meta.(*cloudstack.CloudStackClient)
 
        // Get the name and displaytext
        name := d.Get("name").(string)
-       displaytext := d.Get("displaytext").(string)
+       displaytext := projectDisplayText(d)
 
        // Get domain if provided
        var domain string
@@ -101,7 +131,6 @@ func resourceCloudStackProjectCreate(d 
*schema.ResourceData, meta any) error {
 
                        // Set the basic attributes to match the existing 
project
                        d.Set("name", existingProject.Name)
-                       d.Set("displaytext", existingProject.Displaytext)
                        d.Set("domain", existingProject.Domain)
 
                        return resourceCloudStackProjectRead(d, meta)
@@ -327,9 +356,13 @@ func resourceCloudStackProjectRead(d *schema.ResourceData, 
meta any) error {
 
        // Set the basic attributes
        d.Set("name", project.Name)
-       d.Set("displaytext", project.Displaytext)
        d.Set("domain", project.Domain)
 
+       // Both fields are Computed, so setting both unconditionally reflects 
the
+       // API value without creating a diff for a config that only sets one.
+       d.Set("displaytext", project.Displaytext)
+       d.Set("display_text", project.Displaytext)
+
        // Handle owner information more safely
        // Only set the account, accountid, and userid if they were explicitly 
set in the configuration
        // and if the owner information is available
@@ -397,7 +430,7 @@ func resourceCloudStackProjectUpdate(d 
*schema.ResourceData, meta any) error {
        cs := meta.(*cloudstack.CloudStackClient)
 
        // Check if the name or displaytext is changed
-       if d.HasChange("name") || d.HasChange("displaytext") {
+       if d.HasChange("name") || d.HasChange("displaytext") || 
d.HasChange("display_text") {
                // Create a new parameter struct
                p := cs.Project.NewUpdateProjectParams(d.Id())
 
@@ -409,8 +442,8 @@ func resourceCloudStackProjectUpdate(d 
*schema.ResourceData, meta any) error {
                        p.SetName(d.Get("name").(string))
                }
 
-               if d.HasChange("displaytext") {
-                       p.SetDisplaytext(d.Get("displaytext").(string))
+               if d.HasChange("displaytext") || d.HasChange("display_text") {
+                       p.SetDisplaytext(projectDisplayText(d))
                }
 
                log.Printf("[DEBUG] Updating project %s", d.Id())
@@ -490,7 +523,7 @@ func resourceCloudStackProjectUpdate(d 
*schema.ResourceData, meta any) error {
                        return retry.RetryableError(fmt.Errorf("project name 
not updated yet"))
                }
 
-               if d.HasChange("displaytext") && project.Displaytext != 
d.Get("displaytext").(string) {
+               if (d.HasChange("displaytext") || d.HasChange("display_text")) 
&& project.Displaytext != projectDisplayText(d) {
                        log.Printf("[DEBUG] Project %s displaytext not updated 
yet, retrying...", d.Id())
                        return retry.RetryableError(fmt.Errorf("project 
displaytext not updated yet"))
                }
diff --git a/cloudstack/resource_cloudstack_storage_network_ip_range.go 
b/cloudstack/resource_cloudstack_storage_network_ip_range.go
index 709108d..24d317e 100644
--- a/cloudstack/resource_cloudstack_storage_network_ip_range.go
+++ b/cloudstack/resource_cloudstack_storage_network_ip_range.go
@@ -47,6 +47,7 @@ func resourceCloudStackStorageNetworkIpRange() 
*schema.Resource {
                                Description: "the netmask for the storage 
network IP range",
                                Type:        schema.TypeString,
                                Required:    true,
+                               ForceNew:    true,
                        },
                        "pod_id": {
                                Description: "the Pod ID for the storage 
network IP range",
@@ -58,12 +59,14 @@ func resourceCloudStackStorageNetworkIpRange() 
*schema.Resource {
                                Description: "the beginning IP address in the 
storage network IP range",
                                Type:        schema.TypeString,
                                Required:    true,
+                               ForceNew:    true,
                        },
                        "end_ip": {
                                Description: "the ending IP address in the 
storage network IP range",
                                Type:        schema.TypeString,
                                Optional:    true,
                                Computed:    true,
+                               ForceNew:    true,
                        },
                        "vlan": {
                                Description: "the optional VLAN of the storage 
network IP range",
@@ -136,17 +139,17 @@ func resourceCloudStackStorageNetworkIpRangeUpdate(d 
*schema.ResourceData, meta
 
        p := cs.Network.NewUpdateStorageNetworkIpRangeParams(d.Id())
 
-       if v, ok := d.GetOk("netmask"); ok {
-               p.SetNetmask(v.(string))
+       if d.HasChange("netmask") {
+               p.SetNetmask(d.Get("netmask").(string))
        }
-       if v, ok := d.GetOk("start_ip"); ok {
-               p.SetStartip(v.(string))
+       if d.HasChange("start_ip") {
+               p.SetStartip(d.Get("start_ip").(string))
        }
-       if v, ok := d.GetOk("end_ip"); ok {
-               p.SetEndip(v.(string))
+       if d.HasChange("end_ip") {
+               p.SetEndip(d.Get("end_ip").(string))
        }
-       if v, ok := d.GetOk("vlan"); ok {
-               p.SetVlan(v.(int))
+       if d.HasChange("vlan") {
+               p.SetVlan(d.Get("vlan").(int))
        }
 
        _, err := cs.Network.UpdateStorageNetworkIpRange(p)
diff --git a/website/docs/r/project.html.markdown 
b/website/docs/r/project.html.markdown
index 8e0bfe9..46a5f11 100644
--- a/website/docs/r/project.html.markdown
+++ b/website/docs/r/project.html.markdown
@@ -36,7 +36,10 @@ resource "cloudstack_project" "myproject" {
 The following arguments are supported:
 
 * `name` - (Required) The name of the project.
-* `display_text` - (Required) The display text of the project. Required for 
API version 4.18 and lower compatibility. This requirement will be removed when 
support for API versions older than 4.18 is dropped.
+* `display_text` - (Optional) The display text of the project.
+* `displaytext` - (Optional, **Deprecated**) Use `display_text` instead. 
Retained for
+  backwards compatibility with existing state files. Conflicts with
+  `display_text` - set only one of the two in config.
 * `domain` - (Optional) The domain where the project will be created. This 
cannot be changed after the project is created.
 * `account` - (Optional) The account who will be Admin for the project. 
Requires `domain` to be set. This can be updated after the project is created.
 * `accountid` - (Optional) The ID of the account owning the project. This can 
be updated after the project is created.
diff --git a/website/docs/r/service_offering.html.markdown 
b/website/docs/r/service_offering.html.markdown
index 6a5754f..f8feadf 100644
--- a/website/docs/r/service_offering.html.markdown
+++ b/website/docs/r/service_offering.html.markdown
@@ -109,3 +109,14 @@ $ terraform import cloudstack_service_offering.example 
<SERVICEOFFERINGID>
 *NOTE: The importer looks up the service offering by ID and resolves the
 required `name` attribute from it, so it does not need to be set in the
 configuration beforehand.*
+
+*WARNING: After import, the resource's state is refreshed with the imported
+offering's actual `cpu_number`, `cpu_speed`, `memory`, and other `ForceNew`
+attribute values. If your `.tf` configuration for the imported resource
+leaves any of those `ForceNew` attributes unset, `terraform plan` will show
+a destroy-and-recreate of the offering, since an unset attribute is treated
+as its zero value and compared against the real imported value. After
+importing, write out the full set of `ForceNew` attributes in your
+configuration (matching the values shown by `terraform state show`) before
+running `terraform plan`, or the plan may propose replacing a live, possibly
+in-use offering.*
diff --git a/website/docs/r/storage_network_ip_range.html.markdown 
b/website/docs/r/storage_network_ip_range.html.markdown
index 9deb789..cc9fff3 100644
--- a/website/docs/r/storage_network_ip_range.html.markdown
+++ b/website/docs/r/storage_network_ip_range.html.markdown
@@ -33,11 +33,28 @@ The following arguments are supported:
   this forces a new resource to be created.
 - `gateway` - (Required) The gateway for the storage network IP range. Changing
   this forces a new resource to be created.
-- `netmask` - (Required) The netmask for the storage network IP range.
+- `netmask` - (Required) The netmask for the storage network IP range. Changing
+  this forces a new resource to be created.
 - `start_ip` - (Required) The beginning IP address in the storage network IP 
range.
+  Changing this forces a new resource to be created.
 - `end_ip` - (Optional) The ending IP address in the storage network IP range.
+  Changing this forces a new resource to be created.
 - `vlan` - (Optional) The optional VLAN of the storage network IP range.
 
+`netmask`, `start_ip`, and `end_ip` force replacement rather than an in-place
+update: CloudStack's `updateStorageNetworkIpRange` API validates a new range
+against the record's own current IPs without excluding the record being
+updated. An edit that keeps any overlap with the current range (for example
+shrinking or extending it) fails with an IP overlap error against itself;
+only moving to a range fully disjoint from the current one would succeed via
+the API. Since Terraform can't tell which case applies before attempting the
+update, these fields always force a replacement.
+
+Replacement is more destructive than the update it replaces: the existing
+range is deleted before the new one is created (the two would overlap, so
+`create_before_destroy` can't help), so the storage network briefly has no
+IP range configured for this pod during the apply.
+
 ## Attributes Reference
 
 The following attributes are exported:

Reply via email to