From 84681fe092fb5657d6bb68bdb524862b2654a7da Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Fri, 13 Feb 2026 05:48:07 -0800 Subject: [PATCH] refactor: replace hand-rolled API wrapper and polling loop with shared helpers (#927) - hostkey: replace 22-line raw curl wrapper with generic_cloud_api delegation (adds retry logic, standardizes METHOD ENDPOINT [BODY] signature) - exoscale: replace 30-line hand-rolled polling loop with generic_wait_for_instance via thin CLI wrapper Agent: complexity-hunter Co-authored-by: A <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) --- exoscale/lib/common.sh | 41 ++++++++++++++--------------------------- hostkey/lib/common.sh | 39 ++++++++++++--------------------------- 2 files changed, 26 insertions(+), 54 deletions(-) diff --git a/exoscale/lib/common.sh b/exoscale/lib/common.sh index 7558728e..281fbf5c 100644 --- a/exoscale/lib/common.sh +++ b/exoscale/lib/common.sh @@ -165,38 +165,25 @@ get_server_name() { get_validated_server_name "EXOSCALE_SERVER_NAME" "Enter server name: " } +# Thin wrapper around exo CLI that matches the generic_wait_for_instance API +# signature (func METHOD ENDPOINT) so the shared polling loop can be reused. +# The endpoint is the instance ID. +_exoscale_instance_api() { + local _method="$1" # unused; exo CLI doesn't distinguish methods + local instance_id="$2" + exo compute instance show "$instance_id" -O json 2>/dev/null || echo '{}' +} + # Wait for Exoscale instance to become running and get its IP # Sets: EXOSCALE_SERVER_IP # Usage: _wait_for_exoscale_instance INSTANCE_ID [MAX_ATTEMPTS] _wait_for_exoscale_instance() { local instance_id="$1" - local max_attempts=${2:-60} - local attempt=1 - - log_step "Waiting for instance to become running..." - while [[ "$attempt" -le "$max_attempts" ]]; do - local status_json - status_json=$(exo compute instance show "$instance_id" -O json 2>/dev/null || echo '{}') - - local status - status=$(echo "$status_json" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('state','unknown'))" 2>/dev/null || echo "unknown") - - if [[ "$status" == "running" ]]; then - EXOSCALE_SERVER_IP=$(echo "$status_json" | python3 -c "import json,sys; d=json.loads(sys.stdin.read()); print(d.get('public-ip',''))" 2>/dev/null) - export EXOSCALE_SERVER_IP - if [[ -n "$EXOSCALE_SERVER_IP" ]]; then - log_info "Instance running: IP=$EXOSCALE_SERVER_IP" - return 0 - fi - fi - - log_step "Instance status: $status ($attempt/$max_attempts)" - sleep "${INSTANCE_STATUS_POLL_DELAY}" - attempt=$((attempt + 1)) - done - - log_error "Instance did not become running in time" - return 1 + local max_attempts="${2:-60}" + generic_wait_for_instance _exoscale_instance_api "$instance_id" \ + "running" "d.get('state','unknown')" \ + "d.get('public-ip','')" \ + EXOSCALE_SERVER_IP "Instance" "$max_attempts" } # Write cloud-init userdata to a temporary file for exo CLI diff --git a/hostkey/lib/common.sh b/hostkey/lib/common.sh index 3e38d41f..ddb03d78 100644 --- a/hostkey/lib/common.sh +++ b/hostkey/lib/common.sh @@ -23,35 +23,20 @@ fi readonly HOSTKEY_API_BASE="https://invapi.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 endpoint="$1" - local body="${2:-}" + local method="$1" + local endpoint="$2" + local body="${3:-}" - if [[ -z "${HOSTKEY_API_KEY:-}" ]]; then - log_error "HOSTKEY_API_KEY is not set" - return 1 - fi - - local response - if [[ -n "$body" ]]; then - response=$(curl -s "${HOSTKEY_API_BASE}${endpoint}" \ - -H "Authorization: Bearer ${HOSTKEY_API_KEY}" \ - -H "Content-Type: application/json" \ - -d "$body") - else - response=$(curl -s "${HOSTKEY_API_BASE}${endpoint}" \ - -H "Authorization: Bearer ${HOSTKEY_API_KEY}") - fi - - printf '%s' "$response" + generic_cloud_api "$HOSTKEY_API_BASE" "$HOSTKEY_API_KEY" "$method" "$endpoint" "$body" } # Test HOSTKEY API key validity test_hostkey_token() { local response - # Try to get server list as a simple auth test - response=$(curl -s "${HOSTKEY_API_BASE}/v1/services" \ - -H "Authorization: Bearer ${HOSTKEY_API_KEY:-}" 2>&1) + 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" @@ -80,7 +65,7 @@ ensure_hostkey_token() { hostkey_check_ssh_key() { local fingerprint="$1" local response - response=$(hostkey_api "/ssh_keys") + response=$(hostkey_api GET "/ssh_keys") if echo "$response" | grep -q "$fingerprint"; then return 0 @@ -99,7 +84,7 @@ hostkey_register_ssh_key() { local register_body="{\"name\":\"$key_name\",\"public_key\":$json_pub_key}" local register_response - register_response=$(hostkey_api "/ssh_keys" "$register_body") + 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")" @@ -227,7 +212,7 @@ create_server() { '{name: $name, location: $location, preset: $preset, os: "ubuntu-24.04"}') local response - response=$(hostkey_api "/eq/order_instance" "$order_body") + response=$(hostkey_api POST "/eq/order_instance" "$order_body") if echo "$response" | grep -qi "error"; then log_error "Failed to create HOSTKEY instance" @@ -268,7 +253,7 @@ destroy_server() { log_step "Destroying instance $instance_id..." local response - response=$(hostkey_api "/eq/terminate" "{\"id\":\"$instance_id\"}") + response=$(hostkey_api POST "/eq/terminate" "{\"id\":\"$instance_id\"}") if echo "$response" | grep -qi "error"; then log_error "Failed to destroy instance: $response" @@ -281,7 +266,7 @@ destroy_server() { # List all HOSTKEY instances list_servers() { local response - response=$(hostkey_api "/v1/services") + response=$(hostkey_api GET "/v1/services") local count count=$(printf '%s' "$response" | jq 'length' 2>/dev/null || echo "0")