mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-09 19:49:58 +00:00
After an interactive SSH session ends, users are now shown: - A warning that their server is still running (and may incur charges) - A link to the cloud provider's dashboard to manage/delete it - The SSH command to reconnect This prevents users from unknowingly leaving servers running after exiting their agent session. Covers all 25 SSH-based cloud providers. Agent: ux-engineer Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
283 lines
9.2 KiB
Bash
283 lines
9.2 KiB
Bash
#!/bin/bash
|
|
set -eo pipefail
|
|
# Common bash functions for HOSTKEY spawn scripts
|
|
|
|
# ============================================================
|
|
# Provider-agnostic functions
|
|
# ============================================================
|
|
|
|
# Source shared provider-agnostic functions (local or remote fallback)
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" 2>/dev/null && pwd)"
|
|
if [[ -n "$SCRIPT_DIR" && -f "$SCRIPT_DIR/../../shared/common.sh" ]]; then
|
|
source "$SCRIPT_DIR/../../shared/common.sh"
|
|
else
|
|
eval "$(curl -fsSL https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/shared/common.sh)"
|
|
fi
|
|
|
|
# Note: Provider-agnostic functions (logging, OAuth, browser, nc_listen) are now in shared/common.sh
|
|
|
|
# ============================================================
|
|
# HOSTKEY specific functions
|
|
# ============================================================
|
|
|
|
readonly HOSTKEY_API_BASE="https://invapi.hostkey.com"
|
|
SPAWN_DASHBOARD_URL="https://manage.hostkey.com/"
|
|
|
|
# Centralized curl wrapper for HOSTKEY API
|
|
# Delegates to generic_cloud_api for retry logic and error handling
|
|
# Usage: hostkey_api METHOD ENDPOINT [BODY]
|
|
hostkey_api() {
|
|
local method="$1"
|
|
local endpoint="$2"
|
|
local body="${3:-}"
|
|
|
|
generic_cloud_api "$HOSTKEY_API_BASE" "$HOSTKEY_API_KEY" "$method" "$endpoint" "$body"
|
|
}
|
|
|
|
# Test HOSTKEY API key validity
|
|
test_hostkey_token() {
|
|
local response
|
|
response=$(hostkey_api GET "/v1/services" 2>&1) || true
|
|
|
|
if echo "$response" | grep -qi "unauthorized\|invalid\|error"; then
|
|
log_error "API Error: Invalid or expired HOSTKEY API key"
|
|
log_error ""
|
|
log_error "How to fix:"
|
|
log_error " 1. Log in to HOSTKEY at: https://hostkey.com/"
|
|
log_error " 2. Navigate to API settings in your account"
|
|
log_error " 3. Generate a new API key if needed"
|
|
log_error " 4. Set HOSTKEY_API_KEY environment variable"
|
|
return 1
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
# Ensure HOSTKEY_API_KEY is available (env var → config file → prompt+save)
|
|
ensure_hostkey_token() {
|
|
ensure_api_token_with_provider \
|
|
"HOSTKEY" \
|
|
"HOSTKEY_API_KEY" \
|
|
"$HOME/.config/spawn/hostkey.json" \
|
|
"https://hostkey.com/documentation/apidocs/api/" \
|
|
"test_hostkey_token"
|
|
}
|
|
|
|
# Check if SSH key is registered with HOSTKEY
|
|
hostkey_check_ssh_key() {
|
|
local fingerprint="$1"
|
|
local response
|
|
response=$(hostkey_api GET "/ssh_keys")
|
|
|
|
if echo "$response" | grep -q "$fingerprint"; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
}
|
|
|
|
# Register SSH key with HOSTKEY
|
|
hostkey_register_ssh_key() {
|
|
local key_name="$1"
|
|
local pub_path="$2"
|
|
local pub_key
|
|
pub_key=$(cat "$pub_path")
|
|
local json_pub_key json_name
|
|
json_pub_key=$(json_escape "$pub_key")
|
|
json_name=$(json_escape "$key_name")
|
|
|
|
local register_body="{\"name\":$json_name,\"public_key\":$json_pub_key}"
|
|
local register_response
|
|
register_response=$(hostkey_api POST "/ssh_keys" "$register_body")
|
|
|
|
if echo "$register_response" | grep -qi "error"; then
|
|
log_error "API Error: $(echo "$register_response" | grep -o '"message":"[^"]*"' || echo "$register_response")"
|
|
log_error ""
|
|
log_error "Common causes:"
|
|
log_error " - SSH key already registered with this name"
|
|
log_error " - Invalid SSH key format"
|
|
log_error " - API key lacks write permissions"
|
|
return 1
|
|
fi
|
|
|
|
return 0
|
|
}
|
|
|
|
# Ensure SSH key exists locally and is registered with HOSTKEY
|
|
ensure_ssh_key() {
|
|
ensure_ssh_key_with_provider hostkey_check_ssh_key hostkey_register_ssh_key "HOSTKEY"
|
|
}
|
|
|
|
# Get server name from env var or prompt
|
|
get_server_name() {
|
|
get_validated_server_name "HOSTKEY_SERVER_NAME" "Enter server name: "
|
|
}
|
|
|
|
# List available HOSTKEY locations
|
|
_list_locations() {
|
|
printf '%s\n' "nl|Amsterdam|Netherlands"
|
|
printf '%s\n' "de|Frankfurt|Germany"
|
|
printf '%s\n' "fi|Helsinki|Finland"
|
|
printf '%s\n' "is|Reykjavik|Iceland"
|
|
printf '%s\n' "tr|Istanbul|Turkey"
|
|
printf '%s\n' "us|New York|United States"
|
|
}
|
|
|
|
# Interactive location picker (skipped if HOSTKEY_LOCATION is set)
|
|
_pick_location() {
|
|
interactive_pick "HOSTKEY_LOCATION" "nl" "locations" _list_locations
|
|
}
|
|
|
|
# Get available instance presets for a location
|
|
_list_instance_presets() {
|
|
local location="$1"
|
|
|
|
ensure_jq || return 1
|
|
|
|
# Call HOSTKEY presets API
|
|
local response
|
|
response=$(curl -s "${HOSTKEY_API_BASE}/presets.php" -X POST \
|
|
--data "action=list" \
|
|
--data "location=${location}")
|
|
|
|
# Parse and format response
|
|
printf '%s' "$response" | jq -r \
|
|
'.[] | "\(.id)|\(.cores) vCPU|\(.ram) GB RAM|\(.storage) GB disk|€\(.price)/mo"' 2>/dev/null || {
|
|
log_error "Failed to fetch instance presets"
|
|
return 1
|
|
}
|
|
}
|
|
|
|
# Interactive instance preset picker
|
|
_pick_instance_preset() {
|
|
local location="$1"
|
|
_list_presets_for_location() { _list_instance_presets "$location"; }
|
|
interactive_pick "HOSTKEY_INSTANCE_PRESET" "1" "instance presets" _list_presets_for_location "1"
|
|
unset -f _list_presets_for_location
|
|
}
|
|
|
|
# Build JSON order body for HOSTKEY instance creation
|
|
# Usage: _hostkey_build_order_body NAME LOCATION PRESET
|
|
_hostkey_build_order_body() {
|
|
local name="$1" location="$2" preset="$3"
|
|
jq -n \
|
|
--arg name "$name" \
|
|
--arg location "$location" \
|
|
--arg preset "$preset" \
|
|
'{name: $name, location: $location, preset: $preset, os: "ubuntu-24.04"}'
|
|
}
|
|
|
|
# Check HOSTKEY API response for errors and log diagnostics
|
|
# Returns 0 if error detected, 1 if no error
|
|
_hostkey_check_create_error() {
|
|
local response="$1"
|
|
|
|
if ! echo "$response" | grep -qi "error"; then
|
|
return 1
|
|
fi
|
|
|
|
log_error "Failed to create HOSTKEY instance"
|
|
local error_msg
|
|
error_msg=$(printf '%s' "$response" | jq -r '.error // .message // "Unknown error"' 2>/dev/null || echo "$response")
|
|
log_error "API Error: $error_msg"
|
|
log_error ""
|
|
log_error "Common issues:"
|
|
log_error " - Insufficient account balance"
|
|
log_error " - Instance limit reached"
|
|
log_error " - Invalid location or preset"
|
|
log_error ""
|
|
log_error "Check your account status: https://hostkey.com/"
|
|
return 0
|
|
}
|
|
|
|
# Parse instance ID and IP from HOSTKEY order response
|
|
# Sets HOSTKEY_INSTANCE_ID and HOSTKEY_INSTANCE_IP on success
|
|
_hostkey_parse_instance_response() {
|
|
local response="$1"
|
|
HOSTKEY_INSTANCE_ID=$(printf '%s' "$response" | jq -r '.id // .instance_id')
|
|
HOSTKEY_INSTANCE_IP=$(printf '%s' "$response" | jq -r '.ip // .ipv4')
|
|
export HOSTKEY_INSTANCE_ID HOSTKEY_INSTANCE_IP
|
|
}
|
|
|
|
# Create a HOSTKEY compute instance
|
|
create_server() {
|
|
local name="$1"
|
|
|
|
# Interactive location + instance preset selection (skipped if env vars are set)
|
|
local location
|
|
location=$(_pick_location)
|
|
# Validate location before using it in API calls (preset listing)
|
|
validate_region_name "$location" || { log_error "Invalid HOSTKEY_LOCATION"; return 1; }
|
|
|
|
local preset
|
|
preset=$(_pick_instance_preset "$location")
|
|
validate_resource_name "$preset" || { log_error "Invalid HOSTKEY_INSTANCE_PRESET"; return 1; }
|
|
|
|
log_step "Creating HOSTKEY instance '$name' (preset: $preset, location: $location)..."
|
|
|
|
local order_body
|
|
order_body=$(_hostkey_build_order_body "$name" "$location" "$preset")
|
|
|
|
local response
|
|
response=$(hostkey_api POST "/eq/order_instance" "$order_body")
|
|
|
|
if _hostkey_check_create_error "$response"; then
|
|
return 1
|
|
fi
|
|
|
|
_hostkey_parse_instance_response "$response"
|
|
|
|
log_info "Instance created: ID=$HOSTKEY_INSTANCE_ID, IP=$HOSTKEY_INSTANCE_IP"
|
|
|
|
# Wait for instance to be ready
|
|
log_step "Waiting for instance to be ready..."
|
|
sleep 10
|
|
}
|
|
|
|
# SSH operations — delegates to shared helpers (SSH_USER defaults to root)
|
|
verify_server_connectivity() { ssh_verify_connectivity "$@"; }
|
|
run_server() { ssh_run_server "$@"; }
|
|
upload_file() { ssh_upload_file "$@"; }
|
|
interactive_session() { ssh_interactive_session "$@"; }
|
|
|
|
# Destroy a HOSTKEY instance
|
|
destroy_server() {
|
|
local instance_id="$1"
|
|
|
|
log_step "Destroying instance $instance_id..."
|
|
local response
|
|
local json_id
|
|
json_id=$(json_escape "$instance_id")
|
|
response=$(hostkey_api POST "/eq/terminate" "{\"id\":$json_id}")
|
|
|
|
if echo "$response" | grep -qi "error"; then
|
|
log_error "Failed to destroy instance $instance_id"
|
|
log_error "API Error: $(extract_api_error_message "$response" "$response")"
|
|
log_error ""
|
|
log_error "The instance may still be running and incurring charges."
|
|
log_error "Delete it manually at: https://manage.hostkey.com/"
|
|
return 1
|
|
fi
|
|
|
|
log_info "Instance $instance_id destroyed"
|
|
}
|
|
|
|
# List all HOSTKEY instances
|
|
list_servers() {
|
|
local response
|
|
response=$(hostkey_api GET "/v1/services")
|
|
|
|
local count
|
|
count=$(printf '%s' "$response" | jq 'length' 2>/dev/null || echo "0")
|
|
|
|
if [[ "$count" -eq 0 ]]; then
|
|
printf 'No instances found\n'
|
|
return 0
|
|
fi
|
|
|
|
printf '%-25s %-12s %-12s %-16s\n' "NAME" "ID" "STATUS" "IP"
|
|
printf '%s\n' "-----------------------------------------------------------------"
|
|
printf '%s' "$response" | jq -r \
|
|
'.[] | "\(.name // "N/A")|\(.id // "N/A")|\(.status // "N/A")|\(.ip // "N/A")"' \
|
|
| while IFS='|' read -r name sid status ip; do
|
|
printf '%-25s %-12s %-12s %-16s\n' "$name" "$sid" "$status" "$ip"
|
|
done
|
|
}
|