Last active
March 17, 2026 10:13
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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