Skip to content

Instantly share code, notes, and snippets.

@birme
Last active March 17, 2026 10:13
Show Gist options
  • Select an option

  • Save birme/00bbb4260567552687e73f11f8cebc1a to your computer and use it in GitHub Desktop.

Select an option

Save birme/00bbb4260567552687e73f11f8cebc1a to your computer and use it in GitHub Desktop.
The OSC Exit Test — Prove you can leave a cloud platform. Creates a CouchDB on Eyevinn Open Source Cloud, exports with curl, runs locally with Docker, verifies data is identical.
#!/usr/bin/env bash
#
# The OSC Exit Test
# =================
# Can you actually leave? This script proves it.
#
# We create a CouchDB database on Eyevinn Open Source Cloud,
# fill it with data using the standard CouchDB HTTP API,
# export everything with curl, spin up the exact same database
# locally with Docker, import the data, and verify it's identical.
#
# No vendor SDK. No proprietary export format. No lock-in.
# Just open source software and standard protocols.
#
# Prerequisites:
# - An OSC account (https://app.osaas.io) with a Personal Access Token
# - curl, jq, docker
#
# Usage:
# export OSC_ACCESS_TOKEN="your-token-here"
# bash osc-exit-test.sh
#
# Get your token: OSC Dashboard → Settings → Personal Access Tokens
#
# Learn more: https://www.osaas.io
set -euo pipefail
# --- Configuration -----------------------------------------------------------
OSC_ENVIRONMENT="${OSC_ENVIRONMENT:-prod}"
INSTANCE_NAME="exittest"
ADMIN_PASSWORD="exit-test-$(date +%s)"
LOCAL_COUCHDB_PORT=5984
EXPORT_FILE="/tmp/osc-exit-test-export.json"
SERVICE_ID="apache-couchdb"
# --- Helpers -----------------------------------------------------------------
info() { printf "\033[1;34m→\033[0m %s\n" "$1"; }
ok() { printf "\033[1;32m✓\033[0m %s\n" "$1"; }
fail() { printf "\033[1;31m✗\033[0m %s\n" "$1"; exit 1; }
step() { printf "\n\033[1;37m── Step %s ──\033[0m\n\n" "$1"; }
check_deps() {
for cmd in curl jq docker; do
command -v "$cmd" >/dev/null 2>&1 || fail "Missing dependency: $cmd"
done
}
wait_for_url() {
local url="$1" max_attempts="${2:-30}" attempt=0
while [ $attempt -lt $max_attempts ]; do
if curl -skf -o /dev/null "$url" 2>/dev/null; then
return 0
fi
attempt=$((attempt + 1))
sleep 2
done
return 1
}
# curl wrapper for OSC instance requests (TLS cert may still be provisioning)
couch() { curl -sk "$@"; }
# --- OSC API helpers ---------------------------------------------------------
# These use the same HTTP calls as the @osaas/client-core SDK.
# We use curl directly so you can see exactly what's happening.
CATALOG_URL="https://catalog.svc.${OSC_ENVIRONMENT}.osaas.io"
TOKEN_URL="https://token.svc.${OSC_ENVIRONMENT}.osaas.io"
PAT_HEADER="x-pat-jwt: Bearer ${OSC_ACCESS_TOKEN:-}"
osc_subscribe() {
curl -sf -X POST "$CATALOG_URL/mysubscriptions" \
-H "$PAT_HEADER" \
-H "Content-Type: application/json" \
-d "{\"services\": [\"$SERVICE_ID\"]}" -o /dev/null 2>/dev/null || true
}
osc_get_service_token() {
curl -sf -X POST "$TOKEN_URL/servicetoken" \
-H "$PAT_HEADER" \
-H "Content-Type: application/json" \
-d "{\"serviceId\": \"$SERVICE_ID\"}" | jq -r '.token'
}
osc_get_api_url() {
curl -sf "$CATALOG_URL/mysubscriptions" \
-H "$PAT_HEADER" \
| jq -r ".[] | select(.serviceId == \"$SERVICE_ID\") | .apiUrl"
}
# --- Preflight ---------------------------------------------------------------
check_deps
if [ -z "${OSC_ACCESS_TOKEN:-}" ]; then
fail "Set OSC_ACCESS_TOKEN first. Get one at https://app.osaas.io → Settings → Personal Access Tokens"
fi
cat <<'BANNER'
╔══════════════════════════════════════════════════╗
║ The OSC Exit Test ║
║ ║
║ Can you actually leave a cloud platform? ║
║ Let's find out. ║
╚══════════════════════════════════════════════════╝
BANNER
# --- Step 1: Create a CouchDB instance on OSC --------------------------------
step "1/7: Create a CouchDB database on Eyevinn Open Source Cloud"
info "Subscribing to CouchDB service..."
osc_subscribe
info "Getting service access token..."
SAT=$(osc_get_service_token)
[ -n "$SAT" ] && [ "$SAT" != "null" ] || fail "Could not get service access token"
info "Looking up service orchestrator..."
API_URL=$(osc_get_api_url)
[ -n "$API_URL" ] && [ "$API_URL" != "null" ] || fail "Could not find service API URL"
info "Creating instance '$INSTANCE_NAME' via $API_URL ..."
CREATE_RESPONSE=$(curl -sf -X POST "$API_URL" \
-H "x-jwt: Bearer $SAT" \
-H "Content-Type: application/json" \
-d "{\"name\": \"$INSTANCE_NAME\", \"AdminPassword\": \"$ADMIN_PASSWORD\"}" 2>/dev/null || echo "{}")
INSTANCE_URL=$(echo "$CREATE_RESPONSE" | jq -r '.url // empty')
if [ -z "$INSTANCE_URL" ]; then
# Instance might already exist — fetch it
info "Instance may already exist, fetching..."
INSTANCE_URL=$(curl -sf "$API_URL/$INSTANCE_NAME" \
-H "x-jwt: Bearer $SAT" | jq -r '.url // empty')
fi
[ -n "$INSTANCE_URL" ] || fail "Could not create or find instance"
COUCHDB_URL="$INSTANCE_URL"
COUCHDB_AUTH_URL=$(echo "$COUCHDB_URL" | sed "s|://|://admin:${ADMIN_PASSWORD}@|")
info "Waiting for CouchDB to be ready at $COUCHDB_URL ..."
# Check _all_dbs (not root) — root returns "Hello, world!" before the API is ready
wait_for_url "$COUCHDB_AUTH_URL/_all_dbs" 90 || fail "CouchDB did not become ready in time"
ok "CouchDB is running on OSC"
# --- Step 2: Create a database and add documents -----------------------------
step "2/7: Add data using the standard CouchDB HTTP API"
info "Creating database 'demo'..."
couch -f -X PUT "$COUCHDB_AUTH_URL/demo" -o /dev/null 2>/dev/null || true
info "Inserting documents..."
for i in 1 2 3 4 5; do
couch -f -X POST "$COUCHDB_AUTH_URL/demo" \
-H "Content-Type: application/json" \
-d "{
\"type\": \"article\",
\"title\": \"Article $i\",
\"content\": \"This is test content for article number $i.\",
\"author\": \"exit-test\",
\"tags\": [\"demo\", \"portability\"],
\"created_at\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
}" -o /dev/null
done
DOC_COUNT=$(couch -f "$COUCHDB_AUTH_URL/demo" | jq '.doc_count')
ok "Inserted $DOC_COUNT documents into CouchDB on OSC"
# --- Step 3: Export everything with curl --------------------------------------
step "3/7: Export all data (just curl — no vendor tools)"
info "Fetching all documents from $COUCHDB_URL/demo/_all_docs ..."
couch -f "$COUCHDB_AUTH_URL/demo/_all_docs?include_docs=true" \
| jq '{docs: [.rows[].doc | del(._rev)]}' \
> "$EXPORT_FILE"
EXPORTED_COUNT=$(jq '.docs | length' "$EXPORT_FILE")
ok "Exported $EXPORTED_COUNT documents to $EXPORT_FILE"
info "Export format: plain JSON. Open it, read it, use it anywhere."
# --- Step 4: Delete the OSC instance ------------------------------------------
step "4/7: Delete the OSC instance (we're leaving)"
info "Removing instance from OSC..."
curl -sf -X DELETE "$API_URL/$INSTANCE_NAME" \
-H "x-jwt: Bearer $SAT" -o /dev/null 2>/dev/null || true
ok "Instance deleted from OSC. We have officially left."
# --- Step 5: Run CouchDB locally with Docker ----------------------------------
step "5/7: Run the exact same database locally with Docker"
# Clean up any previous container
docker rm -f osc-exit-test-couchdb 2>/dev/null || true
info "Starting CouchDB with: docker run couchdb:3"
docker run -d --name osc-exit-test-couchdb \
-e COUCHDB_USER=admin \
-e COUCHDB_PASSWORD="$ADMIN_PASSWORD" \
-p "$LOCAL_COUCHDB_PORT:5984" \
couchdb:3 > /dev/null
LOCAL_URL="http://admin:${ADMIN_PASSWORD}@localhost:${LOCAL_COUCHDB_PORT}"
info "Waiting for local CouchDB to start..."
wait_for_url "$LOCAL_URL" 30 || fail "Local CouchDB did not start"
ok "CouchDB is running locally on port $LOCAL_COUCHDB_PORT"
# --- Step 6: Import the exported data -----------------------------------------
step "6/7: Import the exported data into local CouchDB"
info "Creating database 'demo'..."
curl -sf -X PUT "$LOCAL_URL/demo" -o /dev/null
info "Bulk-importing documents..."
curl -sf -X POST "$LOCAL_URL/demo/_bulk_docs" \
-H "Content-Type: application/json" \
-d @"$EXPORT_FILE" -o /dev/null
LOCAL_COUNT=$(curl -sf "$LOCAL_URL/demo" | jq '.doc_count')
ok "Imported $LOCAL_COUNT documents into local CouchDB"
# --- Step 7: Verify the data is identical -------------------------------------
step "7/7: Verify the data is identical"
info "Comparing documents..."
# Export local data in the same format
LOCAL_EXPORT=$(curl -sf "$LOCAL_URL/demo/_all_docs?include_docs=true" \
| jq '{docs: [.rows[].doc | del(._rev)]}')
# Compare document contents (ignoring CouchDB-internal fields)
OSC_CONTENT=$(jq -c '[.docs[] | {title, content, author, tags, type}] | sort_by(.title)' "$EXPORT_FILE")
LOCAL_CONTENT=$(echo "$LOCAL_EXPORT" | jq -c '[.docs[] | {title, content, author, tags, type}] | sort_by(.title)')
if [ "$OSC_CONTENT" = "$LOCAL_CONTENT" ]; then
ok "All $LOCAL_COUNT documents match perfectly"
else
fail "Data mismatch detected"
fi
# --- Summary ------------------------------------------------------------------
cat <<SUMMARY
╔══════════════════════════════════════════════════╗
║ Exit test: PASSED ║
╠══════════════════════════════════════════════════╣
║ ║
║ Created a database on OSC ✓ ║
║ Added $DOC_COUNT documents via HTTP API ✓ ║
║ Exported with curl (plain JSON) ✓ ║
║ Deleted the OSC instance ✓ ║
║ Ran CouchDB locally (Docker) ✓ ║
║ Imported all data ✓ ║
║ Verified: identical ✓ ║
║ ║
║ Tools used: curl, jq, docker ║
║ Vendor SDK required: none ║
║ Proprietary formats: zero ║
║ ║
╚══════════════════════════════════════════════════╝
Your data is now running on localhost:$LOCAL_COUCHDB_PORT
Open http://localhost:$LOCAL_COUCHDB_PORT/_utils to see it.
Cleanup: docker rm -f osc-exit-test-couchdb
SUMMARY
# Clean up export file
rm -f "$EXPORT_FILE"
ok "The best infrastructure is infrastructure you can leave."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment