CLI tool for managing DigitalOcean droplets for Trail of Bits engineers. Pre-configured cloud-init, Tailscale VPN (enabled by default), and SSH config management.
- Never use
pip— always useuvfor all Python operations - Always run
prek runbefore committing (orprek installto auto-run on commit) - Keep README.md in sync when adding commands or features
- Run E2E tests before pushing with
prek run --stage manual— creates a real droplet with hardcoded defaults
uv sync # Install dependencies
prek install # Set up pre-commit hooks (one-time)
prek run --stage manual # Run E2E test (before pushing)
prek run # Run all checks (ruff, ty, shellcheck, etc.)
uv run pytest # Run tests
uv run dropkit --help # CLI helpdropkit/
├── dropkit/ # CLI source (Typer entry point: main.py)
│ └── templates/ # Jinja2 cloud-init templates
└── tests/ # pytest tests
└── e2e/
- Python 3.11+ with
uv(NOT pip) - CLI: Typer + Rich
- API: Direct REST calls (requests library, no SDK)
- Config: YAML + Pydantic 2.x validation
- Templating: Jinja2 for cloud-init
- Code Quality: Ruff (linter + formatter), ty (types)
- Derived from DigitalOcean account email, not configured
- Fetched via
/v2/account, sanitized for Linux compatibility john.doe@example.com→john_doe
- All SSH entries use
dropkit.<droplet-name>format - Centralized in
get_ssh_hostname()helper
- Default tags:
owner:<username>andfirewall - Additional tags extend defaults (never replace)
- Used for filtering, billing, and security
- Only public keys (*.pub) accepted
- Strict validation rejects private keys
- Auto-detects id_ed25519.pub, id_rsa.pub, id_ecdsa.pub
- Enabled by default for new droplets
- Locks down UFW to only allow tailscale0 interface
- Disable with
--no-tailscaleflag
-
Username from email — Not stored in config; fetched from DO API on demand. Ensures consistency across machines.
-
SSH key validation — Prevents accidental private key upload. Validates format (ssh-rsa, ssh-ed25519, ecdsa-sha2-*, etc.).
-
Tags extend defaults —
--tagsadds to defaults, never replaces. Ensures owner tag always present. -
Direct REST API — No python-digitalocean library. Simpler, fewer dependencies, full control.
-
Tailscale by default — Secure VPN access without public SSH. Local Tailscale required; keeps public IP if local Tailscale not running.
-
Pydantic validation — Runtime type safety for config files. Clear errors for invalid configurations.
-
SSH config backups — Created at
~/.ssh/config.bakbefore modifications. Each backup overwrites previous.
cloud-init status --format=json may return non-zero exit code but valid JSON.
Always parse JSON regardless of subprocess return code.
# CORRECT: Parse JSON even on error
result = subprocess.run([...], capture_output=True)
data = json.loads(result.stdout) # Don't check returncode first
# WRONG: Checking returncode before parsing
if result.returncode == 0: # May skip valid JSON with error status
data = json.loads(result.stdout)Status values: "done" (success), "error" (failed), "running" (in progress).
Cannot be undone. Use --no-disk to resize only CPU/memory.
Only keeps one backup at ~/.ssh/config.bak.
uv add <package> # Add dependency
uv add --dev <package> # Add dev dependency
uv sync # Install all
uv run <command> # Run in venvprek run # Run all checks (required before commit)
prek run --all-files # Check all files, not just staged
uv run ruff check --fix . # Lint + autofix only
uv run ty check dropkit/ # Type check onlyRuff config: Python 3.11+, 100-char lines, modern syntax (str | None, list[str]).
uv run pytest # All tests
uv run pytest tests/test_api.py # Specific file
uv run pytest -k "validate_ssh" # Pattern match
uv run pytest -v # VerboseCoverage: Minimum 29% enforced via --cov-fail-under=29 in pyproject.toml.
The E2E lifecycle test creates a real droplet, verifies SSH connectivity,
and destroys it. Registered as a prek manual stage hook — run before
pushing changes that affect core workflows. Uses hardcoded defaults
(nyc3, s-1vcpu-1gb, ubuntu-24-04-x64) to avoid dependence on user config.
./tests/e2e/test_lifecycle.sh # Run directly
prek run --stage manual # Run via prekRequires a valid dropkit config (~/.config/dropkit/config.yaml) with a
DigitalOcean API token. Optional overrides: DROPLET_NAME, DROPLET_REGION,
DROPLET_SIZE, DROPLET_IMAGE, E2E_SSH_TIMEOUT.
DropkitConfig— Root config withextra='forbid'DigitalOceanConfig— API token validationDefaultsConfig— Region, size, image slugsCloudInitConfig— Template path, SSH keys (min 1)SSHConfig— SSH config path, identity fileTailscaleConfig— VPN settings (enabled, lock_down_firewall, auth_timeout)
Config files: ~/.config/dropkit/config.yaml, ~/.config/dropkit/cloud-init.yaml
dropkit --install-completion zsh # Enable tab completionProvides dynamic completion for droplet names (filtered by owner tag).