diff --git a/linode/helper/instance.go b/linode/helper/instance.go index 7b89c002b..49f3ed853 100644 --- a/linode/helper/instance.go +++ b/linode/helper/instance.go @@ -305,3 +305,121 @@ func FlattenInterfaces(interfaces []linodego.InstanceConfigInterface) []map[stri } return result } + +// BootInstanceSync boots the instance with the given ID and waits for the operation to +// complete before returning. +func BootInstanceSync( + ctx context.Context, + client *linodego.Client, + instanceID, + configID, + deadlineSeconds int, +) error { + ctx = SetLogFieldBulk( + ctx, + map[string]any{ + "instance_id": instanceID, + "config_id": configID, + }, + ) + + tflog.Info(ctx, "Booting instance") + + p, err := client.NewEventPoller(ctx, instanceID, linodego.EntityLinode, linodego.ActionLinodeBoot) + if err != nil { + return fmt.Errorf("failed to initialize event poller: %s", err) + } + + tflog.Debug(ctx, "client.BootInstance(...)") + + if err := client.BootInstance(ctx, instanceID, configID); err != nil { + return fmt.Errorf("failed to boot instance: %s", err) + } + + tflog.Debug(ctx, "Waiting for instance boot to finish") + + if _, err := p.WaitForFinished(ctx, deadlineSeconds); err != nil { + return fmt.Errorf("failed to wait for instance boot: %s", err) + } + + tflog.Debug(ctx, "Instance has finished booting") + + return nil +} + +// ShutDownInstanceSync shuts down the instance with the given ID and waits for the operation to +// complete before returning. +func ShutDownInstanceSync( + ctx context.Context, + client *linodego.Client, + instanceID, + deadlineSeconds int, +) error { + ctx = tflog.SetField(ctx, "instance_id", instanceID) + + tflog.Info(ctx, "Shutting down instance") + + p, err := client.NewEventPoller(ctx, instanceID, linodego.EntityLinode, linodego.ActionLinodeShutdown) + if err != nil { + return fmt.Errorf("failed to initialize event poller: %s", err) + } + + tflog.Debug(ctx, "client.ShutdownInstance(...)") + + if err := client.ShutdownInstance(ctx, instanceID); err != nil { + return fmt.Errorf("failed to shutdown instance: %s", err) + } + + tflog.Debug(ctx, "Waiting for instance shutdown to finish") + + if _, err := p.WaitForFinished(ctx, deadlineSeconds); err != nil { + return fmt.Errorf("failed to wait for instance shutdown: %s", err) + } + + tflog.Debug(ctx, "Instance has finished shutting down") + + return nil +} + +// WaitForInstanceNonTransientStatus waits for the instance with the given ID to enter +// a non-transient status (e.g. running, offline), and returns the final status of the instance. +func WaitForInstanceNonTransientStatus( + ctx context.Context, + client *linodego.Client, + linodeID int, + timeoutSeconds int, +) (linodego.InstanceStatus, error) { + instance, err := client.GetInstance(ctx, linodeID) + if err != nil { + return "", fmt.Errorf("failed to get instance: %w", err) + } + + var targetStatus linodego.InstanceStatus + + switch instance.Status { + case linodego.InstanceBooting, linodego.InstanceRebooting: + targetStatus = linodego.InstanceRunning + + case linodego.InstanceShuttingDown: + targetStatus = linodego.InstanceOffline + + case linodego.InstanceRunning, linodego.InstanceOffline: + // Instance is offline, nothing to do here + return instance.Status, nil + + default: + return "", fmt.Errorf("cannot wait for instance to exit transient status %s", instance.Status) + } + + instance, err = client.WaitForInstanceStatus( + ctx, + instance.ID, + targetStatus, + timeoutSeconds, + ) + if err != nil { + return "", fmt.Errorf("failed to wait for instance to reach status %s: %w", targetStatus, err) + } + + return instance.Status, nil +} diff --git a/linode/instance/helpers.go b/linode/instance/helpers.go index 03569a96e..d6fb50eeb 100644 --- a/linode/instance/helpers.go +++ b/linode/instance/helpers.go @@ -1180,25 +1180,20 @@ func handleBootedUpdate( return nil } - inst, err := client.GetInstance(ctx, instanceID) - if err != nil { - return err - } - - instStatus, err := waitForRunningOrOfflineState(ctx, inst.Status, &client, instanceID) + instStatus, err := helper.WaitForInstanceNonTransientStatus(ctx, &client, instanceID, 120) if err != nil { return err } // Boot or shutdown the instance if necessary if instStatus != linodego.InstanceRunning && booted.(bool) { - if err := BootInstanceSync(ctx, &client, instanceID, configID, deadlineSeconds); err != nil { + if err := helper.BootInstanceSync(ctx, &client, instanceID, configID, deadlineSeconds); err != nil { return err } } if instStatus != linodego.InstanceOffline && !booted.(bool) { - if err := shutDownInstanceSync(ctx, client, instanceID, deadlineSeconds); err != nil { + if err := helper.ShutDownInstanceSync(ctx, &client, instanceID, deadlineSeconds); err != nil { return err } } @@ -1206,101 +1201,6 @@ func handleBootedUpdate( return nil } -func waitForRunningOrOfflineState( - ctx context.Context, status linodego.InstanceStatus, client *linodego.Client, instanceID int, -) (instStatus linodego.InstanceStatus, err error) { - // Ensure the Linode reaches a running or offline state - // if in a transition state (shutting down, booting, rebooting) - // TODO: clean up this logic - switch status { - - // These cases can be ignored - case linodego.InstanceRunning: - case linodego.InstanceOffline: - - case linodego.InstanceShuttingDown: - tflog.Info(ctx, "Awaiting instance shutdown before continuing") - - tflog.Trace(ctx, "client.WaitForInstanceStatus(...)", map[string]any{ - "status": linodego.InstanceOffline, - }) - _, err = client.WaitForInstanceStatus(ctx, instanceID, linodego.InstanceOffline, 120) - if err != nil { - return "", fmt.Errorf("failed to wait for instance offline: %s", err) - } - - instStatus = linodego.InstanceOffline - - case linodego.InstanceBooting, linodego.InstanceRebooting: - tflog.Info(ctx, "Awaiting instance boot before continuing") - - tflog.Trace(ctx, "client.WaitForInstanceStatus(...)", map[string]any{ - "status": linodego.InstanceRunning, - }) - _, err = client.WaitForInstanceStatus(ctx, instanceID, linodego.InstanceRunning, 120) - if err != nil { - return "", fmt.Errorf("failed to wait for instance running: %s", err) - } - - instStatus = linodego.InstanceRunning - - default: - return "", fmt.Errorf("instance is in unhandled state %s", status) - } - return instStatus, nil -} - -func shutDownInstanceSync(ctx context.Context, client linodego.Client, instanceID, deadlineSeconds int) error { - tflog.Info(ctx, "Shutting down instance") - - p, err := client.NewEventPoller(ctx, instanceID, linodego.EntityLinode, linodego.ActionLinodeShutdown) - if err != nil { - return fmt.Errorf("failed to initialize event poller: %s", err) - } - - tflog.Debug(ctx, "client.ShutdownInstance(...)") - - if err := client.ShutdownInstance(ctx, instanceID); err != nil { - return fmt.Errorf("failed to shutdown instance: %s", err) - } - - tflog.Debug(ctx, "Waiting for instance shutdown to finish") - - if _, err := p.WaitForFinished(ctx, deadlineSeconds); err != nil { - return fmt.Errorf("failed to wait for instance shutdown: %s", err) - } - - tflog.Debug(ctx, "Instance has finished shutting down") - - return nil -} - -func BootInstanceSync(ctx context.Context, client *linodego.Client, instanceID, configID, deadlineSeconds int) error { - ctx = tflog.SetField(ctx, "config_id", configID) - - tflog.Info(ctx, "Booting instance") - - p, err := client.NewEventPoller(ctx, instanceID, linodego.EntityLinode, linodego.ActionLinodeBoot) - if err != nil { - return fmt.Errorf("failed to initialize event poller: %s", err) - } - - tflog.Debug(ctx, "client.BootInstance(...)") - if err := client.BootInstance(ctx, instanceID, configID); err != nil { - return fmt.Errorf("failed to boot instance: %s", err) - } - - tflog.Debug(ctx, "Waiting for instance boot to finish") - - if _, err := p.WaitForFinished(ctx, deadlineSeconds); err != nil { - return fmt.Errorf("failed to wait for instance boot: %s", err) - } - - tflog.Debug(ctx, "Instance has finished booting") - - return nil -} - func getDiskSizeSum(ctx context.Context, d *schema.ResourceData, client *linodego.Client, instanceID int, ) (int, error) { @@ -1431,7 +1331,7 @@ func VPCInterfaceIncluded( func BootInstanceAfterVPCInterfaceUpdate(ctx context.Context, meta *helper.ProviderMeta, instanceID, targetConfigID, deadlineSeconds int) diag.Diagnostics { tflog.Debug(ctx, "Booting instance after VPC interface change applied") - if err := BootInstanceSync( + if err := helper.BootInstanceSync( ctx, &meta.Client, instanceID, targetConfigID, deadlineSeconds, ); err != nil { return diag.Errorf("failed to boot instance after VPC interface change applied: %s", err) @@ -1452,23 +1352,19 @@ func ShutdownInstanceForVPCInterfaceUpdate(ctx context.Context, client *linodego } func SafeShutdownInstance(ctx context.Context, client *linodego.Client, instanceID, deadlineSeconds int) error { - instance, err := client.GetInstance(ctx, instanceID) - if err != nil { - return fmt.Errorf("failed to get instance %d: %s", instanceID, err) - } - tflog.Debug(ctx, "Shutting down Linode instance") - if _, err := waitForRunningOrOfflineState( - ctx, instance.Status, client, instance.ID, - ); err != nil { + instanceStatus, err := helper.WaitForInstanceNonTransientStatus( + ctx, client, instanceID, 120, + ) + if err != nil { return fmt.Errorf( - "failed waiting for instance %d to be in running or offline state: %s", instance.ID, err, + "failed waiting for instance %d to be in running or offline state: %s", instanceID, err, ) } - if instance.Status != linodego.InstanceOffline { - if err := shutDownInstanceSync( - ctx, *client, instance.ID, deadlineSeconds, + if instanceStatus != linodego.InstanceOffline { + if err := helper.ShutDownInstanceSync( + ctx, client, instanceID, deadlineSeconds, ); err != nil { return fmt.Errorf("failed to shutdown instance: %s", err) } diff --git a/linode/instancedisk/framework_resource.go b/linode/instancedisk/framework_resource.go index f31c3e6e7..e357ca9ee 100644 --- a/linode/instancedisk/framework_resource.go +++ b/linode/instancedisk/framework_resource.go @@ -3,10 +3,11 @@ package instancedisk import ( "context" "fmt" - "reflect" "strconv" "time" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-log/tflog" @@ -14,7 +15,6 @@ import ( "github.com/hashicorp/terraform-plugin-framework/types" "github.com/linode/linodego" "github.com/linode/terraform-provider-linode/v2/linode/helper" - "github.com/linode/terraform-provider-linode/v2/linode/instance" ) const ( @@ -245,12 +245,12 @@ func (r *Resource) Update( } if !state.Size.Equal(plan.Size) { - if err := handleDiskResize( - ctx, client, linodeID, id, size, timeoutSeconds, - ); err != nil { - resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Resize Disk %d", id), err.Error(), - ) + resp.Diagnostics.Append( + resizeDiskSync( + ctx, client, r.Meta, linodeID, id, size, timeoutSeconds, + )..., + ) + if resp.Diagnostics.HasError() { return } } @@ -336,122 +336,40 @@ func (r *Resource) Delete( ctx, cancel := context.WithTimeout(ctx, deleteTimeout) defer cancel() - configID, err := helper.GetCurrentBootedConfig(ctx, client, linodeID) - if err != nil { - resp.Diagnostics.AddWarning( - fmt.Sprintf("Failed to Get the Current Booted Config of Linode %d", linodeID), - fmt.Sprintf( - "Will attempt to delete disk without without rebooting the instance. Error: %s", - err.Error(), - ), - ) - } - - shouldShutdown := configID != 0 - diskInConfig, err := diskInConfig(ctx, client, id, linodeID, configID) - if err != nil { - resp.Diagnostics.AddWarning( - fmt.Sprintf( - "Failed to Check If Disk (%d) is Used in the Booted Config (%d) of Linode Instance (%d)", - id, configID, linodeID, - ), - fmt.Sprintf( - "%s\nWill attempt to delete disk without without rebooting the instance.", - err.Error(), - ), - ) - } - - // Shutdown instance if active - if shouldShutdown { - if r.Meta.Config.SkipInstanceReadyPoll.ValueBool() { - resp.Diagnostics.AddError( - "Linode Instance Shutdown is Required for this Disk Deletion", - "Please consider set please consider setting 'skip_implicit_reboots' "+ - "to true in the Linode provider config.", + d := runDiskOperation( + ctx, client, r.Meta, linodeID, timeoutSeconds, func() (resultDiag diag.Diagnostics) { + tflog.Info(ctx, "Deleting instance disk") + p, err := client.NewEventPollerWithSecondary( + ctx, + linodeID, + linodego.EntityLinode, + id, + linodego.ActionDiskDelete, ) - return - } - if err := instance.SafeShutdownInstance( - ctx, client, linodeID, timeoutSeconds, - ); err != nil { - resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Shutdown Linode Instance %d", linodeID), - err.Error(), - ) - } - } + if err != nil { + resp.Diagnostics.AddError("Failed to initialize event poller", err.Error()) + return + } + + tflog.Debug(ctx, "client.DeleteInstanceDisk(...)") + if err := client.DeleteInstanceDisk(ctx, linodeID, id); err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to delete Linode instance disk %d", id), err.Error(), + ) + return + } + + if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { + resp.Diagnostics.AddError( + "Failed to wait for Linode instance disk deletion to finish", err.Error(), + ) + return + } - tflog.Info(ctx, "Deleting instance disk") - p, err := client.NewEventPollerWithSecondary( - ctx, - linodeID, - linodego.EntityLinode, - id, - linodego.ActionDiskDelete, + return + }, ) - if err != nil { - resp.Diagnostics.AddError("Failed to Initialize Event Poller", err.Error()) - return - } - - tflog.Debug(ctx, "client.DeleteInstanceDisk(...)") - if err := client.DeleteInstanceDisk(ctx, linodeID, id); err != nil { - resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Delete Linode Instance Disk %d", id), err.Error(), - ) - return - } - - if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { - resp.Diagnostics.AddError( - "Failed to Wait for Linode Instance Disk Deletion Finished", err.Error(), - ) - return - } - - // Reboot the instance if necessary - if shouldShutdown && !diskInConfig { - if err := instance.BootInstanceSync( - ctx, client, linodeID, configID, timeoutSeconds, - ); err != nil { - resp.Diagnostics.AddError( - fmt.Sprintf("Failed to Boot Instance %d", linodeID), err.Error(), - ) - } - } -} - -func diskInConfig( - ctx context.Context, client *linodego.Client, diskID, linodeID, configID int, -) (bool, error) { - if configID == 0 { - return false, nil - } - - cfg, err := client.GetInstanceConfig(ctx, linodeID, configID) - if err != nil { - return false, err - } - - if cfg.Devices == nil { - return false, nil - } - - reflectMap := reflect.ValueOf(*cfg.Devices) - - for i := 0; i < reflectMap.NumField(); i++ { - field := reflectMap.Field(i).Interface().(*linodego.InstanceConfigDevice) - if field == nil { - continue - } - - if field.DiskID == diskID { - return true, nil - } - } - - return false, nil + resp.Diagnostics.Append(d...) } func (r *Resource) ImportState( diff --git a/linode/instancedisk/helper.go b/linode/instancedisk/helper.go index ae5a29271..5fa19dfd9 100644 --- a/linode/instancedisk/helper.go +++ b/linode/instancedisk/helper.go @@ -5,11 +5,12 @@ import ( "fmt" "strconv" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" - "github.com/hashicorp/terraform-plugin-log/tflog" "github.com/linode/linodego" "github.com/linode/terraform-provider-linode/v2/linode/helper" ) @@ -25,103 +26,131 @@ func getLinodeIDAndDiskID(data ResourceModel, diags *diag.Diagnostics) (int, int return linodeID, id } -func handleDiskResize( - ctx context.Context, client *linodego.Client, instID, diskID, newSize, timeoutSeconds int, -) error { - configID, err := helper.GetCurrentBootedConfig(ctx, client, instID) +// resizeDiskSync resizes the instance with the given ID to the given size, +// shutting down/booting the instance if necessary. +func resizeDiskSync( + ctx context.Context, + client *linodego.Client, + meta *helper.FrameworkProviderMeta, + linodeID, + diskID, + newSize, + timeoutSeconds int, +) diag.Diagnostics { + return runDiskOperation( + ctx, client, meta, linodeID, timeoutSeconds, + func() (resultDiag diag.Diagnostics) { + disk, err := client.GetInstanceDisk(ctx, linodeID, diskID) + if err != nil { + resultDiag.AddError("Failed to get Linode instance disk", err.Error()) + return + } + + tflog.Info(ctx, "Resizing instance disk", map[string]any{ + "old_size": disk.Size, + "new_size": newSize, + }) + + p, err := client.NewEventPollerWithSecondary( + ctx, + linodeID, + linodego.EntityLinode, + diskID, + linodego.ActionDiskResize) + if err != nil { + resultDiag.AddError("Failed to create event poller", err.Error()) + return + } + + tflog.Debug(ctx, "client.ResizeInstanceDisk(...)", map[string]any{ + "new_size": newSize, + }) + if err := client.ResizeInstanceDisk(ctx, linodeID, diskID, newSize); err != nil { + resultDiag.AddError("Failed to resize Linode instance disk", err.Error()) + return + } + + // Wait for the resize event to complete + if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { + resultDiag.AddError( + "Failed to wait for Linode instance disk resize to complete", + err.Error(), + ) + return + } + + // Check to see if the resize operation worked + if updatedDisk, err := client.WaitForInstanceDiskStatus( + ctx, + linodeID, + disk.ID, + linodego.DiskReady, + timeoutSeconds, + ); err != nil { + resultDiag.AddError( + "Failed to wait for Linode instance disk to be ready", + err.Error(), + ) + return + } else if updatedDisk.Size != newSize { + resultDiag.AddError( + "Failed to resize Linode instance disk", + fmt.Sprintf("Disk %d has size %d, expected %d", disk.ID, disk.Size, newSize), + ) + return + } + + tflog.Debug(ctx, "Resize operation complete") + return + }, + ) +} + +func runDiskOperation( + ctx context.Context, + client *linodego.Client, + meta *helper.FrameworkProviderMeta, + linodeID int, + timeoutSeconds int, + callOperation func() diag.Diagnostics, +) (resultDiag diag.Diagnostics) { + originalStatus, err := helper.WaitForInstanceNonTransientStatus( + ctx, client, linodeID, timeoutSeconds, + ) if err != nil { - return err + resultDiag.AddError("Failed to wait for instance to exit transient status", err.Error()) + return } - shouldShutdown := configID != 0 - - if shouldShutdown { - tflog.Info(ctx, "Shutting down Instance for disk resize") - - p, err := client.NewEventPoller(ctx, instID, linodego.EntityLinode, linodego.ActionLinodeShutdown) - if err != nil { - return fmt.Errorf("failed to poll for events: %s", err) + if originalStatus == linodego.InstanceRunning { + if meta.Config.SkipImplicitReboots.ValueBool() { + resultDiag.AddError( + "Linode instance shutdown is required to complete this operation", + "Please consider setting 'skip_implicit_reboots' "+ + "to true in the Linode provider config.", + ) + return } - tflog.Debug(ctx, "client.ShutdownInstance(...)") - - if err := client.ShutdownInstance(ctx, instID); err != nil { - return fmt.Errorf("failed to shutdown instance: %s", err) + if err := helper.ShutDownInstanceSync(ctx, client, linodeID, timeoutSeconds); err != nil { + resultDiag.AddError("Failed to shutdown Linode instance", err.Error()) + return } - - tflog.Debug(ctx, "Waiting for Instance shutdown operation to complete") - - if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { - return fmt.Errorf("failed to wait for instance shutdown: %s", err) - } - - tflog.Debug(ctx, "Instance finished shutting down") } - disk, err := client.GetInstanceDisk(ctx, instID, diskID) - if err != nil { - return fmt.Errorf("failed to get instance disk: %s", err) + // Run the operation + resultDiag.Append(callOperation()...) + if resultDiag.HasError() { + return } - tflog.Info(ctx, "Resizing Instance disk", map[string]any{ - "old_size": disk.Size, - "new_size": newSize, - }) - - p, err := client.NewEventPollerWithSecondary( - ctx, - instID, - linodego.EntityLinode, - diskID, - linodego.ActionDiskResize) - if err != nil { - return fmt.Errorf("failed to poll for events: %s", err) - } - - tflog.Debug(ctx, "client.ResizeInstanceDisk(...)", map[string]any{ - "new_size": newSize, - }) - if err := client.ResizeInstanceDisk(ctx, instID, diskID, newSize); err != nil { - return fmt.Errorf("failed to resize disk: %s", err) - } - - // Wait for the resize event to complete - if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { - return fmt.Errorf("failed to wait for disk resize: %s", err) - } - - // Check to see if the resize operation worked - if updatedDisk, err := client.WaitForInstanceDiskStatus(ctx, instID, disk.ID, linodego.DiskReady, - timeoutSeconds); err != nil { - return fmt.Errorf("failed to wait for disk ready: %s", err) - } else if updatedDisk.Size != newSize { - return fmt.Errorf( - "failed to resize disk %d from %d to %d", disk.ID, disk.Size, newSize) - } - - tflog.Debug(ctx, "Resize operation complete") - - // Reboot the instance if necessary - if shouldShutdown { - tflog.Info(ctx, "Rebooting instance to previously booted config") - - p, err := client.NewEventPoller(ctx, instID, linodego.EntityLinode, linodego.ActionLinodeBoot) - if err != nil { - return fmt.Errorf("failed to poll for events: %s", err) - } - - tflog.Debug(ctx, "client.BootInstance(...)", map[string]any{ - "config_id": configID, - }) - if err := client.BootInstance(ctx, instID, configID); err != nil { - return fmt.Errorf("failed to boot instance %d %d: %s", instID, configID, err) + // Boot the instance back up if necessary + if originalStatus == linodego.InstanceRunning { + // NOTE: A config ID of 0 will boot the instance into its previously booted config + if err := helper.BootInstanceSync(ctx, client, linodeID, 0, timeoutSeconds); err != nil { + resultDiag.AddError("Failed to boot Linode instance", err.Error()) + return } - - if _, err := p.WaitForFinished(ctx, timeoutSeconds); err != nil { - return fmt.Errorf("failed to wait for instance boot: %s", err) - } - - tflog.Debug(ctx, "Reboot event finished") } return nil