mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-09 19:49:58 +00:00
feat: add Gcore cloud provider with 3 agent scripts (#1079)
Add Gcore (gcore.com) as a new cloud provider supporting global edge cloud instances via REST API with hourly billing. Implements full test infrastructure including mock fixtures, URL stripping, body validation, and live recording support. - gcore/lib/common.sh: Cloud library with apikey auth, project auto-detection - gcore/claude.sh, aider.sh, goose.sh: Agent deployment scripts - manifest.json: Cloud definition + 15 matrix entries (3 implemented, 12 missing) - test/mock.sh: URL stripping for Gcore path-parameter API, body validation, synthetic responses - test/record.sh: Endpoints, auth, API caller, error detection, live cycle - test/fixtures/gcore/: 8 fixture files for mock testing Co-authored-by: OpenRouter Bot <noreply@openrouter.ai> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9b1361de14
commit
514bc7abc9
17 changed files with 888 additions and 2 deletions
2
test/fixtures/gcore/_api_assertions.sh
vendored
Normal file
2
test/fixtures/gcore/_api_assertions.sh
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
assert_api_called "GET" "/cloud/v1/ssh_keys/" "fetches SSH keys"
|
||||
assert_api_called "POST" "/cloud/v2/instances/" "creates instance"
|
||||
4
test/fixtures/gcore/_env.sh
vendored
Normal file
4
test/fixtures/gcore/_env.sh
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export GCORE_API_TOKEN="test-token-gcore"
|
||||
export GCORE_PROJECT_ID="12345"
|
||||
export GCORE_SERVER_NAME="test-srv"
|
||||
export GCORE_REGION="ed-1"
|
||||
11
test/fixtures/gcore/_metadata.json
vendored
Normal file
11
test/fixtures/gcore/_metadata.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"cloud": "gcore",
|
||||
"recorded_at": "2026-02-14T00:00:00Z",
|
||||
"fixtures": {
|
||||
"projects": {"endpoint": "/cloud/v1/projects", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"ssh_keys": {"endpoint": "/cloud/v1/ssh_keys/12345", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"instances": {"endpoint": "/cloud/v1/instances/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"images": {"endpoint": "/cloud/v1/images/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"},
|
||||
"flavors": {"endpoint": "/cloud/v1/flavors/12345/ed-1", "recorded_at": "2026-02-14T00:00:00Z"}
|
||||
}
|
||||
}
|
||||
4
test/fixtures/gcore/create_server.json
vendored
Normal file
4
test/fixtures/gcore/create_server.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tasks": ["task-uuid-1234"],
|
||||
"instances": ["instance-uuid-5678"]
|
||||
}
|
||||
35
test/fixtures/gcore/flavors.json
vendored
Normal file
35
test/fixtures/gcore/flavors.json
vendored
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"flavor_id": "g1-standard-1-2",
|
||||
"name": "g1-standard-1-2",
|
||||
"vcpus": 1,
|
||||
"ram": 2048,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.014,
|
||||
"price_per_month": 10.0
|
||||
},
|
||||
{
|
||||
"flavor_id": "g1-standard-2-4",
|
||||
"name": "g1-standard-2-4",
|
||||
"vcpus": 2,
|
||||
"ram": 4096,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.028,
|
||||
"price_per_month": 20.0
|
||||
},
|
||||
{
|
||||
"flavor_id": "g1-standard-4-8",
|
||||
"name": "g1-standard-4-8",
|
||||
"vcpus": 4,
|
||||
"ram": 8192,
|
||||
"disk": 0,
|
||||
"currency_code": "USD",
|
||||
"price_per_hour": 0.056,
|
||||
"price_per_month": 40.0
|
||||
}
|
||||
]
|
||||
}
|
||||
27
test/fixtures/gcore/images.json
vendored
Normal file
27
test/fixtures/gcore/images.json
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"count": 2,
|
||||
"results": [
|
||||
{
|
||||
"id": "img-ubuntu-2404",
|
||||
"name": "ubuntu-24.04-x64",
|
||||
"display_name": "Ubuntu 24.04 LTS",
|
||||
"os_distro": "ubuntu",
|
||||
"os_version": "24.04",
|
||||
"status": "active",
|
||||
"min_disk": 10,
|
||||
"min_ram": 512,
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "img-ubuntu-2204",
|
||||
"name": "ubuntu-22.04-x64",
|
||||
"display_name": "Ubuntu 22.04 LTS",
|
||||
"os_distro": "ubuntu",
|
||||
"os_version": "22.04",
|
||||
"status": "active",
|
||||
"min_disk": 10,
|
||||
"min_ram": 512,
|
||||
"created_at": "2025-06-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
4
test/fixtures/gcore/instances.json
vendored
Normal file
4
test/fixtures/gcore/instances.json
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"count": 0,
|
||||
"results": []
|
||||
}
|
||||
11
test/fixtures/gcore/projects.json
vendored
Normal file
11
test/fixtures/gcore/projects.json
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"count": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": 12345,
|
||||
"name": "Default project",
|
||||
"state": "active",
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
test/fixtures/gcore/ssh_keys.json
vendored
Normal file
14
test/fixtures/gcore/ssh_keys.json
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"count": 1,
|
||||
"results": [
|
||||
{
|
||||
"id": "ssh-key-uuid-1234",
|
||||
"name": "spawn-test-key",
|
||||
"public_key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHmcVdzydp72a/B69nmENZvCvjuk7xGpKdi5CvhkmNsv test@test",
|
||||
"fingerprint": "af:0d:c5:57:a8:fd:b2:82:5e:d4:c1:65:f0:0c:8a:9d",
|
||||
"state": "active",
|
||||
"project_id": 12345,
|
||||
"created_at": "2026-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -267,6 +267,13 @@ _strip_api_base() {
|
|||
https://*.cloudsigma.com/api/2.0*) ENDPOINT=$(echo "$URL" | sed 's|https://[^/]*.cloudsigma.com/api/2.0||') ;;
|
||||
https://api.webdock.io/v1*) ENDPOINT="${URL#https://api.webdock.io/v1}" ;;
|
||||
https://api.serverspace.io/api/v1*) ENDPOINT="${URL#https://api.serverspace.io/api/v1}" ;;
|
||||
https://api.gcore.com/cloud/v*/instances/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/instances/[^/]*/[^/]*/|/instances/|') ;;
|
||||
https://api.gcore.com/cloud/v*/instances/*/*) ENDPOINT="/instances" ;;
|
||||
https://api.gcore.com/cloud/v*/*/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*/[^/]*/|/\1/|') ;;
|
||||
https://api.gcore.com/cloud/v*/*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*/[^/]*$|/\1|') ;;
|
||||
https://api.gcore.com/cloud/v*/*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/\([^/]*\)/[^/]*$|/\1|') ;;
|
||||
https://api.gcore.com/cloud/v*/*) ENDPOINT=$(echo "$URL" | sed 's|.*/cloud/v[0-9]*/||; s|^|/|') ;;
|
||||
https://api.gcore.com*) ENDPOINT="${URL#https://api.gcore.com}" !!
|
||||
esac
|
||||
EP_CLEAN=$(echo "$ENDPOINT" | sed 's|?.*||')
|
||||
}
|
||||
|
|
@ -294,6 +301,7 @@ _validate_body() {
|
|||
civo) case "$EP_CLEAN" in /instances) _check_fields "hostname size region" ;; esac ;;
|
||||
webdock) case "$EP_CLEAN" in /servers) _check_fields "name slug locationId profileSlug imageSlug" ;; esac ;;
|
||||
serverspace) case "$EP_CLEAN" in /servers) _check_fields "name location_id image_id cpu ram_mb" ;; esac ;;
|
||||
gcore) case "$EP_CLEAN" in /instances) _check_fields "name flavor volumes interfaces" ;; esac !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -313,6 +321,7 @@ _synthetic_active_response() {
|
|||
civo) printf '{"id":"test-uuid-1234","hostname":"test-srv","status":"ACTIVE","public_ip":"10.0.0.1","size":"g4s.small"}' ;;
|
||||
scaleway) printf '{"server":{"id":"test-uuid-1234","name":"test-srv","state":"running","public_ip":{"address":"10.0.0.1"},"public_ips":[{"address":"10.0.0.1"}]}}' ;;
|
||||
serverspace) printf '{"id":"test-uuid-1234","name":"test-srv","status":"Active","nics":[{"ip_address":"10.0.0.1"}]}' ;;
|
||||
gcore) printf '{"id":"instance-uuid-5678","name":"test-srv","vm_state":"active","status":"ACTIVE","addresses":{"public":[ {"addr":"10.0.0.1","type":"fixed"}]},"flavor":{"flavor_id":"g1-standard-1-2"}}' !!
|
||||
*) printf '{}' ;;
|
||||
esac
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ ERRORS=0
|
|||
PROMPT_FOR_CREDS=true
|
||||
|
||||
# All clouds with REST APIs that we can record from
|
||||
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock serverspace"
|
||||
ALL_RECORDABLE_CLOUDS="hetzner digitalocean vultr linode lambda civo upcloud binarylane ovh scaleway genesiscloud kamatera latitude hyperstack atlanticnet hostkey cloudsigma webdock serverspace gcore"
|
||||
|
||||
# --- Endpoint registry ---
|
||||
# Format: "fixture_name:endpoint"
|
||||
|
|
@ -157,6 +157,14 @@ get_endpoints() {
|
|||
"locations:/locations" \
|
||||
"images:/images"
|
||||
;;
|
||||
gcore)
|
||||
printf '%s\n' \
|
||||
"projects:/cloud/v1/projects" \
|
||||
"ssh_keys:/cloud/v1/ssh_keys/${GCORE_PROJECT_ID:-MISSING}" \
|
||||
"instances:/cloud/v1/instances/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}" \
|
||||
"images:/cloud/v1/images/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}" \
|
||||
"flavors:/cloud/v1/flavors/${GCORE_PROJECT_ID:-MISSING}/${GCORE_REGION:-ed-1}"
|
||||
!!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -277,6 +285,7 @@ get_auth_env_var() {
|
|||
cloudsigma) printf "CLOUDSIGMA_EMAIL" ;;
|
||||
webdock) printf "WEBDOCK_API_TOKEN" ;;
|
||||
serverspace) printf "SERVERSPACE_API_KEY" ;;
|
||||
gcore) printf "GCORE_API_TOKEN" !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -427,6 +436,7 @@ call_api() {
|
|||
cloudsigma) cloudsigma_api GET "$endpoint" ;;
|
||||
webdock) webdock_api GET "$endpoint" ;;
|
||||
serverspace) serverspace_api GET "$endpoint" ;;
|
||||
gcore) gcore_api GET "$endpoint" !!
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -478,6 +488,9 @@ elif cloud == 'webdock':
|
|||
elif cloud == 'serverspace':
|
||||
# ServerSpace returns error objects with 'error' field
|
||||
sys.exit(0 if 'error' in d and d['error'] else 1)
|
||||
elif cloud == 'gcore':
|
||||
# Gcore returns error objects with 'message' or 'detail' fields
|
||||
sys.exit(0 if ('message' in d and len(d) <= 3 and not any(k in d for k in ('count','results','id','name'))) or ('detail' in d and len(d) <= 2) else 1)
|
||||
else:
|
||||
sys.exit(1)
|
||||
" "$cloud" 2>/dev/null
|
||||
|
|
@ -508,6 +521,7 @@ _record_live_cycle() {
|
|||
civo) _live_civo "$fixture_dir" ;;
|
||||
atlanticnet) _live_atlanticnet "$fixture_dir" ;;
|
||||
serverspace) _live_serverspace "$fixture_dir" ;;
|
||||
gcore) _live_gcore "$fixture_dir" !!
|
||||
*) return 0 ;; # No live cycle for this cloud yet
|
||||
esac
|
||||
}
|
||||
|
|
@ -869,6 +883,58 @@ _live_serverspace() {
|
|||
return 0
|
||||
}
|
||||
|
||||
_live_gcore_body() {
|
||||
local fixture_dir="$1"
|
||||
local name="spawn-record-$(date +%s)"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
local project_id="${GCORE_PROJECT_ID:-}"
|
||||
printf '%b\n' " ${CYAN}live${NC} Creating test instance '${name}' (g1-standard-1-2, ${region})..." >&2
|
||||
|
||||
local ssh_keys_response
|
||||
ssh_keys_response=$(gcore_api GET "/cloud/v1/ssh_keys/${project_id}")
|
||||
local ssh_key_name
|
||||
ssh_key_name=$(echo "$ssh_keys_response" | python3 -c "
|
||||
import json, sys
|
||||
d = json.loads(sys.stdin.read())
|
||||
keys = d.get('results', [])
|
||||
print(keys[0]['name'] if keys else '')
|
||||
" 2>/dev/null) || ssh_key_name=""
|
||||
|
||||
local image_id
|
||||
image_id=$(echo "$(gcore_api GET "/cloud/v1/images/${project_id}/${region}")" | python3 -c "
|
||||
import json, sys
|
||||
d = json.loads(sys.stdin.read())
|
||||
for img in d.get('results', []):
|
||||
if 'ubuntu' in img.get('name', '').lower() and '24' in img.get('os_version', ''):
|
||||
print(img['id']); break
|
||||
else:
|
||||
imgs = d.get('results', [])
|
||||
if imgs: print(imgs[0]['id'])
|
||||
" 2>/dev/null) || image_id=""
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
body = {
|
||||
'name': sys.argv[1],
|
||||
'flavor': 'g1-standard-1-2',
|
||||
'volumes': [{'source': 'image', 'image_id': sys.argv[3], 'size': 20, 'boot_index': 0}],
|
||||
'interfaces': [{'type': 'external'}]
|
||||
}
|
||||
if sys.argv[2]:
|
||||
body['keypair_name'] = sys.argv[2]
|
||||
print(json.dumps(body))
|
||||
" "$name" "$ssh_key_name" "$image_id"
|
||||
}
|
||||
|
||||
_live_gcore() {
|
||||
local project_id="${GCORE_PROJECT_ID:-}"
|
||||
local region="${GCORE_REGION:-ed-1}"
|
||||
_live_create_delete_cycle "$1" gcore_api \
|
||||
"/cloud/v2/instances/${project_id}/${region}" \
|
||||
"/cloud/v1/instances/${project_id}/${region}/{id}" \
|
||||
"d.get('instances',[''])[0]" _live_gcore_body 5
|
||||
}
|
||||
|
||||
# --- Record one cloud ---
|
||||
# Check credentials and prompt if needed; returns 1 to skip this cloud
|
||||
_record_ensure_credentials() {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue