Repository: canonical/terraform-provider-maas
Pull request #310: feat: 72-erase-disks-when-destroying-instances
Author: login: tobiasdemendonca
Created: 2025-04-23T09:07:35Z | Merged: 2025-04-24T09:27:09Z

## Description of changes

Add ability to erase disks when destroying the resource `maas_instance`. 

## Issue or ticket link (if applicable)

Resolves: #72 
Resolves: #260

## Checklist

- [x] I have written a PR title that follows the advice in DEVELOPMENT.md with the title format `type(scope): title`
- [x] I have written a description of the changes in the PR that is clear and concise.
- [x] I have updated the documentation to reflect the changes.
- [x] I have added or updated the tests to reflect the changes.
- [x] I have run the unit tests and they pass.
- [x] I have run the acceptance tests and they pass.



Diff:
diff --git a/docs/resources/instance.md b/docs/resources/instance.md
index ed592daa..fa1b17a8 100644
--- a/docs/resources/instance.md
+++ b/docs/resources/instance.md
@@ -42,6 +42,7 @@ resource "maas_instance" "kvm" {
 - `allocate_params` (Block List, Max: 1) Nested argument with the constraints used to machine allocation. Defined below. (see [below for nested schema](#nestedblock--allocate_params))
 - `deploy_params` (Block List, Max: 1) Nested argument with the config used to deploy the allocated machine. Defined below. (see [below for nested schema](#nestedblock--deploy_params))
 - `network_interfaces` (Block Set) Specifies a network interface configuration done before the machine is deployed. Parameters defined below. This argument is processed in [attribute-as-blocks mode](https://www.terraform.io/docs/configuration/attr-as-blocks.html). (see [below for nested schema](#nestedblock--network_interfaces))
+- `release_params` (Block List, Max: 1) Parameters used to release the allocated machine when the resource is destroyed. (see [below for nested schema](#nestedblock--release_params))
 - `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))
 
 ### Read-Only
@@ -97,6 +98,18 @@ Optional:
 - `subnet_cidr` (String) An existing subnet CIDR used to configure the network interface. Unless `ip_address` is defined, a free IP address is allocated from the subnet.
 
 
+<a id="nestedblock--release_params"></a>
+### Nested Schema for `release_params`
+
+Optional:
+
+- `comment` (String) A comment to be added to the event log when the machine is released.
+- `erase` (Boolean) Erase the disk when releasing.
+- `force` (Boolean) Force the release of the machine.
+- `quick_erase` (Boolean) Use quick erase. Wipe 2MiB at the start and at the end of the drive to make data recovery inconvenient and unlikely to happen by accident. This is not secure.
+- `secure_erase` (Boolean) Use the drive's secure erase feature if available.  In some cases, this can be much faster than overwriting the drive. Some drives implement secure erasure by overwriting themselves so this could still be slow.
+
+
 <a id="nestedblock--timeouts"></a>
 ### Nested Schema for `timeouts`
 
diff --git a/maas/resource_maas_instance.go b/maas/resource_maas_instance.go
index ae7676bd..e362624f 100644
--- a/maas/resource_maas_instance.go
+++ b/maas/resource_maas_instance.go
@@ -18,6 +18,7 @@ func resourceMAASInstance() *schema.Resource {
 		CreateContext: resourceInstanceCreate,
 		ReadContext:   resourceInstanceRead,
 		DeleteContext: resourceInstanceDelete,
+		UpdateContext: resourceInstanceUpdate,
 		Importer: &schema.ResourceImporter{
 			StateContext: func(ctx context.Context, d *schema.ResourceData, meta any) ([]*schema.ResourceData, error) {
 				client := meta.(*ClientConfig).Client
@@ -197,6 +198,41 @@ func resourceMAASInstance() *schema.Resource {
 				Computed:    true,
 				Description: "The deployed MAAS machine pool name.",
 			},
+			"release_params": {
+				Type:        schema.TypeList,
+				Optional:    true,
+				MaxItems:    1,
+				Description: "Parameters used to release the allocated machine when the resource is destroyed.",
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"comment": {
+							Type:        schema.TypeString,
+							Optional:    true,
+							Description: "A comment to be added to the event log when the machine is released.",
+						},
+						"erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Erase the disk when releasing.",
+						},
+						"force": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Force the release of the machine.",
+						},
+						"quick_erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Use quick erase. Wipe 2MiB at the start and at the end of the drive to make data recovery inconvenient and unlikely to happen by accident. This is not secure.",
+						},
+						"secure_erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Use the drive's secure erase feature if available.  In some cases, this can be much faster than overwriting the drive. Some drives implement secure erasure by overwriting themselves so this could still be slow.",
+						},
+					},
+				},
+			},
 			"tags": {
 				Type:        schema.TypeSet,
 				Computed:    true,
@@ -283,17 +319,24 @@ func resourceInstanceRead(ctx context.Context, d *schema.ResourceData, meta any)
 	return nil
 }
 
+func resourceInstanceUpdate(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
+	// Dummy update to allow release params to be updated.
+	return resourceInstanceRead(ctx, d, meta)
+}
+
 func resourceInstanceDelete(ctx context.Context, d *schema.ResourceData, meta any) diag.Diagnostics {
 	client := meta.(*ClientConfig).Client
 
+	releaseParams := getReleaseParams(d)
+
 	// Release MAAS machine
-	err := client.Machines.Release([]string{d.Id()}, "Released by Terraform")
+	_, err := client.Machine.Release(d.Id(), releaseParams)
 	if err != nil {
 		return diag.FromErr(err)
 	}
 
 	// Wait MAAS machine to be released
-	_, err = waitForMachineStatus(ctx, client, d.Id(), []string{"Releasing"}, []string{"Ready"}, d.Timeout(schema.TimeoutDelete))
+	_, err = waitForMachineStatus(ctx, client, d.Id(), []string{"Releasing", "Disk erasing"}, []string{"Ready"}, d.Timeout(schema.TimeoutDelete))
 	if err != nil {
 		return diag.FromErr(err)
 	}
@@ -341,6 +384,25 @@ func getMachineDeployParams(d *schema.ResourceData) *entity.MachineDeployParams
 	return &entity.MachineDeployParams{}
 }
 
+func getReleaseParams(d *schema.ResourceData) *entity.MachineReleaseParams {
+	if p, ok := d.GetOk("release_params"); ok {
+		releaseParamsData := p.([]any)
+		if releaseParamsData[0] != nil {
+			releaseParams := releaseParamsData[0].(map[string]any)
+
+			return &entity.MachineReleaseParams{
+				Comment:     releaseParams["comment"].(string),
+				Erase:       releaseParams["erase"].(bool),
+				Force:       releaseParams["force"].(bool),
+				QuickErase:  releaseParams["quick_erase"].(bool),
+				SecureErase: releaseParams["secure_erase"].(bool),
+			}
+		}
+	}
+
+	return &entity.MachineReleaseParams{}
+}
+
 func configureInstanceNetworkInterfaces(client *client.Client, d *schema.ResourceData, machine *entity.Machine) error {
 	for _, networkInterface := range d.Get("network_interfaces").(*schema.Set).List() {
 		n := networkInterface.(map[string]any)
diff --git a/maas/resource_maas_instance_test.go b/maas/resource_maas_instance_test.go
new file mode 100644
index 00000000..1eb2de9c
--- /dev/null
+++ b/maas/resource_maas_instance_test.go
@@ -0,0 +1,211 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
+
+	baseChecks := []resource.TestCheckFunc{
+		testAccMAASInstanceCheckExists("maas_instance.test"),
+		resource.TestCheckResourceAttr("maas_instance.test", "hostname", hostname),
+		resource.TestCheckResourceAttr("maas_instance.test", "memory", "4096"),
+		resource.TestCheckResourceAttr("maas_instance.test", "cpu_count", "1"),
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:     func() { testutils.PreCheck(t, []string{"TF_ACC_VM_HOST_ID"}) },
+		Providers:    testutils.TestAccProviders,
+		ErrorCheck:   func(err error) error { return err },
+		CheckDestroy: testAccMAASInstanceCheckDestroy,
+		Steps: []resource.TestStep{
+			// Test creation
+			{
+				Config: testAccMAASInstanceConfigBasic(vmHost, hostname),
+				Check:  resource.ComposeTestCheckFunc(baseChecks...),
+			},
+			// Test update
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname) + testAccMAASInstanceConfigReleaseParams(comment, erase, force, quickErase, secureErase),
+				Check: resource.ComposeTestCheckFunc(append(
+					baseChecks,
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.#", "1"),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.comment", comment),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.erase", erase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.force", force),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.quick_erase", quickErase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.secure_erase", secureErase),
+				)...,
+				),
+			},
+			// Test destroy leaves the machine in a ready state
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname),
+				Check:  testAccMAASInstanceCheckMachineLogsForDestroy(hostname, erase == "true"),
+			},
+		},
+	})
+}
+
+// Check logs for relevant events to determine if the machine was released as expected during destroy
+func testAccMAASInstanceCheckMachineLogsForDestroy(hostname string, erase bool) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		conn := testutils.TestAccProvider.Meta().(*maas.ClientConfig).Client
+
+		params := entity.EventParams{
+			Hostname: hostname,
+		}
+
+		events, err := conn.Events.Get(&params)
+		if err != nil {
+			return err
+		}
+
+		if len(events.Events) == 0 {
+			return fmt.Errorf("no events found for hostname %s", hostname)
+		}
+
+		// Check through all events to see if the machine was released as expected
+		wasErased := false
+		wasReleased := false
+
+		for _, event := range events.Events {
+			if event.Type == "Disks erased" {
+				wasErased = true
+			}
+
+			if event.Type == "Released" {
+				wasReleased = true
+			}
+		}
+
+		if !wasReleased {
+			return fmt.Errorf("machine %s was not released as expected", hostname)
+		}
+
+		if wasErased != erase {
+			return fmt.Errorf("machine %s did not have disks erased as expected", hostname)
+		}
+
+		return nil
+	}
+}
+
+func testAccMAASInstanceCheckExists(rn string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[rn]
+		if !ok {
+			return fmt.Errorf("not found: %s", rn)
+		}
+
+		if rs.Primary.ID == "" {
+			return fmt.Errorf("resource id not set: %s", rn)
+		}
+
+		conn := testutils.TestAccProvider.Meta().(*maas.ClientConfig).Client
+
+		gotMachine, err := conn.Machine.Get(rs.Primary.ID)
+		if err != nil {
+			return err
+		}
+
+		if gotMachine.SystemID != rs.Primary.ID {
+			return fmt.Errorf("machine ID %s does not match expected id %s", gotMachine.SystemID, rs.Primary.ID)
+		}
+
+		return nil
+	}
+}
+
+func testAccMAASInstanceCheckDestroy(s *terraform.State) error {
+	conn := testutils.TestAccProvider.Meta().(*maas.ClientConfig).Client
+
+	for _, rs := range s.RootModule().Resources {
+		if rs.Type != "maas_instance" {
+			continue
+		}
+
+		// Get the machine from maas and check if it exists
+		response, err := conn.Machine.Get(rs.Primary.ID)
+		if err == nil {
+			if response != nil && response.SystemID == rs.Primary.ID {
+				return fmt.Errorf("instance %s still exists", rs.Primary.ID)
+			}
+		}
+
+		// If the error is equivalent to a 404, the instance was destroyed as expected.
+		if !strings.Contains(err.Error(), "404 Not Found") {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func testAccMAASInstanceConfigSetup(vmHost, hostname string) string {
+	return fmt.Sprintf(
+		`
+resource "maas_vm_host_machine" "test" {
+  vm_host  = %q
+  cores    = 1
+  memory   = 4096  # set to above the default
+  hostname = %q
+}
+
+`, vmHost, hostname)
+}
+
+func testAccMAASInstanceConfigBasic(vmHost, hostname string) string {
+	return fmt.Sprintf(`
+%s 
+
+resource "maas_instance" "test" {
+  allocate_params {
+    hostname      = maas_vm_host_machine.test.hostname
+    min_memory    = 4000
+    min_cpu_count = 1
+  }
+}
+`, testAccMAASInstanceConfigSetup(vmHost, hostname))
+}
+
+func testAccMAASInstanceConfigReleaseParams(comment, erase, force, quickErase, secureErase string) string {
+	return fmt.Sprintf(
+		`
+resource "maas_instance" "test" {
+  release_params {
+    comment      = %q
+    erase        = %q
+    force        = %q
+    quick_erase  = %q
+    secure_erase = %q	
+  }
+
+  allocate_params {
+    hostname      = maas_vm_host_machine.test.hostname
+    min_memory    = 4000
+    min_cpu_count = 1
+  }
+
+}
+`, comment, erase, force, quickErase, secureErase)
+}


Reviews:
user: login: tobiasdemendonca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T09:40:48Z
user: login: tobiasdemendonca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T12:41:42Z
user: login: tobiasdemendonca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T12:42:01Z
user: login: SK1Y101
body: Awesome! a few questions clarifying my understanding, but looks good!!
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-23T15:32:22Z
user: login: tobiasdemendonca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T16:26:03Z
user: login: tobiasdemendonca
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-23T16:37:56Z
user: login: skatsaounis
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-24T06:19:17Z
user: login: skatsaounis
body: LGTM +1
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-24T06:22:22Z

Review comments:
diff_hunk: @@ -0,0 +1,192 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/canonical/gomaasclient/entity/node"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	// Required to check if the machine is released into its read state after destroy
+	var machine entity.Machine
+
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
+
+	baseChecks := []resource.TestCheckFunc{
+		testAccMAASInstanceCheckExists("maas_instance.test", &machine),
+		resource.TestCheckResourceAttr("maas_instance.test", "hostname", hostname),
+		resource.TestCheckResourceAttr("maas_instance.test", "memory", "4096"),
+		resource.TestCheckResourceAttr("maas_instance.test", "cpu_count", "1"),
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:     func() { testutils.PreCheck(t, []string{"TF_ACC_VM_HOST_ID"}) },
+		Providers:    testutils.TestAccProviders,
+		ErrorCheck:   func(err error) error { return err },
+		CheckDestroy: testAccMAASInstanceCheckDestroy,
+		Steps: []resource.TestStep{
+			// Test creation
+			{
+				Config: testAccMAASInstanceConfigBasic(vmHost, hostname),
+				Check:  resource.ComposeTestCheckFunc(baseChecks...),
+			},
+			// Test update
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname) + testAccMAASInstanceConfigReleaseParams(comment, erase, force, quickErase, secureErase),
+				Check: resource.ComposeTestCheckFunc(append(
+					baseChecks,
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.#", "1"),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.comment", comment),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.erase", erase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.force", force),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.quick_erase", quickErase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.secure_erase", secureErase),
+				)...,
+				),
+			},
+			// Test destroy leaves the machine in a ready state
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname),
+				Check:  testAccMAASInstanceCheckMachineInStatus(machine.SystemID, node.StatusReady),
+			},
path: maas/resource_maas_instance_test.go
user: login: tobiasdemendonca
body: Can't think of a better way of checking if release has been called with the correct parameters. `client.Events.Get` does not show the log comments I was hoping for
created_at: 2025-04-23T09:40:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,6 +98,18 @@ Optional:
 - `subnet_cidr` (String) An existing subnet CIDR used to configure the network interface. Unless `ip_address` is defined, a free IP address is allocated from the subnet.
 
 
+<a id="nestedblock--release_params"></a>
+### Nested Schema for `release_params`
+
+Optional:
+
+- `comment` (String) A comment to be added to the event log when the machine is released.
+- `erase` (Boolean) Erase the disk when releasing.
+- `force` (Boolean) Force the release of the machine. If the machine was deployed as a KVM host, all machines inside the host will be deleted. Use with caution.
path: docs/resources/instance.md
user: login: tobiasdemendonca
body: I guess the machine will never be deployed as the kvm host with this resource, so I will remove this part of the description with the rest of the review comments
created_at: 2025-04-23T12:41:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -97,6 +98,18 @@ Optional:
 - `subnet_cidr` (String) An existing subnet CIDR used to configure the network interface. Unless `ip_address` is defined, a free IP address is allocated from the subnet.
 
 
+<a id="nestedblock--release_params"></a>
+### Nested Schema for `release_params`
+
+Optional:
+
+- `comment` (String) A comment to be added to the event log when the machine is released.
+- `erase` (Boolean) Erase the disk when releasing.
+- `force` (Boolean) Force the release of the machine. If the machine was deployed as a KVM host, all machines inside the host will be deleted. Use with caution.
path: docs/resources/instance.md
user: login: tobiasdemendonca
body: Note that descriptions were taken from https://maas.io/docs/machine
created_at: 2025-04-23T12:42:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +198,41 @@ func resourceMAASInstance() *schema.Resource {
 				Computed:    true,
 				Description: "The deployed MAAS machine pool name.",
 			},
+			"release_params": {
+				Type:        schema.TypeList,
+				Optional:    true,
+				MaxItems:    1,
+				Description: "Parameters used to release the allocated machine when the resource is destroyed.",
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"comment": {
+							Type:        schema.TypeString,
+							Optional:    true,
+							Description: "A comment to be added to the event log when the machine is released.",
+						},
+						"erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Erase the disk when releasing.",
+						},
+						"force": {
path: maas/resource_maas_instance.go
user: login: SK1Y101
body: I know the MAAS API has this as force, but i'm wondering whether `force_release` or something slightly more informative might be useful for the terraform side @skatsaounis ?
created_at: 2025-04-23T15:11:50Z
author_association: MEMBER
diff_hunk: @@ -197,6 +198,41 @@ func resourceMAASInstance() *schema.Resource {
 				Computed:    true,
 				Description: "The deployed MAAS machine pool name.",
 			},
+			"release_params": {
+				Type:        schema.TypeList,
+				Optional:    true,
+				MaxItems:    1,
+				Description: "Parameters used to release the allocated machine when the resource is destroyed.",
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"comment": {
+							Type:        schema.TypeString,
+							Optional:    true,
+							Description: "A comment to be added to the event log when the machine is released.",
+						},
+						"erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Erase the disk when releasing.",
+						},
+						"force": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Force the release of the machine. If the machine was deployed as a KVM host, all machines inside the host will be deleted. Use with caution.",
path: maas/resource_maas_instance.go
user: login: SK1Y101
body: also yeah of course slightly update if this won't be a KVM host, but you've covered this already above
created_at: 2025-04-23T15:12:48Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,211 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
path: maas/resource_maas_instance_test.go
user: login: SK1Y101
body: won't this violate the `MaxItems: 1` constraint on the release params, or am I misunderstanding something?

actually i think I've realised, that allows you to only specify release params once per machine, rather than only include one of the sub-fields at a time?
created_at: 2025-04-23T15:23:28Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,211 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
+
+	baseChecks := []resource.TestCheckFunc{
+		testAccMAASInstanceCheckExists("maas_instance.test"),
+		resource.TestCheckResourceAttr("maas_instance.test", "hostname", hostname),
+		resource.TestCheckResourceAttr("maas_instance.test", "memory", "4096"),
+		resource.TestCheckResourceAttr("maas_instance.test", "cpu_count", "1"),
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:     func() { testutils.PreCheck(t, []string{"TF_ACC_VM_HOST_ID"}) },
+		Providers:    testutils.TestAccProviders,
+		ErrorCheck:   func(err error) error { return err },
+		CheckDestroy: testAccMAASInstanceCheckDestroy,
+		Steps: []resource.TestStep{
+			// Test creation
+			{
+				Config: testAccMAASInstanceConfigBasic(vmHost, hostname),
+				Check:  resource.ComposeTestCheckFunc(baseChecks...),
+			},
+			// Test update
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname) + testAccMAASInstanceConfigReleaseParams(comment, erase, force, quickErase, secureErase),
+				Check: resource.ComposeTestCheckFunc(append(
+					baseChecks,
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.#", "1"),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.comment", comment),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.erase", erase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.force", force),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.quick_erase", quickErase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.secure_erase", secureErase),
+				)...,
+				),
+			},
+			// Test destroy leaves the machine in a ready state
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname),
+				Check:  testAccMAASInstanceCheckMachineLogsForDestroy(hostname, erase == "true"),
+			},
+		},
+	})
+}
+
+// Check logs for relevant events to determine if the machine was released as expected during destroy
+func testAccMAASInstanceCheckMachineLogsForDestroy(hostname string, erase bool) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		conn := testutils.TestAccProvider.Meta().(*maas.ClientConfig).Client
+
+		params := entity.EventParams{
+			Hostname: hostname,
+		}
+
+		events, err := conn.Events.Get(&params)
+		if err != nil {
+			return err
+		}
+
+		if len(events.Events) == 0 {
+			return fmt.Errorf("no events found for hostname %s", hostname)
+		}
+
+		// Check through all events to see if the machine was released as expected
+		wasErased := false
+		wasReleased := false
+
+		for _, event := range events.Events {
+			if event.Type == "Disks erased" {
+				wasErased = true
+			}
+
+			if event.Type == "Released" {
+				wasReleased = true
+			}
path: maas/resource_maas_instance_test.go
user: login: SK1Y101
body: you could probably also exit early here with something like `if (wasReleased && wasErased) { break }`
Not that there will be a huge performance hit probably if you do look at the rest of the event log
created_at: 2025-04-23T15:27:59Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,211 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
path: maas/resource_maas_instance_test.go
user: login: tobiasdemendonca
body: Yes once per `maas_instance` resource, see `deploy_params` or `allocate_params` in this resource as an example
created_at: 2025-04-23T16:26:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,211 @@
+package maas_test
+
+import (
+	// "encoding/json"
+	"fmt"
+	"os"
+
+	"strings"
+	"terraform-provider-maas/maas"
+	"terraform-provider-maas/maas/testutils"
+	"testing"
+
+	"github.com/canonical/gomaasclient/entity"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
+	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
+)
+
+func TestAccResourceMAASInstance_basic(t *testing.T) {
+	vmHost := os.Getenv("TF_ACC_VM_HOST_ID")
+	hostname := acctest.RandomWithPrefix("tf-instance")
+	comment := acctest.RandomWithPrefix("tf-instance-comment")
+	erase := "true"
+	force := "false"
+	quickErase := "true"
+	secureErase := "false"
+
+	baseChecks := []resource.TestCheckFunc{
+		testAccMAASInstanceCheckExists("maas_instance.test"),
+		resource.TestCheckResourceAttr("maas_instance.test", "hostname", hostname),
+		resource.TestCheckResourceAttr("maas_instance.test", "memory", "4096"),
+		resource.TestCheckResourceAttr("maas_instance.test", "cpu_count", "1"),
+	}
+
+	resource.ParallelTest(t, resource.TestCase{
+		PreCheck:     func() { testutils.PreCheck(t, []string{"TF_ACC_VM_HOST_ID"}) },
+		Providers:    testutils.TestAccProviders,
+		ErrorCheck:   func(err error) error { return err },
+		CheckDestroy: testAccMAASInstanceCheckDestroy,
+		Steps: []resource.TestStep{
+			// Test creation
+			{
+				Config: testAccMAASInstanceConfigBasic(vmHost, hostname),
+				Check:  resource.ComposeTestCheckFunc(baseChecks...),
+			},
+			// Test update
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname) + testAccMAASInstanceConfigReleaseParams(comment, erase, force, quickErase, secureErase),
+				Check: resource.ComposeTestCheckFunc(append(
+					baseChecks,
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.#", "1"),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.comment", comment),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.erase", erase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.force", force),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.quick_erase", quickErase),
+					resource.TestCheckResourceAttr("maas_instance.test", "release_params.0.secure_erase", secureErase),
+				)...,
+				),
+			},
+			// Test destroy leaves the machine in a ready state
+			{
+				Config: testAccMAASInstanceConfigSetup(vmHost, hostname),
+				Check:  testAccMAASInstanceCheckMachineLogsForDestroy(hostname, erase == "true"),
+			},
+		},
+	})
+}
+
+// Check logs for relevant events to determine if the machine was released as expected during destroy
+func testAccMAASInstanceCheckMachineLogsForDestroy(hostname string, erase bool) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		conn := testutils.TestAccProvider.Meta().(*maas.ClientConfig).Client
+
+		params := entity.EventParams{
+			Hostname: hostname,
+		}
+
+		events, err := conn.Events.Get(&params)
+		if err != nil {
+			return err
+		}
+
+		if len(events.Events) == 0 {
+			return fmt.Errorf("no events found for hostname %s", hostname)
+		}
+
+		// Check through all events to see if the machine was released as expected
+		wasErased := false
+		wasReleased := false
+
+		for _, event := range events.Events {
+			if event.Type == "Disks erased" {
+				wasErased = true
+			}
+
+			if event.Type == "Released" {
+				wasReleased = true
+			}
path: maas/resource_maas_instance_test.go
user: login: tobiasdemendonca
body: I initially agreed but actually realised I have to check the entire log, because if the machine has been erased when it shouldn't be expected, then this should throw an error. 

Thinking about this more thoroughly did catch an additional error in this test which I have just fixed
created_at: 2025-04-23T16:37:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -197,6 +198,41 @@ func resourceMAASInstance() *schema.Resource {
 				Computed:    true,
 				Description: "The deployed MAAS machine pool name.",
 			},
+			"release_params": {
+				Type:        schema.TypeList,
+				Optional:    true,
+				MaxItems:    1,
+				Description: "Parameters used to release the allocated machine when the resource is destroyed.",
+				Elem: &schema.Resource{
+					Schema: map[string]*schema.Schema{
+						"comment": {
+							Type:        schema.TypeString,
+							Optional:    true,
+							Description: "A comment to be added to the event log when the machine is released.",
+						},
+						"erase": {
+							Type:        schema.TypeBool,
+							Optional:    true,
+							Description: "Erase the disk when releasing.",
+						},
+						"force": {
path: maas/resource_maas_instance.go
user: login: skatsaounis
body: I have no strong preference here. Typically, I follow the API so that users that using both are not confused.
created_at: 2025-04-24T06:19:17Z
author_association: COLLABORATOR

PR comments:
user: login: tobiasdemendonca
created_at: 2025-04-23T09:15:33Z
body: Final check of the test is currently failing
author_association: CONTRIBUTOR
user: login: tobiasdemendonca
created_at: 2025-04-23T12:38:52Z
body: > Final check of the test is currently failing

Fixed
author_association: CONTRIBUTOR