mirror of
https://github.com/trailofbits/dropkit
synced 2026-06-21 14:11:54 +00:00
Auto power-on after resize and interactive disk resize option (#50)
* Auto power-on after resize and add 'nodisk' confirmation option
DigitalOcean leaves droplets powered off after resize, with no API flag
to auto-restart. Previously, `dropkit resize` completed silently with
the droplet off — users had to discover this and run `dropkit on`
manually.
Now the resize command:
1. Automatically powers the droplet back on after resize completes,
using the same pattern as `dropkit on` (with status polling and
progress messages).
2. Offers a "nodisk" answer in the confirmation prompt when disk resize
would increase disk size. This supports the common workflow of
temporarily scaling up CPU/RAM for heavy builds or benchmarks and
scaling back down later — which requires NOT resizing the disk
(disk resize is permanent and prevents future downsizing).
The prompt changes from:
Are you sure? [yes/no]
to:
Are you sure? [yes/nodisk/no]
with a tip explaining the option. The "nodisk" choice only appears
when relevant (disk flag is true AND new size has larger disk).
Expected terminal experience after resize:
✓ Resize completed successfully
Powering on droplet...
✓ Power on action started (ID: 3105287233)
Waiting for droplet to power on...
✓ Droplet powered on successfully
Droplet claude-code-box has been resized to s-2vcpu-4gb and is now active
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Make disk resize an interactive option when not specified via flag
Per reviewer feedback, instead of a "nodisk" escape hatch in the
confirmation prompt, make --disk/--no-disk a tri-state (True/False/None).
When neither flag is passed, the user is asked interactively — consistent
with how region, size, and image are already handled.
The interactive question only appears when the new size has a different
disk size. It defaults to "no" (skip disk resize) since disk resize
is permanent and prevents future downsizing.
Flow with no flags:
Changes:
Disk: 25 GB → 80 GB (+55 GB)
Disk resize is PERMANENT and cannot be undone.
Skipping disk resize keeps the option to downsize later.
Resize disk too? [yes/no] (no):
Flow with --no-disk: skips the question, shows "not resized"
Flow with --disk: skips the question, proceeds with disk resize
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Move interactive disk question before table display and drop isinstance guards
Address ret2libc's review feedback:
- Ask the interactive disk question BEFORE building the changes table,
so the displayed disk row reflects the user's actual choice
- Remove unnecessary isinstance(disk_diff, int) guards since disk_diff
is always int (computed with an isinstance ternary that defaults to 0)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
GitHub
parent
9159934207
commit
b9dde90cc6
+64
-12
@@ -3291,8 +3291,8 @@ def rename(
|
||||
def resize(
|
||||
droplet_name: str = typer.Argument(..., autocompletion=complete_droplet_or_snapshot_name),
|
||||
size: str | None = typer.Option(None, "--size", "-s", help="New size slug (e.g., s-4vcpu-8gb)"),
|
||||
disk: bool = typer.Option(
|
||||
True, "--disk/--no-disk", help="Resize disk (permanent, default: True)"
|
||||
disk: bool | None = typer.Option(
|
||||
None, "--disk/--no-disk", help="Resize disk (permanent, asked interactively if omitted)"
|
||||
),
|
||||
):
|
||||
"""
|
||||
@@ -3493,13 +3493,36 @@ def resize(
|
||||
if isinstance(new_disk, int) and isinstance(current_disk, int)
|
||||
else 0
|
||||
)
|
||||
|
||||
# If --disk/--no-disk was not explicitly passed, ask interactively
|
||||
# when the new size has a different disk. This surfaces the
|
||||
# permanent/irreversible nature of disk resize at decision time,
|
||||
# consistent with how region/size/image are handled interactively.
|
||||
# Resolved BEFORE building the table so the row reflects the choice.
|
||||
if disk is None:
|
||||
if disk_diff != 0:
|
||||
console.print()
|
||||
console.print(
|
||||
"[bold yellow]Disk resize is PERMANENT and cannot be undone.[/bold yellow]"
|
||||
)
|
||||
console.print("[dim]Skipping disk resize keeps the option to downsize later.[/dim]")
|
||||
disk_choice = Prompt.ask(
|
||||
"[bold]Resize disk too?[/bold]",
|
||||
choices=["yes", "no"],
|
||||
default="no",
|
||||
)
|
||||
disk = disk_choice == "yes"
|
||||
else:
|
||||
# No disk change — default to True (no-op for disk)
|
||||
disk = True
|
||||
|
||||
disk_change = f"{current_disk} GB → {new_disk} GB"
|
||||
if disk and disk_diff > 0:
|
||||
disk_change += f" [green](+{disk_diff} GB)[/green]"
|
||||
elif disk and disk_diff < 0:
|
||||
disk_change += f" [yellow]({disk_diff} GB)[/yellow]"
|
||||
elif not disk:
|
||||
if disk is False:
|
||||
disk_change = f"{current_disk} GB (not resized)"
|
||||
elif disk_diff > 0:
|
||||
disk_change += f" [green](+{disk_diff} GB)[/green]"
|
||||
elif disk_diff < 0:
|
||||
disk_change += f" [yellow]({disk_diff} GB)[/yellow]"
|
||||
changes_table.add_row("Disk:", disk_change)
|
||||
|
||||
# Price
|
||||
@@ -3516,16 +3539,17 @@ def resize(
|
||||
# Show warnings
|
||||
console.print()
|
||||
console.print(
|
||||
"[bold yellow]⚠ WARNING: This operation will cause downtime (droplet will be powered off)[/bold yellow]"
|
||||
"[bold yellow]⚠ WARNING: This operation will cause downtime "
|
||||
"(droplet will be powered off)[/bold yellow]"
|
||||
)
|
||||
|
||||
if disk:
|
||||
if disk and disk_diff > 0:
|
||||
console.print(
|
||||
"[bold red]⚠ WARNING: Disk resize is PERMANENT and cannot be undone![/bold red]"
|
||||
)
|
||||
else:
|
||||
elif not disk:
|
||||
console.print(
|
||||
"[dim]Note: Disk will NOT be resized. You can resize it later, but it's permanent.[/dim]"
|
||||
"[dim]Note: Disk will NOT be resized. You can resize back down later.[/dim]"
|
||||
)
|
||||
|
||||
# Confirmation
|
||||
@@ -3562,9 +3586,37 @@ def resize(
|
||||
api.wait_for_action_complete(action_id, timeout=600) # 10 minutes
|
||||
|
||||
console.print("[green]✓[/green] Resize completed successfully")
|
||||
|
||||
# Power the droplet back on — DigitalOcean leaves it powered off
|
||||
# after resize (no auto-power-on API flag exists). Since the user
|
||||
# almost certainly wants their droplet running, we power it on
|
||||
# automatically instead of leaving them to discover it's off.
|
||||
console.print()
|
||||
console.print("[dim]Powering on droplet...[/dim]")
|
||||
|
||||
power_action = api.power_on_droplet(droplet_id)
|
||||
power_action_id = power_action.get("id")
|
||||
|
||||
if not power_action_id:
|
||||
console.print(
|
||||
"[yellow]Warning: Could not get power-on action ID. "
|
||||
f"You may need to run: dropkit on {droplet_name}[/yellow]"
|
||||
)
|
||||
else:
|
||||
console.print(
|
||||
f"[green]✓[/green] Power on action started (ID: [cyan]{power_action_id}[/cyan])"
|
||||
)
|
||||
console.print("[dim]Waiting for droplet to power on...[/dim]")
|
||||
|
||||
with console.status("[cyan]Powering on...[/cyan]"):
|
||||
api.wait_for_action_complete(power_action_id, timeout=120)
|
||||
|
||||
console.print("[green]✓[/green] Droplet powered on successfully")
|
||||
|
||||
console.print()
|
||||
console.print(
|
||||
f"[bold green]Droplet {droplet_name} has been resized to {new_size_slug}[/bold green]"
|
||||
f"[bold green]Droplet {droplet_name} has been resized to "
|
||||
f"{new_size_slug} and is now active[/bold green]"
|
||||
)
|
||||
|
||||
except DigitalOceanAPIError as e:
|
||||
|
||||
Reference in New Issue
Block a user