From 2cd02a6753057ee33fc654864360d02a41dfdbc0 Mon Sep 17 00:00:00 2001 From: Curverneur Date: Wed, 3 Dec 2025 14:02:13 +0100 Subject: [PATCH 1/6] added deleteprotection option for instances and disks --- cloudstack/resource_cloudstack_disk.go | 37 ++++++++++++++++++++-- cloudstack/resource_cloudstack_instance.go | 31 ++++++++++++++++++ website/docs/r/disk.html.markdown | 3 ++ website/docs/r/instance.html.markdown | 9 ++++-- 4 files changed, 76 insertions(+), 4 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk.go b/cloudstack/resource_cloudstack_disk.go index dc325bca..e76f7a39 100644 --- a/cloudstack/resource_cloudstack_disk.go +++ b/cloudstack/resource_cloudstack_disk.go @@ -92,13 +92,18 @@ func resourceCloudStackDisk() *schema.Resource { ForceNew: true, }, - "tags": tagsSchema(), - "reattach_on_change": { Type: schema.TypeBool, Optional: true, Default: false, }, + + "deleteprotection": { + Type: schema.TypeBool, + Optional: true, + }, + + "tags": tagsSchema(), }, } } @@ -147,6 +152,21 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro // Set the volume ID and partials d.SetId(r.Id) + // Set deleteprotection using UpdateVolume + if v, ok := d.GetOk("deleteprotection"); ok { + // p_update := cs.Volume.NewUpdateVolumeParams() + // p_update.SetDeleteprotection(v.(bool)) + p := cs.Volume.NewUpdateVolumeParams() + p.SetId(d.Id()) + p.SetDeleteprotection(v.(bool)) + + _, err := cs.Volume.UpdateVolume(p) + if err != nil { + return fmt.Errorf( + "Error updating the deleteprotection for disk %s: %s", name, err) + } + } + // Set tags if necessary err = setTags(cs, d, "Volume") if err != nil { @@ -278,6 +298,19 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro } } + // Check if the deleteprotection has changed and if so, update the deleteprotection + if d.HasChange("deleteprotection") { + p := cs.Volume.NewUpdateVolumeParams() + p.SetId(d.Id()) + p.SetDeleteprotection(d.Get("deleteprotection").(bool)) + + _, err := cs.Volume.UpdateVolume(p) + if err != nil { + return fmt.Errorf( + "Error updating the deleteprotection for disk %s: %s", name, err) + } + } + return resourceCloudStackDiskRead(d, meta) } diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 6a38ddb4..ce6a041d 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -249,6 +249,11 @@ func resourceCloudStackInstance() *schema.Resource { Optional: true, }, + "deleteprotection": { + Type: schema.TypeBool, + Optional: true, + }, + "tags": tagsSchema(), }, } @@ -479,6 +484,20 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) d.SetId(r.Id) + // Set deleteprotection using UpdateVirtualMachine + if v, ok := d.GetOk("deleteprotection"); ok { + // p_update := cs.VirtualMachine.NewUpdateVirtualMachineParams(r.Id) + // p_update.SetDeleteprotection(v.(bool)) + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + p.SetDeleteprotection(v.(bool)) + + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating the deleteprotection for instance %s: %s", name, err) + } + } + // Set tags if necessary if err = setTags(cs, d, "userVm"); err != nil { return fmt.Errorf("Error setting tags on the new instance %s: %s", name, err) @@ -873,6 +892,18 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + // Check if the deleteprotection has changed and if so, update the deleteprotection + if d.HasChange("deleteprotection") { + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + p.SetDeleteprotection(d.Get("deleteprotection").(bool)) + + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating the deleteprotection for instance %s: %s", name, err) + } + } + return resourceCloudStackInstanceRead(d, meta) } diff --git a/website/docs/r/disk.html.markdown b/website/docs/r/disk.html.markdown index 1f4e0528..15c92913 100644 --- a/website/docs/r/disk.html.markdown +++ b/website/docs/r/disk.html.markdown @@ -56,6 +56,9 @@ The following arguments are supported: * `reattach_on_change` - (Optional) Determines whether or not to detach the disk volume from the virtual machine on disk offering or size change. +* `deleteprotection` - (Optional) Set delete protection for the volume. If true, The volume will be protected from deletion. + Note: If the volume is managed by another service like autoscaling groups or CKS, delete protection will be ignored. + ## Attributes Reference The following attributes are exported: diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index ebf3f20f..d703853d 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -175,9 +175,11 @@ The following arguments are supported: * `user_data` - (Optional) The user data to provide when launching the instance. This can be either plain text or base64 encoded text. -* `userdata_id` - (Optional) The ID of a registered CloudStack user data to use for this instance. Cannot be used together with `user_data`. +* `userdata_id` - (Optional) The ID of a registered CloudStack user data to use for this instance. + Cannot be used together with `user_data`. -* `userdata_details` - (Optional) A map of key-value pairs to pass as parameters to the user data script. Only valid when `userdata_id` is specified. Keys must match the parameter names defined in the user data. +* `userdata_details` - (Optional) A map of key-value pairs to pass as parameters to the user data script. + Only valid when `userdata_id` is specified. Keys must match the parameter names defined in the user data. * `keypair` - (Optional) The name of the SSH key pair that will be used to access this instance. (Mutual exclusive with keypairs) @@ -192,6 +194,9 @@ The following arguments are supported: * `boot_mode` - (Optional) The boot mode of the instance. Can only be specified when uefi is true. Valid options are 'Legacy' and 'Secure'. +* `deleteprotection` - (Optional) Set delete protection for the virtual machine. If true, the instance will be protected from deletion. + Note: If the instance is managed by another service like autoscaling groups or CKS, delete protection will be ignored. + ## Attributes Reference The following attributes are exported: From d51a6cf4ec8c4da2af03aafb7a3f208cddfe3049 Mon Sep 17 00:00:00 2001 From: Curverneur Date: Tue, 16 Dec 2025 14:20:16 +0100 Subject: [PATCH 2/6] removed commented code --- cloudstack/resource_cloudstack_disk.go | 2 -- cloudstack/resource_cloudstack_instance.go | 2 -- 2 files changed, 4 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk.go b/cloudstack/resource_cloudstack_disk.go index e76f7a39..ee6bd0f8 100644 --- a/cloudstack/resource_cloudstack_disk.go +++ b/cloudstack/resource_cloudstack_disk.go @@ -154,8 +154,6 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro // Set deleteprotection using UpdateVolume if v, ok := d.GetOk("deleteprotection"); ok { - // p_update := cs.Volume.NewUpdateVolumeParams() - // p_update.SetDeleteprotection(v.(bool)) p := cs.Volume.NewUpdateVolumeParams() p.SetId(d.Id()) p.SetDeleteprotection(v.(bool)) diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index ce6a041d..9e8b9538 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -486,8 +486,6 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) // Set deleteprotection using UpdateVirtualMachine if v, ok := d.GetOk("deleteprotection"); ok { - // p_update := cs.VirtualMachine.NewUpdateVirtualMachineParams(r.Id) - // p_update.SetDeleteprotection(v.(bool)) p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) p.SetDeleteprotection(v.(bool)) From 255ded21024b0c2dae30f0933ce95cb3fee7a3eb Mon Sep 17 00:00:00 2001 From: Curverneur Date: Thu, 18 Dec 2025 09:49:46 +0100 Subject: [PATCH 3/6] fix typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- website/docs/r/disk.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/disk.html.markdown b/website/docs/r/disk.html.markdown index 15c92913..e4f7b333 100644 --- a/website/docs/r/disk.html.markdown +++ b/website/docs/r/disk.html.markdown @@ -56,7 +56,7 @@ The following arguments are supported: * `reattach_on_change` - (Optional) Determines whether or not to detach the disk volume from the virtual machine on disk offering or size change. -* `deleteprotection` - (Optional) Set delete protection for the volume. If true, The volume will be protected from deletion. +* `deleteprotection` - (Optional) Set delete protection for the volume. If true, the volume will be protected from deletion. Note: If the volume is managed by another service like autoscaling groups or CKS, delete protection will be ignored. ## Attributes Reference From b17f0158e03c42f4f47f02dd113c31a5e226e043 Mon Sep 17 00:00:00 2001 From: Curverneur Date: Thu, 18 Dec 2025 10:02:37 +0100 Subject: [PATCH 4/6] set computed to true Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cloudstack/resource_cloudstack_disk.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cloudstack/resource_cloudstack_disk.go b/cloudstack/resource_cloudstack_disk.go index ee6bd0f8..56bcd378 100644 --- a/cloudstack/resource_cloudstack_disk.go +++ b/cloudstack/resource_cloudstack_disk.go @@ -101,6 +101,7 @@ func resourceCloudStackDisk() *schema.Resource { "deleteprotection": { Type: schema.TypeBool, Optional: true, + Computed: true, }, "tags": tagsSchema(), From 845d6e364c1c4724c511f00ee176d4ef52f774f3 Mon Sep 17 00:00:00 2001 From: Curverneur Date: Thu, 18 Dec 2025 10:02:53 +0100 Subject: [PATCH 5/6] set computed to true Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- cloudstack/resource_cloudstack_instance.go | 1 + 1 file changed, 1 insertion(+) diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 9e8b9538..9cebbc49 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -252,6 +252,7 @@ func resourceCloudStackInstance() *schema.Resource { "deleteprotection": { Type: schema.TypeBool, Optional: true, + Computed: true, }, "tags": tagsSchema(), From 67d670ee4845a621867b6f08871b94bf99d3bd64 Mon Sep 17 00:00:00 2001 From: Curverneur Date: Tue, 17 Mar 2026 16:38:47 +0100 Subject: [PATCH 6/6] renamed delete_protection terraform key, added tests for delete_protection in instance and disk resource --- cloudstack/resource_cloudstack_disk.go | 16 ++--- cloudstack/resource_cloudstack_disk_test.go | 52 +++++++++++++++ cloudstack/resource_cloudstack_instance.go | 16 ++--- .../resource_cloudstack_instance_test.go | 63 +++++++++++++++++++ website/docs/r/disk.html.markdown | 2 +- website/docs/r/instance.html.markdown | 2 +- 6 files changed, 133 insertions(+), 18 deletions(-) diff --git a/cloudstack/resource_cloudstack_disk.go b/cloudstack/resource_cloudstack_disk.go index 56bcd378..a879b9cb 100644 --- a/cloudstack/resource_cloudstack_disk.go +++ b/cloudstack/resource_cloudstack_disk.go @@ -98,7 +98,7 @@ func resourceCloudStackDisk() *schema.Resource { Default: false, }, - "deleteprotection": { + "delete_protection": { Type: schema.TypeBool, Optional: true, Computed: true, @@ -153,8 +153,8 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro // Set the volume ID and partials d.SetId(r.Id) - // Set deleteprotection using UpdateVolume - if v, ok := d.GetOk("deleteprotection"); ok { + // Set delete protection using UpdateVolume + if v, ok := d.GetOk("delete_protection"); ok { p := cs.Volume.NewUpdateVolumeParams() p.SetId(d.Id()) p.SetDeleteprotection(v.(bool)) @@ -162,7 +162,7 @@ func resourceCloudStackDiskCreate(d *schema.ResourceData, meta interface{}) erro _, err := cs.Volume.UpdateVolume(p) if err != nil { return fmt.Errorf( - "Error updating the deleteprotection for disk %s: %s", name, err) + "Error updating the delete protection for disk %s: %s", name, err) } } @@ -297,16 +297,16 @@ func resourceCloudStackDiskUpdate(d *schema.ResourceData, meta interface{}) erro } } - // Check if the deleteprotection has changed and if so, update the deleteprotection - if d.HasChange("deleteprotection") { + // Check if the delete protection has changed and if so, update the delete protection + if d.HasChange("delete_protection") { p := cs.Volume.NewUpdateVolumeParams() p.SetId(d.Id()) - p.SetDeleteprotection(d.Get("deleteprotection").(bool)) + p.SetDeleteprotection(d.Get("delete_protection").(bool)) _, err := cs.Volume.UpdateVolume(p) if err != nil { return fmt.Errorf( - "Error updating the deleteprotection for disk %s: %s", name, err) + "Error updating the delete protection for disk %s: %s", name, err) } } diff --git a/cloudstack/resource_cloudstack_disk_test.go b/cloudstack/resource_cloudstack_disk_test.go index 76031a62..1bf850bc 100644 --- a/cloudstack/resource_cloudstack_disk_test.go +++ b/cloudstack/resource_cloudstack_disk_test.go @@ -21,6 +21,7 @@ package cloudstack import ( "fmt" + "regexp" "testing" "github.com/apache/cloudstack-go/v2/cloudstack" @@ -122,6 +123,45 @@ func TestAccCloudStackDisk_import(t *testing.T) { }) } +func TestAccCloudStackDisk_deleteProtection(t *testing.T) { + var disk cloudstack.Volume + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskDestroy, + Steps: []resource.TestStep{ + { + // create disk with delete protection enabled + Config: fmt.Sprintf(testAccCloudStackDisk_deleteProtection, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists("cloudstack_disk.foo", &disk), + resource.TestCheckResourceAttr("cloudstack_disk.foo", "delete_protection", "true"), + ), + }, + { + // attempt to destroy disk. expected to fail due to delete protection is enabled + Config: fmt.Sprintf(testAccCloudStackDisk_deleteProtection, true), + Destroy: true, + ExpectError: regexp.MustCompile(".*has delete protection enabled and cannot be deleted."), + }, + { + // disable delete protection + Config: fmt.Sprintf(testAccCloudStackDisk_deleteProtection, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackDiskExists("cloudstack_disk.foo", &disk), + resource.TestCheckResourceAttr("cloudstack_disk.foo", "delete_protection", "false"), + ), + }, + { + // destroy disk. expected to pass due to disk protection is disabledd + Config: fmt.Sprintf(testAccCloudStackDisk_deleteProtection, false), + Destroy: true, + }, + }, + }) +} + func testAccCheckCloudStackDiskExists( n string, disk *cloudstack.Volume) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -252,3 +292,15 @@ resource "cloudstack_disk" "foo" { virtual_machine_id = cloudstack_instance.foobar.id zone = cloudstack_instance.foobar.zone }` + +const testAccCloudStackDisk_deleteProtection = ` +resource "cloudstack_disk" "foo" { + name = "terraform-disk" + attach = false + disk_offering = "Small" + zone = "Sandbox-simulator" + delete_protection = %t + tags = { + terraform-tag = "true" + } +}` diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 9cebbc49..03bd75f2 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -249,7 +249,7 @@ func resourceCloudStackInstance() *schema.Resource { Optional: true, }, - "deleteprotection": { + "delete_protection": { Type: schema.TypeBool, Optional: true, Computed: true, @@ -485,15 +485,15 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) d.SetId(r.Id) - // Set deleteprotection using UpdateVirtualMachine - if v, ok := d.GetOk("deleteprotection"); ok { + // Set delete protection using UpdateVirtualMachine + if v, ok := d.GetOk("delete_protection"); ok { p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) p.SetDeleteprotection(v.(bool)) _, err := cs.VirtualMachine.UpdateVirtualMachine(p) if err != nil { return fmt.Errorf( - "Error updating the deleteprotection for instance %s: %s", name, err) + "Error updating the delete protection for instance %s: %s", name, err) } } @@ -891,15 +891,15 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } - // Check if the deleteprotection has changed and if so, update the deleteprotection - if d.HasChange("deleteprotection") { + // Check if the delete protection has changed and if so, update the deleteprotection + if d.HasChange("delete_protection") { p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) - p.SetDeleteprotection(d.Get("deleteprotection").(bool)) + p.SetDeleteprotection(d.Get("delete_protection").(bool)) _, err := cs.VirtualMachine.UpdateVirtualMachine(p) if err != nil { return fmt.Errorf( - "Error updating the deleteprotection for instance %s: %s", name, err) + "Error updating the delete_protection for instance %s: %s", name, err) } } diff --git a/cloudstack/resource_cloudstack_instance_test.go b/cloudstack/resource_cloudstack_instance_test.go index 5979aaaf..8632c966 100644 --- a/cloudstack/resource_cloudstack_instance_test.go +++ b/cloudstack/resource_cloudstack_instance_test.go @@ -295,6 +295,45 @@ func TestAccCloudStackInstance_userData(t *testing.T) { }) } +func TestAccCloudStackInstance_deleteProtection(t *testing.T) { + var instance cloudstack.VirtualMachine + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCloudStackDiskDestroy, + Steps: []resource.TestStep{ + { + // create vm with delete protection enabled + Config: fmt.Sprintf(testAccCloudStackInstance_deleteProtection, true), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists("cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "delete_protection", "true"), + ), + }, + { + // attempt to destroy vm. expected to fail due to delete protection is enabled + Config: fmt.Sprintf(testAccCloudStackInstance_deleteProtection, true), + Destroy: true, + ExpectError: regexp.MustCompile(".*has delete protection enabled and cannot be deleted."), + }, + { + // disable delete protection + Config: fmt.Sprintf(testAccCloudStackInstance_deleteProtection, false), + Check: resource.ComposeTestCheckFunc( + testAccCheckCloudStackInstanceExists("cloudstack_instance.foobar", &instance), + resource.TestCheckResourceAttr("cloudstack_instance.foobar", "delete_protection", "false"), + ), + }, + { + // destroy vm. expected to pass due to disk protection is disabledd + Config: fmt.Sprintf(testAccCloudStackInstance_deleteProtection, false), + Destroy: true, + }, + }, + }) +} + func testAccCheckCloudStackInstanceExists( n string, instance *cloudstack.VirtualMachine) resource.TestCheckFunc { return func(s *terraform.State) error { @@ -576,3 +615,27 @@ ${random_bytes.string.base64} EOF EOFTF }` + +const testAccCloudStackInstance_deleteProtection = ` +resource "cloudstack_network" "foo" { + name = "terraform-network" + display_text = "terraform-network" + cidr = "10.1.1.0/24" + network_offering = "DefaultIsolatedNetworkOfferingWithSourceNatService" + zone = "Sandbox-simulator" +} + +resource "cloudstack_instance" "foobar" { + name = "terraform-test" + display_name = "terraform-test" + service_offering= "Small Instance" + network_id = cloudstack_network.foo.id + template = "CentOS 5.6 (64-bit) no GUI (Simulator)" + zone = "Sandbox-simulator" + user_data = "foobar\nfoo\nbar" + expunge = true + delete_protection = %t + tags = { + terraform-tag = "true" + } +}` diff --git a/website/docs/r/disk.html.markdown b/website/docs/r/disk.html.markdown index e4f7b333..05d2dfb7 100644 --- a/website/docs/r/disk.html.markdown +++ b/website/docs/r/disk.html.markdown @@ -56,7 +56,7 @@ The following arguments are supported: * `reattach_on_change` - (Optional) Determines whether or not to detach the disk volume from the virtual machine on disk offering or size change. -* `deleteprotection` - (Optional) Set delete protection for the volume. If true, the volume will be protected from deletion. +* `delete_protection` - (Optional) Set delete protection for the volume. If true, the volume will be protected from deletion. Note: If the volume is managed by another service like autoscaling groups or CKS, delete protection will be ignored. ## Attributes Reference diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index d703853d..285e64bc 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -194,7 +194,7 @@ The following arguments are supported: * `boot_mode` - (Optional) The boot mode of the instance. Can only be specified when uefi is true. Valid options are 'Legacy' and 'Secure'. -* `deleteprotection` - (Optional) Set delete protection for the virtual machine. If true, the instance will be protected from deletion. +* `delete_protection` - (Optional) Set delete protection for the virtual machine. If true, the instance will be protected from deletion. Note: If the instance is managed by another service like autoscaling groups or CKS, delete protection will be ignored. ## Attributes Reference