diff --git a/cloudstack/resource_cloudstack_project.go b/cloudstack/resource_cloudstack_project.go index 9c028465..ea747434 100644 --- a/cloudstack/resource_cloudstack_project.go +++ b/cloudstack/resource_cloudstack_project.go @@ -47,6 +47,12 @@ func resourceCloudStackProject() *schema.Resource { }, "displaytext": { + Type: schema.TypeString, + Optional: true, + Deprecated: "use display_text instead", + }, + + "display_text": { Type: schema.TypeString, Optional: true, }, @@ -76,12 +82,22 @@ func resourceCloudStackProject() *schema.Resource { } } +// projectDisplayText resolves the effective display text from the new +// display_text field and the deprecated displaytext field. display_text +// wins when both are set, since it's the field new configs should use. +func projectDisplayText(d *schema.ResourceData) 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 +117,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 +342,19 @@ 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) + // Only refresh whichever of displaytext (deprecated) / display_text the + // config is actually using, so a config that only sets one of them + // doesn't see a perpetual diff on the other. + _, displaytextOk := d.GetOk("displaytext") + _, displayTextOk := d.GetOk("display_text") + if displaytextOk && !displayTextOk { + d.Set("displaytext", project.Displaytext) + } else { + 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 +422,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 +434,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 +515,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 709108d9..24d317e8 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 8e0bfe98..64a61c75 100644 --- a/website/docs/r/project.html.markdown +++ b/website/docs/r/project.html.markdown @@ -37,6 +37,9 @@ 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. +* `displaytext` - (Optional, **Deprecated**) Use `display_text` instead. Retained for + backwards compatibility with existing state files; if both `displaytext` and + `display_text` are set, `display_text` takes precedence. * `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 6a5754f1..f8feadf6 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 *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 9deb7895..5d1cf7ee 100644 --- a/website/docs/r/storage_network_ip_range.html.markdown +++ b/website/docs/r/storage_network_ip_range.html.markdown @@ -33,11 +33,20 @@ 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, so any in-place edit of these fields fails with an IP overlap error +against itself. + ## Attributes Reference The following attributes are exported: