diff --git a/internal/instance/resource_instance.go b/internal/instance/resource_instance.go index 45e230ac..25a51255 100644 --- a/internal/instance/resource_instance.go +++ b/internal/instance/resource_instance.go @@ -82,9 +82,6 @@ func (r InstanceResource) Schema(ctx context.Context, _ resource.SchemaRequest, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ Required: true, - PlanModifiers: []planmodifier.String{ - stringplanmodifier.RequiresReplace(), - }, }, "description": schema.StringAttribute{ @@ -789,13 +786,25 @@ func (r InstanceResource) Update(ctx context.Context, req resource.UpdateRequest return } - instanceName := plan.Name.ValueString() + instanceName := state.Name.ValueString() instanceState, _, err := server.GetInstanceState(instanceName) if err != nil { resp.Diagnostics.AddError(fmt.Sprintf("Failed to retrieve state of instance %q", instanceName), err.Error()) return } + // Handle instance rename if instance is already stopped. + newInstanceName := plan.Name.ValueString() + if instanceName != newInstanceName && isInstanceStopped(*instanceState) { + err := renameInstance(ctx, server, instanceName, newInstanceName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to rename instance %q", instanceName), err.Error()) + return + } + + instanceName = newInstanceName + } + // Indicates if the instance has been just started. instanceStarted := false instanceRunning := isInstanceOperational(*instanceState) @@ -832,6 +841,17 @@ func (r InstanceResource) Update(ctx context.Context, req resource.UpdateRequest } } + // Handle instance rename. + if instanceName != newInstanceName { + err := renameInstance(ctx, server, instanceName, newInstanceName) + if err != nil { + resp.Diagnostics.AddError(fmt.Sprintf("Failed to rename instance %q", instanceName), err.Error()) + return + } + + instanceName = newInstanceName + } + // Get instance. instance, etag, err := server.GetInstance(instanceName) if err != nil { @@ -1307,6 +1327,17 @@ func stopInstance(ctx context.Context, server lxd.InstanceServer, instanceName s return true, nil } +// renameInstance renames an instance with the given old name to a new name. +// Instance has to be stopped beforehand, otherwise the operation will fail. +func renameInstance(ctx context.Context, server lxd.InstanceServer, oldName string, newName string) error { + op, err := server.RenameInstance(oldName, api.InstancePost{Name: newName}) + if err != nil { + return err + } + + return op.WaitContext(ctx) +} + // waitInstanceNetwork waits for an instance with the given name to receive // an IPv4 address on any interface (excluding loopback). This should be // called only if the instance is running. diff --git a/internal/instance/resource_instance_test.go b/internal/instance/resource_instance_test.go index 0b9452f6..4422305f 100644 --- a/internal/instance/resource_instance_test.go +++ b/internal/instance/resource_instance_test.go @@ -216,6 +216,62 @@ func TestAccInstance_restartVirtualMachine(t *testing.T) { }) } +func TestAccInstance_renameInstance(t *testing.T) { + instanceNameA := acctest.GenerateName(3, "-") + instanceNameB := acctest.GenerateName(3, "-") + instanceNameC := acctest.GenerateName(3, "-") + instanceNameD := acctest.GenerateName(3, "-") + instanceNameE := acctest.GenerateName(3, "-") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.PreCheck(t) }, + ProtoV6ProviderFactories: acctest.ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + // Launch a new instance. + Config: testAccInstance_rename(instanceNameA, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("lxd_instance.instance1", "name", instanceNameA), + resource.TestCheckResourceAttr("lxd_instance.instance1", "status", "Running"), + ), + }, + { + // Stop and rename the instance. + Config: testAccInstance_rename(instanceNameB, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("lxd_instance.instance1", "name", instanceNameB), + resource.TestCheckResourceAttr("lxd_instance.instance1", "status", "Stopped"), + ), + }, + { + // Rename the instance while stopped. + Config: testAccInstance_rename(instanceNameC, false), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("lxd_instance.instance1", "name", instanceNameC), + resource.TestCheckResourceAttr("lxd_instance.instance1", "status", "Stopped"), + ), + }, + { + // Rename and start the instance. + Config: testAccInstance_rename(instanceNameD, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("lxd_instance.instance1", "name", instanceNameD), + resource.TestCheckResourceAttr("lxd_instance.instance1", "status", "Running"), + ), + }, + { + // Ensure instance rename fails when instance is running. + Config: testAccInstance_rename(instanceNameE, true), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr("lxd_instance.instance1", "name", instanceNameD), // Ensure name is unchanged. + resource.TestCheckResourceAttr("lxd_instance.instance1", "status", "Running"), + ), + ExpectError: regexp.MustCompile("Renaming of running instance not allowed"), + }, + }, + }) +} + func TestAccInstance_remoteImage(t *testing.T) { instanceName := acctest.GenerateName(2, "-") @@ -1884,6 +1940,16 @@ resource "lxd_instance" "instance1" { `, instanceName, acctest.TestImage) } +func testAccInstance_rename(name string, running bool) string { + return fmt.Sprintf(` +resource "lxd_instance" "instance1" { + name = "%s" + running = %v + image = "%s" +} + `, name, running, acctest.TestImage) +} + func testAccInstance_remoteImage(name string) string { return fmt.Sprintf(` resource "lxd_instance" "instance1" {