Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 680ec3b897 |
@@ -1,6 +1,6 @@
|
|||||||
# .gitea/workflows/ci.yaml
|
# .gitea/workflows/ci.yaml
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - CI/CD Pipeline (v0.17.3)
|
# Armature - CI/CD Pipeline (v0.17.3)
|
||||||
# ============================================
|
# ============================================
|
||||||
# Single unified image (Go backend + nginx frontend).
|
# Single unified image (Go backend + nginx frontend).
|
||||||
# v0.1.0: Dropped FE/BE image split per ROADMAP design decision.
|
# v0.1.0: Dropped FE/BE image split per ROADMAP design decision.
|
||||||
@@ -29,22 +29,22 @@
|
|||||||
# Tags (v*) → always full pipeline
|
# Tags (v*) → always full pipeline
|
||||||
#
|
#
|
||||||
# Deployment mapping (single domain, path-based):
|
# Deployment mapping (single domain, path-based):
|
||||||
# PR → FE + BE :dev → switchboard.DOMAIN/dev/ (DB wipe + fresh schema)
|
# PR → FE + BE :dev → armature.DOMAIN/dev/ (DB wipe + fresh schema)
|
||||||
# Push to main → FE + BE :test → switchboard.DOMAIN/test/ (migrate only)
|
# Push to main → FE + BE :test → armature.DOMAIN/test/ (migrate only)
|
||||||
# Tag v* → FE + BE :latest → switchboard.DOMAIN/ (migrate only)
|
# Tag v* → FE + BE :latest → armature.DOMAIN/ (migrate only)
|
||||||
# → Unified :latest → Docker Hub (not deployed)
|
# → Unified :latest → Docker Hub (not deployed)
|
||||||
#
|
#
|
||||||
# Database lifecycle:
|
# Database lifecycle:
|
||||||
# CI: bootstrap (admin creds) → create DB + role + extensions
|
# CI: bootstrap (admin creds) → create DB + role + extensions
|
||||||
# CI: dev wipe (app creds) → drop tables for fresh install test
|
# CI: dev wipe (app creds) → drop tables for fresh install test
|
||||||
# BE: auto-migrate on startup → schema_migrations tracking
|
# BE: auto-migrate on startup → schema_migrations tracking
|
||||||
# BE: bootstrap admin from SWITCHBOARD_ADMIN_* env vars (upsert on every restart)
|
# BE: bootstrap admin from ARMATURE_ADMIN_* env vars (upsert on every restart)
|
||||||
#
|
#
|
||||||
# Shared PG safety:
|
# Shared PG safety:
|
||||||
# - Bootstrap uses IF NOT EXISTS (idempotent)
|
# - Bootstrap uses IF NOT EXISTS (idempotent)
|
||||||
# - Dev wipe REFUSES to run on databases not ending in _dev
|
# - Dev wipe REFUSES to run on databases not ending in _dev
|
||||||
# - Admin creds never reach the backend pods
|
# - Admin creds never reach the backend pods
|
||||||
# - Each env has its own DB name (switchboard_core_{dev,test,})
|
# - Each env has its own DB name (armature_{dev,test,})
|
||||||
#
|
#
|
||||||
# Required Gitea Variables:
|
# Required Gitea Variables:
|
||||||
# REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST
|
# REGISTRY, NAMESPACE, DOMAIN, POSTGRES_HOST
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
# Required Gitea Secrets:
|
# Required Gitea Secrets:
|
||||||
# POSTGRES_USER, POSTGRES_PASSWORD
|
# POSTGRES_USER, POSTGRES_PASSWORD
|
||||||
# POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD
|
# POSTGRES_ADMIN_USER, POSTGRES_ADMIN_PASSWORD
|
||||||
# SWITCHBOARD_ADMIN_USERNAME, SWITCHBOARD_ADMIN_PASSWORD, SWITCHBOARD_ADMIN_EMAIL
|
# ARMATURE_ADMIN_USERNAME, ARMATURE_ADMIN_PASSWORD, ARMATURE_ADMIN_EMAIL
|
||||||
# ENCRYPTION_KEY — AES-256 key for API key encryption (openssl rand -base64 32)
|
# ENCRYPTION_KEY — AES-256 key for API key encryption (openssl rand -base64 32)
|
||||||
# PROVIDER_KEY — API key for live provider integration tests (optional, tests skip if missing)
|
# PROVIDER_KEY — API key for live provider integration tests (optional, tests skip if missing)
|
||||||
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional)
|
# DOCKERHUB_USERNAME, DOCKERHUB_TOKEN (optional)
|
||||||
@@ -88,8 +88,8 @@ env:
|
|||||||
CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }}
|
CERT_ISSUER: ${{ vars.CERT_ISSUER_PROD || 'letsencrypt-prod' }}
|
||||||
POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }}
|
POSTGRES_HOST: ${{ vars.POSTGRES_HOST || 'postgres-primary.postgres.svc.cluster.local' }}
|
||||||
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
|
POSTGRES_PORT: ${{ vars.POSTGRES_PORT || '5432' }}
|
||||||
IMAGE: ${{ vars.REGISTRY }}/switchboard/core
|
IMAGE: ${{ vars.REGISTRY }}/armature
|
||||||
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/switchboard-core' }}
|
DOCKERHUB_IMAGE: ${{ vars.DOCKERHUB_IMAGE || 'gobha/armature' }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Stage 0: Detect Changed Paths ──────────────
|
# ── Stage 0: Detect Changed Paths ──────────────
|
||||||
@@ -319,7 +319,7 @@ jobs:
|
|||||||
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
|
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
|
||||||
APP_USER: ${{ secrets.POSTGRES_USER }}
|
APP_USER: ${{ secrets.POSTGRES_USER }}
|
||||||
APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
APP_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
DB_NAME: switchboard_core_ci
|
DB_NAME: armature_ci
|
||||||
run: |
|
run: |
|
||||||
echo "━━━ CI Test Database Setup ━━━"
|
echo "━━━ CI Test Database Setup ━━━"
|
||||||
# Create DB for Go integration tests (admin creds)
|
# Create DB for Go integration tests (admin creds)
|
||||||
@@ -363,7 +363,7 @@ jobs:
|
|||||||
PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }}
|
PGUSER: ${{ secrets.POSTGRES_ADMIN_USER }}
|
||||||
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
|
PGPASSWORD: ${{ secrets.POSTGRES_ADMIN_PASSWORD }}
|
||||||
run: |
|
run: |
|
||||||
psql -c "DROP DATABASE IF EXISTS switchboard_core_ci;" postgres
|
psql -c "DROP DATABASE IF EXISTS armature_ci;" postgres
|
||||||
echo "✓ Dropped CI test database"
|
echo "✓ Dropped CI test database"
|
||||||
|
|
||||||
# ── Stage 2: Build, Database, Deploy ─────────
|
# ── Stage 2: Build, Database, Deploy ─────────
|
||||||
@@ -395,9 +395,9 @@ jobs:
|
|||||||
- name: Determine environment
|
- name: Determine environment
|
||||||
id: setup
|
id: setup
|
||||||
run: |
|
run: |
|
||||||
# All environments share one host: switchboard.DOMAIN
|
# All environments share one host: armature.DOMAIN
|
||||||
# Environments are separated by path prefix (BASE_PATH)
|
# Environments are separated by path prefix (BASE_PATH)
|
||||||
DEPLOY_HOST="switchboard.${DOMAIN}"
|
DEPLOY_HOST="armature.${DOMAIN}"
|
||||||
echo "DEPLOY_HOST=${DEPLOY_HOST}" >> "$GITHUB_OUTPUT"
|
echo "DEPLOY_HOST=${DEPLOY_HOST}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ gitea.event_name }}" == "pull_request" ]]; then
|
||||||
@@ -406,7 +406,7 @@ jobs:
|
|||||||
echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT"
|
echo "IMAGE_TAG=dev" >> "$GITHUB_OUTPUT"
|
||||||
echo "BASE_PATH=/dev" >> "$GITHUB_OUTPUT"
|
echo "BASE_PATH=/dev" >> "$GITHUB_OUTPUT"
|
||||||
echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT"
|
echo "DEPLOY_SUFFIX=-dev" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_NAME=switchboard_core_dev" >> "$GITHUB_OUTPUT"
|
echo "DB_NAME=armature_dev" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_WIPE=true" >> "$GITHUB_OUTPUT"
|
echo "DB_WIPE=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
|
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||||
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
||||||
@@ -422,7 +422,7 @@ jobs:
|
|||||||
echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "EXTRA_TAG=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "BASE_PATH=" >> "$GITHUB_OUTPUT"
|
echo "BASE_PATH=" >> "$GITHUB_OUTPUT"
|
||||||
echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT"
|
echo "DEPLOY_SUFFIX=" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_NAME=switchboard_core" >> "$GITHUB_OUTPUT"
|
echo "DB_NAME=armature" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "REPLICAS=2" >> "$GITHUB_OUTPUT"
|
echo "REPLICAS=2" >> "$GITHUB_OUTPUT"
|
||||||
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
||||||
@@ -437,7 +437,7 @@ jobs:
|
|||||||
echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT"
|
echo "IMAGE_TAG=test" >> "$GITHUB_OUTPUT"
|
||||||
echo "BASE_PATH=/test" >> "$GITHUB_OUTPUT"
|
echo "BASE_PATH=/test" >> "$GITHUB_OUTPUT"
|
||||||
echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT"
|
echo "DEPLOY_SUFFIX=-test" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_NAME=switchboard_core_test" >> "$GITHUB_OUTPUT"
|
echo "DB_NAME=armature_test" >> "$GITHUB_OUTPUT"
|
||||||
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
echo "DB_WIPE=false" >> "$GITHUB_OUTPUT"
|
||||||
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
|
echo "REPLICAS=1" >> "$GITHUB_OUTPUT"
|
||||||
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
echo "MEMORY_REQUEST=256Mi" >> "$GITHUB_OUTPUT"
|
||||||
@@ -579,14 +579,17 @@ jobs:
|
|||||||
# ── Push to Docker Hub (release only) ────────
|
# ── Push to Docker Hub (release only) ────────
|
||||||
- name: Push to Docker Hub
|
- name: Push to Docker Hub
|
||||||
if: steps.setup.outputs.is_release == 'true'
|
if: steps.setup.outputs.is_release == 'true'
|
||||||
|
env:
|
||||||
|
DH_USER: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
DH_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:latest
|
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:latest
|
||||||
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
|
docker tag ${IMAGE}:${{ steps.setup.outputs.IMAGE_TAG }} ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
|
||||||
if [[ -n "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then
|
if [[ -n "${DH_TOKEN}" ]]; then
|
||||||
echo "${{ secrets.DOCKERHUB_TOKEN }}" | \
|
echo "${DH_TOKEN}" | docker login -u "${DH_USER}" --password-stdin
|
||||||
docker login -u "${{ secrets.DOCKERHUB_USERNAME }}" --password-stdin
|
|
||||||
docker push ${DOCKERHUB_IMAGE}:latest
|
docker push ${DOCKERHUB_IMAGE}:latest
|
||||||
docker push ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
|
docker push ${DOCKERHUB_IMAGE}:${{ steps.setup.outputs.EXTRA_TAG }}
|
||||||
|
echo "✓ Pushed to Docker Hub"
|
||||||
else
|
else
|
||||||
echo "⚠ Docker Hub credentials not configured, skipping push"
|
echo "⚠ Docker Hub credentials not configured, skipping push"
|
||||||
fi
|
fi
|
||||||
@@ -600,31 +603,31 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
PG_USER: ${{ secrets.POSTGRES_USER }}
|
PG_USER: ${{ secrets.POSTGRES_USER }}
|
||||||
PG_PASS: ${{ secrets.POSTGRES_PASSWORD }}
|
PG_PASS: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
SW_ADMIN_USER: ${{ secrets.SWITCHBOARD_ADMIN_USERNAME }}
|
SW_ADMIN_USER: ${{ secrets.ARMATURE_ADMIN_USERNAME }}
|
||||||
SW_ADMIN_PASS: ${{ secrets.SWITCHBOARD_ADMIN_PASSWORD }}
|
SW_ADMIN_PASS: ${{ secrets.ARMATURE_ADMIN_PASSWORD }}
|
||||||
SW_ADMIN_EMAIL: ${{ secrets.SWITCHBOARD_ADMIN_EMAIL }}
|
SW_ADMIN_EMAIL: ${{ secrets.ARMATURE_ADMIN_EMAIL }}
|
||||||
SEED_USERS: ${{ vars.SEED_USERS }}
|
SEED_USERS: ${{ vars.SEED_USERS }}
|
||||||
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }}
|
||||||
run: |
|
run: |
|
||||||
kubectl create secret generic switchboard-db-credentials \
|
kubectl create secret generic armature-db-credentials \
|
||||||
--namespace=${NAMESPACE} \
|
--namespace=${NAMESPACE} \
|
||||||
--from-literal=POSTGRES_USER="${PG_USER}" \
|
--from-literal=POSTGRES_USER="${PG_USER}" \
|
||||||
--from-literal=POSTGRES_PASSWORD="${PG_PASS}" \
|
--from-literal=POSTGRES_PASSWORD="${PG_PASS}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
kubectl create secret generic switchboard-admin \
|
kubectl create secret generic armature-admin \
|
||||||
--namespace=${NAMESPACE} \
|
--namespace=${NAMESPACE} \
|
||||||
--from-literal=username="${SW_ADMIN_USER}" \
|
--from-literal=username="${SW_ADMIN_USER}" \
|
||||||
--from-literal=password="${SW_ADMIN_PASS}" \
|
--from-literal=password="${SW_ADMIN_PASS}" \
|
||||||
--from-literal=email="${SW_ADMIN_EMAIL}" \
|
--from-literal=email="${SW_ADMIN_EMAIL}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
kubectl create secret generic switchboard-seed-users \
|
kubectl create secret generic armature-seed-users \
|
||||||
--namespace=${NAMESPACE} \
|
--namespace=${NAMESPACE} \
|
||||||
--from-literal=users="${SEED_USERS}" \
|
--from-literal=users="${SEED_USERS}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
|
|
||||||
kubectl create secret generic switchboard-encryption \
|
kubectl create secret generic armature-encryption \
|
||||||
--namespace=${NAMESPACE} \
|
--namespace=${NAMESPACE} \
|
||||||
--from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
|
--from-literal=ENCRYPTION_KEY="${ENCRYPTION_KEY}" \
|
||||||
--dry-run=client -o yaml | kubectl apply -f -
|
--dry-run=client -o yaml | kubectl apply -f -
|
||||||
@@ -639,7 +642,7 @@ jobs:
|
|||||||
S3_REGION: ${{ vars.S3_REGION || 'us-east-1' }}
|
S3_REGION: ${{ vars.S3_REGION || 'us-east-1' }}
|
||||||
S3_PREFIX: ${{ vars.S3_PREFIX }}
|
S3_PREFIX: ${{ vars.S3_PREFIX }}
|
||||||
run: |
|
run: |
|
||||||
kubectl create secret generic switchboard-s3 \
|
kubectl create secret generic armature-s3 \
|
||||||
--namespace=${NAMESPACE} \
|
--namespace=${NAMESPACE} \
|
||||||
--from-literal=S3_ENDPOINT="${S3_ENDPOINT}" \
|
--from-literal=S3_ENDPOINT="${S3_ENDPOINT}" \
|
||||||
--from-literal=S3_BUCKET="${S3_BUCKET}" \
|
--from-literal=S3_BUCKET="${S3_BUCKET}" \
|
||||||
@@ -673,7 +676,7 @@ jobs:
|
|||||||
envsubst < k8s/storage-pvc.yaml > /tmp/storage-pvc.yaml
|
envsubst < k8s/storage-pvc.yaml > /tmp/storage-pvc.yaml
|
||||||
fi
|
fi
|
||||||
|
|
||||||
envsubst < k8s/switchboard.yaml > /tmp/switchboard.yaml
|
envsubst < k8s/armature.yaml > /tmp/armature.yaml
|
||||||
envsubst < k8s/middleware-retry.yaml > /tmp/middleware-retry.yaml
|
envsubst < k8s/middleware-retry.yaml > /tmp/middleware-retry.yaml
|
||||||
envsubst < k8s/ingress.yaml > /tmp/ingress.yaml
|
envsubst < k8s/ingress.yaml > /tmp/ingress.yaml
|
||||||
|
|
||||||
@@ -687,7 +690,7 @@ jobs:
|
|||||||
echo " ⚠ STORAGE_CLASS not set — file storage disabled"
|
echo " ⚠ STORAGE_CLASS not set — file storage disabled"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
kubectl apply -f /tmp/switchboard.yaml
|
kubectl apply -f /tmp/armature.yaml
|
||||||
|
|
||||||
# Traefik retry middleware (v0.28.8) — requires RBAC for traefik.io CRDs.
|
# Traefik retry middleware (v0.28.8) — requires RBAC for traefik.io CRDs.
|
||||||
# First-time setup: kubectl apply -f k8s/rbac-traefik.yaml (cluster admin)
|
# First-time setup: kubectl apply -f k8s/rbac-traefik.yaml (cluster admin)
|
||||||
@@ -706,8 +709,8 @@ jobs:
|
|||||||
# Traefik invalidates the entire router if it references a missing middleware,
|
# Traefik invalidates the entire router if it references a missing middleware,
|
||||||
# which kills all routes for this path prefix.
|
# which kills all routes for this path prefix.
|
||||||
if [[ "${MW_READY}" == "true" ]]; then
|
if [[ "${MW_READY}" == "true" ]]; then
|
||||||
MW_REF="${NAMESPACE}-switchboard-retry${DEPLOY_SUFFIX}@kubernetescrd"
|
MW_REF="${NAMESPACE}-armature-retry${DEPLOY_SUFFIX}@kubernetescrd"
|
||||||
kubectl annotate ingress "switchboard${DEPLOY_SUFFIX}" \
|
kubectl annotate ingress "armature${DEPLOY_SUFFIX}" \
|
||||||
"traefik.ingress.kubernetes.io/router.middlewares=${MW_REF}" \
|
"traefik.ingress.kubernetes.io/router.middlewares=${MW_REF}" \
|
||||||
--namespace="${NAMESPACE}" --overwrite
|
--namespace="${NAMESPACE}" --overwrite
|
||||||
echo " ✓ Ingress annotated with retry middleware"
|
echo " ✓ Ingress annotated with retry middleware"
|
||||||
@@ -718,20 +721,20 @@ jobs:
|
|||||||
SUFFIX: ${{ steps.setup.outputs.DEPLOY_SUFFIX }}
|
SUFFIX: ${{ steps.setup.outputs.DEPLOY_SUFFIX }}
|
||||||
ENV: ${{ steps.setup.outputs.ENVIRONMENT }}
|
ENV: ${{ steps.setup.outputs.ENVIRONMENT }}
|
||||||
run: |
|
run: |
|
||||||
kubectl rollout restart deployment/switchboard${SUFFIX} -n ${NAMESPACE}
|
kubectl rollout restart deployment/armature${SUFFIX} -n ${NAMESPACE}
|
||||||
kubectl rollout status deployment/switchboard${SUFFIX} -n ${NAMESPACE} --timeout=180s
|
kubectl rollout status deployment/armature${SUFFIX} -n ${NAMESPACE} --timeout=180s
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "━━━ Pod Status ━━━"
|
echo "━━━ Pod Status ━━━"
|
||||||
kubectl get pods -n ${NAMESPACE} -l app=switchboard,env=${ENV}
|
kubectl get pods -n ${NAMESPACE} -l app=armature,env=${ENV}
|
||||||
|
|
||||||
READY=$(kubectl get deployment switchboard${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
|
READY=$(kubectl get deployment armature${SUFFIX} -n ${NAMESPACE} -o jsonpath='{.status.readyReplicas}')
|
||||||
|
|
||||||
if [[ "${READY:-0}" -gt 0 ]]; then
|
if [[ "${READY:-0}" -gt 0 ]]; then
|
||||||
echo "✅ Healthy: ${READY} pods ready"
|
echo "✅ Healthy: ${READY} pods ready"
|
||||||
else
|
else
|
||||||
echo "❌ Unhealthy: ${READY:-0} pods ready"
|
echo "❌ Unhealthy: ${READY:-0} pods ready"
|
||||||
kubectl logs -n ${NAMESPACE} -l app=switchboard,env=${ENV} --tail=30
|
kubectl logs -n ${NAMESPACE} -l app=armature,env=${ENV} --tail=30
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
18
CHANGELOG.md
18
CHANGELOG.md
@@ -1,6 +1,6 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
All notable changes to Switchboard Core are documented here.
|
All notable changes to Armature are documented here.
|
||||||
|
|
||||||
## v0.6.7 — Native mTLS
|
## v0.6.7 — Native mTLS
|
||||||
|
|
||||||
@@ -28,7 +28,7 @@ deployments where the Go binary terminates TLS itself.
|
|||||||
- **Peer TLS config**: `BuildPeerTLSConfig()` constructs a `*tls.Config` for
|
- **Peer TLS config**: `BuildPeerTLSConfig()` constructs a `*tls.Config` for
|
||||||
outbound node-to-node connections (forward-looking — cluster registry is
|
outbound node-to-node connections (forward-looking — cluster registry is
|
||||||
currently DB-backed with no HTTP peer calls).
|
currently DB-backed with no HTTP peer calls).
|
||||||
- **`switchboard-ca.sh`**: Shell wrapper around openssl for cert provisioning.
|
- **`armature-ca.sh`**: Shell wrapper around openssl for cert provisioning.
|
||||||
Three commands: `init` (CA keypair), `issue-node` (365d, ServerAuth +
|
Three commands: `init` (CA keypair), `issue-node` (365d, ServerAuth +
|
||||||
ClientAuth EKU), `issue-user` (90d, ClientAuth only). All ECDSA P-256, PEM
|
ClientAuth EKU), `issue-user` (90d, ClientAuth only). All ECDSA P-256, PEM
|
||||||
output.
|
output.
|
||||||
@@ -875,7 +875,7 @@ Indexes on `folder_id`, `creator_id`, `pinned`, `updated_at`.
|
|||||||
- **Migration 012**: Adds `bundled` to `packages.source` CHECK constraint
|
- **Migration 012**: Adds `bundled` to `packages.source` CHECK constraint
|
||||||
(both Postgres and SQLite).
|
(both Postgres and SQLite).
|
||||||
- **K8s manifest**: `SKIP_BUNDLED_PACKAGES` and `BUNDLED_PACKAGES` env vars
|
- **K8s manifest**: `SKIP_BUNDLED_PACKAGES` and `BUNDLED_PACKAGES` env vars
|
||||||
added to `k8s/switchboard.yaml`.
|
added to `k8s/armature.yaml`.
|
||||||
- **Tests**: 6 handler tests (fresh install, skip existing, missing dir, empty
|
- **Tests**: 6 handler tests (fresh install, skip existing, missing dir, empty
|
||||||
dir, dormant handling, allowlist filtering).
|
dir, dormant handling, allowlist filtering).
|
||||||
|
|
||||||
@@ -1339,7 +1339,7 @@ Indexes on `folder_id`, `creator_id`, `pinned`, `updated_at`.
|
|||||||
(`"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]`).
|
(`"triggers": [{"type": "event", "pattern": "workflow.completed", ...}]`).
|
||||||
Wired via `bus.Subscribe()` on startup. Handlers fire asynchronously.
|
Wired via `bus.Subscribe()` on startup. Handlers fire asynchronously.
|
||||||
- **Webhook triggers**: Inbound HTTP at `/api/v1/hooks/:package_id/:slug`.
|
- **Webhook triggers**: Inbound HTTP at `/api/v1/hooks/:package_id/:slug`.
|
||||||
HMAC-SHA256 verification via `X-Switchboard-Signature` header. Synchronous
|
HMAC-SHA256 verification via `X-Armature-Signature` header. Synchronous
|
||||||
Starlark handler can return custom HTTP status and body.
|
Starlark handler can return custom HTTP status and body.
|
||||||
- **Scheduled tasks**: User-created cron-scheduled Starlark scripts with
|
- **Scheduled tasks**: User-created cron-scheduled Starlark scripts with
|
||||||
restricted sandbox (no raw HTTP, no DB table creation, connections-only
|
restricted sandbox (no raw HTTP, no DB table creation, connections-only
|
||||||
@@ -1492,21 +1492,21 @@ storage, and the Starlark sandbox. Everything else is a package.
|
|||||||
- CI deploy: k8s resource quantity vars (`BE_MEMORY_REQUEST` → `MEMORY_REQUEST`)
|
- CI deploy: k8s resource quantity vars (`BE_MEMORY_REQUEST` → `MEMORY_REQUEST`)
|
||||||
aligned with CI workflow outputs — `envsubst` was producing empty strings
|
aligned with CI workflow outputs — `envsubst` was producing empty strings
|
||||||
- CI deploy: image var (`BE_IMAGE` → `IMAGE`) — caused `InvalidImageName` in pods
|
- CI deploy: image var (`BE_IMAGE` → `IMAGE`) — caused `InvalidImageName` in pods
|
||||||
- CI rollout: deployment name (`switchboard` → `switchboard-be`) — rollout
|
- CI rollout: deployment name (`switchboard` → `armature-be`) — rollout
|
||||||
verification was looking for wrong deployment name
|
verification was looking for wrong deployment name
|
||||||
- Nginx BASE_PATH: regex cache-header locations intercepted static asset
|
- Nginx BASE_PATH: regex cache-header locations intercepted static asset
|
||||||
requests before alias could strip the sub-path prefix — moved inside alias block
|
requests before alias could strip the sub-path prefix — moved inside alias block
|
||||||
- Post-login blank page: dead Go template references (`surface-chat`,
|
- Post-login blank page: dead Go template references (`surface-chat`,
|
||||||
`surface-notes`, `surface-projects`) caused html/template to silently
|
`surface-notes`, `surface-projects`) caused html/template to silently
|
||||||
produce Content-Length: 0 responses
|
produce Content-Length: 0 responses
|
||||||
- Login branding: "Chat Switchboard" → "Switchboard Core", updated tagline
|
- Login branding: "Chat Armature" → "Armature", updated tagline
|
||||||
and feature pills to reflect platform pivot
|
and feature pills to reflect platform pivot
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Go module: `switchboard-core`
|
- Go module: `armature`
|
||||||
- VERSION: `0.1.0`
|
- VERSION: `0.1.0`
|
||||||
- Default DB name: `switchboard_core`
|
- Default DB name: `armature`
|
||||||
- Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables
|
- Fresh migrations: 9 files × 2 dialects (postgres + sqlite), 27 tables
|
||||||
- Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files)
|
- Store interfaces: 40 → 20 (13 in interfaces.go + 7 in separate iface files)
|
||||||
- Stage modes: `chat_only` removed, `custom` added
|
- Stage modes: `chat_only` removed, `custom` added
|
||||||
@@ -1514,7 +1514,7 @@ storage, and the Starlark sandbox. Everything else is a package.
|
|||||||
- Kernel permissions: 16 → 6 (`extension.use`, `extension.install`,
|
- Kernel permissions: 16 → 6 (`extension.use`, `extension.install`,
|
||||||
`workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`)
|
`workflow.create`, `workflow.submit`, `admin.view`, `token.unlimited`)
|
||||||
- Everyone group seed: `["extension.use","workflow.submit"]`
|
- Everyone group seed: `["extension.use","workflow.submit"]`
|
||||||
- Global settings seed: site name "Switchboard Core"
|
- Global settings seed: site name "Armature"
|
||||||
- Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours,
|
- Config: removed 7 dropped fields (SessionExpiryDays, WorkflowStaleHours,
|
||||||
ProviderAutoDisableThreshold, ExtractionConcurrency, etc.)
|
ProviderAutoDisableThreshold, ExtractionConcurrency, etc.)
|
||||||
- Health stores rewritten: kernel-only Prune for stale tickets, counters, presence
|
- Health stores rewritten: kernel-only Prune for stale tickets, counters, presence
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Contributing to Switchboard Core
|
# Contributing to Armature
|
||||||
|
|
||||||
## Development Setup
|
## Development Setup
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd server
|
cd server
|
||||||
go build -o switchboard-core .
|
go build -o armature .
|
||||||
DB_DRIVER=sqlite DATABASE_URL=/tmp/switchboard.db ./switchboard-core
|
DB_DRIVER=sqlite DATABASE_URL=/tmp/armature.db ./armature
|
||||||
```
|
```
|
||||||
|
|
||||||
**Docker (recommended):**
|
**Docker (recommended):**
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core — Unified Dockerfile
|
# Armature — Unified Dockerfile
|
||||||
# ============================================
|
# ============================================
|
||||||
# Stage 1: Build Go backend
|
# Stage 1: Build Go backend
|
||||||
# Stage 2: Download JS vendor libs (marked, DOMPurify)
|
# Stage 2: Download JS vendor libs (marked, DOMPurify)
|
||||||
@@ -20,7 +20,7 @@ COPY server/go.mod server/go.sum* ./
|
|||||||
COPY server/ .
|
COPY server/ .
|
||||||
COPY VERSION ./
|
COPY VERSION ./
|
||||||
RUN go mod download && go mod tidy && go mod verify
|
RUN go mod download && go mod tidy && go mod verify
|
||||||
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(cat VERSION)" -o /bin/switchboard .
|
RUN CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$(cat VERSION)" -o /bin/armature .
|
||||||
|
|
||||||
# ── Stage 2: Vendor JS libs ─────────────────
|
# ── Stage 2: Vendor JS libs ─────────────────
|
||||||
FROM node:20-alpine AS vendor
|
FROM node:20-alpine AS vendor
|
||||||
@@ -70,7 +70,7 @@ FROM nginx:1-alpine
|
|||||||
RUN apk add --no-cache bash git
|
RUN apk add --no-cache bash git
|
||||||
|
|
||||||
# Go backend binary
|
# Go backend binary
|
||||||
COPY --from=backend /bin/switchboard /usr/local/bin/switchboard
|
COPY --from=backend /bin/armature /usr/local/bin/armature
|
||||||
COPY --from=backend /app/database/migrations /app/database/migrations
|
COPY --from=backend /app/database/migrations /app/database/migrations
|
||||||
|
|
||||||
# Frontend static files
|
# Frontend static files
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core — Builder Image
|
# Armature — Builder Image
|
||||||
# ============================================
|
# ============================================
|
||||||
# Pre-caches Go modules and Node dependencies
|
# Pre-caches Go modules and Node dependencies
|
||||||
# for faster custom builds. Use as a base in
|
# for faster custom builds. Use as a base in
|
||||||
@@ -7,12 +7,12 @@
|
|||||||
# download on every build.
|
# download on every build.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# FROM ghcr.io/switchboard-core/builder:latest AS go-builder
|
# FROM ghcr.io/armature/builder:latest AS go-builder
|
||||||
# COPY server/ /app/
|
# COPY server/ /app/
|
||||||
# RUN cd /app && go build -o /bin/switchboard .
|
# RUN cd /app && go build -o /bin/armature .
|
||||||
#
|
#
|
||||||
# Or build this image locally:
|
# Or build this image locally:
|
||||||
# docker build -f Dockerfile.builder -t switchboard-builder .
|
# docker build -f Dockerfile.builder -t armature-builder .
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
# ── Go module cache ─────────────────────────
|
# ── Go module cache ─────────────────────────
|
||||||
@@ -61,5 +61,5 @@ RUN ln -sf /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm
|
|||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
LABEL org.opencontainers.image.title="Switchboard Core Builder"
|
LABEL org.opencontainers.image.title="Armature Builder"
|
||||||
LABEL org.opencontainers.image.description="Pre-cached build dependencies for faster custom Switchboard Core builds"
|
LABEL org.opencontainers.image.description="Pre-cached build dependencies for faster custom Armature builds"
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Switchboard Core
|
# Armature
|
||||||
|
|
||||||
A self-hosted extension platform. Identity, teams, permissions, workflows,
|
A self-hosted extension platform. Identity, teams, permissions, workflows,
|
||||||
and a package system. Everything else ships as installable extensions.
|
and a package system. Everything else ships as installable extensions.
|
||||||
|
|
||||||
## What This Is
|
## What This Is
|
||||||
|
|
||||||
Switchboard Core is the kernel. It provides the primitives that extensions
|
Armature is the kernel. It provides the primitives that extensions
|
||||||
build on: users, teams, groups, scoped credentials, a Starlark sandbox,
|
build on: users, teams, groups, scoped credentials, a Starlark sandbox,
|
||||||
workflow orchestration, notifications, and a package installer. It does not
|
workflow orchestration, notifications, and a package installer. It does not
|
||||||
include AI chat, providers, personas, or any domain-specific features —
|
include AI chat, providers, personas, or any domain-specific features —
|
||||||
@@ -27,7 +27,7 @@ docker compose up --build
|
|||||||
# → http://localhost:3000 (admin/admin)
|
# → http://localhost:3000 (admin/admin)
|
||||||
|
|
||||||
# Or from source
|
# Or from source
|
||||||
git clone <repo-url> && cd switchboard-core
|
git clone <repo-url> && cd armature
|
||||||
cp server/.env.example server/.env # edit DB credentials
|
cp server/.env.example server/.env # edit DB credentials
|
||||||
cd server && go run .
|
cd server && go run .
|
||||||
# → http://localhost:8080
|
# → http://localhost:8080
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Switchboard Core — Roadmap
|
# Armature — Roadmap
|
||||||
|
|
||||||
## Current: v0.6.7 — Native mTLS
|
## Current: v0.6.7 — Native mTLS
|
||||||
|
|
||||||
@@ -135,7 +135,7 @@ End-to-end mutual TLS without a reverse proxy. Targets systemd+podman deployment
|
|||||||
| TLS server mode | ✅ | Go binary calls `ListenAndServeTLS` directly. `TLS_CERT`, `TLS_KEY`, `TLS_CA` path config. TLS 1.3 minimum, no fallback. `server/config/tls.go` loader + validation. |
|
| TLS server mode | ✅ | Go binary calls `ListenAndServeTLS` directly. `TLS_CERT`, `TLS_KEY`, `TLS_CA` path config. TLS 1.3 minimum, no fallback. `server/config/tls.go` loader + validation. |
|
||||||
| `MTLSNativeProvider` | ✅ | Reads `r.TLS.PeerCertificates[0]` — no header trust. `Subject.CommonName` → username. `sha256(Raw)` → `external_id`. Auto-provisions `auth_source=mtls`. Renamed existing `mtls.go` → `mtls_proxy.go`. Shared helpers in `mtls_helpers.go`. |
|
| `MTLSNativeProvider` | ✅ | Reads `r.TLS.PeerCertificates[0]` — no header trust. `Subject.CommonName` → username. `sha256(Raw)` → `external_id`. Auto-provisions `auth_source=mtls`. Renamed existing `mtls.go` → `mtls_proxy.go`. Shared helpers in `mtls_helpers.go`. |
|
||||||
| Node-to-node mTLS | ✅ | `BuildPeerTLSConfig()` constructs outbound TLS config with node cert + CA pool. Forward-looking — cluster registry is DB-backed (PG LISTEN/NOTIFY), no HTTP peer calls yet. |
|
| Node-to-node mTLS | ✅ | `BuildPeerTLSConfig()` constructs outbound TLS config with node cert + CA pool. Forward-looking — cluster registry is DB-backed (PG LISTEN/NOTIFY), no HTTP peer calls yet. |
|
||||||
| `switchboard-ca.sh` | ✅ | Shell wrapper around openssl. Three commands: `init` (CA keypair) · `issue-node --name <n> --san <addrs>` (365d) · `issue-user --cn <name>` (90d). All output PEM. |
|
| `armature-ca.sh` | ✅ | Shell wrapper around openssl. Three commands: `init` (CA keypair) · `issue-node --name <n> --san <addrs>` (365d) · `issue-user --cn <name>` (90d). All output PEM. |
|
||||||
| Unit tests | ✅ | Ephemeral CA via `crypto/x509`. Fabricated `PeerCertificates`. Valid cert → user provisioned · no TLS → `ErrNoCert` · empty certs → `ErrNoCert` · no CN → `ErrInvalidCreds`. |
|
| Unit tests | ✅ | Ephemeral CA via `crypto/x509`. Fabricated `PeerCertificates`. Valid cert → user provisioned · no TLS → `ErrNoCert` · empty certs → `ErrNoCert` · no CN → `ErrInvalidCreds`. |
|
||||||
| Integration tests | ✅ | Real TLS listener on localhost. No cert / wrong CA / expired → TLS handshake rejected. Valid cert → 200. Peer certificate CN + email visibility verified. |
|
| Integration tests | ✅ | Real TLS listener on localhost. No cert / wrong CA / expired → TLS handshake rejected. Valid cert → 200. Peer certificate CN + email visibility verified. |
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
apiVersion: v2
|
apiVersion: v2
|
||||||
name: switchboard
|
name: armature
|
||||||
description: Switchboard Core — self-hosted extension platform
|
description: Armature — self-hosted extension platform
|
||||||
type: application
|
type: application
|
||||||
version: 0.1.0
|
version: 0.1.0
|
||||||
appVersion: "0.0.0" # Patched at CI/release time from /VERSION
|
appVersion: "0.0.0" # Patched at CI/release time from /VERSION
|
||||||
home: https://gobha.ai
|
home: https://gobha.ai
|
||||||
sources:
|
sources:
|
||||||
- https://git.gobha.me/switchboard/core
|
- https://git.gobha.me/armature/core
|
||||||
maintainers:
|
maintainers:
|
||||||
- name: xcaliber
|
- name: xcaliber
|
||||||
|
|||||||
@@ -1,27 +1,27 @@
|
|||||||
# Switchboard Core — PrometheusRule alerts (v0.33.0)
|
# Armature Core — PrometheusRule alerts (v0.33.0)
|
||||||
# Source file for the Helm template. Deploy via:
|
# Source file for the Helm template. Deploy via:
|
||||||
# monitoring.prometheusRule.enabled: true
|
# monitoring.prometheusRule.enabled: true
|
||||||
apiVersion: monitoring.coreos.com/v1
|
apiVersion: monitoring.coreos.com/v1
|
||||||
kind: PrometheusRule
|
kind: PrometheusRule
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard-alerts
|
name: armature-alerts
|
||||||
spec:
|
spec:
|
||||||
groups:
|
groups:
|
||||||
- name: switchboard.rules
|
- name: armature.rules
|
||||||
rules:
|
rules:
|
||||||
# Pod restart (possible OOM)
|
# Pod restart (possible OOM)
|
||||||
- alert: SwitchboardPodRestart
|
- alert: ArmaturePodRestart
|
||||||
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
|
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
|
||||||
for: 0m
|
for: 0m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Switchboard backend pod restarted (possible OOM)"
|
summary: "Armature backend pod restarted (possible OOM)"
|
||||||
description: "Container {{ $labels.container }} in pod {{ $labels.pod }} restarted."
|
description: "Container {{ $labels.container }} in pod {{ $labels.pod }} restarted."
|
||||||
|
|
||||||
# Provider down for 5+ minutes
|
# Provider down for 5+ minutes
|
||||||
- alert: SwitchboardProviderDown
|
- alert: ArmatureProviderDown
|
||||||
expr: switchboard_provider_status > 2
|
expr: armature_provider_status > 2
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
@@ -29,8 +29,8 @@ spec:
|
|||||||
summary: "Provider {{ $labels.provider_config_id }} is down"
|
summary: "Provider {{ $labels.provider_config_id }} is down"
|
||||||
|
|
||||||
# DB pool >80% utilized
|
# DB pool >80% utilized
|
||||||
- alert: SwitchboardDBPoolExhaustion
|
- alert: ArmatureDBPoolExhaustion
|
||||||
expr: switchboard_db_in_use_connections / switchboard_db_open_connections > 0.8
|
expr: armature_db_in_use_connections / armature_db_open_connections > 0.8
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
@@ -38,10 +38,10 @@ spec:
|
|||||||
summary: "DB connection pool >80% utilized"
|
summary: "DB connection pool >80% utilized"
|
||||||
|
|
||||||
# HTTP 5xx error rate >5%
|
# HTTP 5xx error rate >5%
|
||||||
- alert: SwitchboardHighErrorRate
|
- alert: ArmatureHighErrorRate
|
||||||
expr: >
|
expr: >
|
||||||
sum(rate(switchboard_http_requests_total{status=~"5.."}[5m]))
|
sum(rate(armature_http_requests_total{status=~"5.."}[5m]))
|
||||||
/ sum(rate(switchboard_http_requests_total[5m])) > 0.05
|
/ sum(rate(armature_http_requests_total[5m])) > 0.05
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
@@ -49,10 +49,10 @@ spec:
|
|||||||
summary: "HTTP 5xx error rate exceeds 5%"
|
summary: "HTTP 5xx error rate exceeds 5%"
|
||||||
|
|
||||||
# Task failure rate >25%
|
# Task failure rate >25%
|
||||||
- alert: SwitchboardTaskFailureRate
|
- alert: ArmatureTaskFailureRate
|
||||||
expr: >
|
expr: >
|
||||||
rate(switchboard_task_executions_total{status="error"}[15m])
|
rate(armature_task_executions_total{status="error"}[15m])
|
||||||
/ rate(switchboard_task_executions_total[15m]) > 0.25
|
/ rate(armature_task_executions_total[15m]) > 0.25
|
||||||
for: 10m
|
for: 10m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
@@ -60,8 +60,8 @@ spec:
|
|||||||
summary: "Task failure rate exceeds 25% over 15 minutes"
|
summary: "Task failure rate exceeds 25% over 15 minutes"
|
||||||
|
|
||||||
# No completions processed in 15 minutes (canary)
|
# No completions processed in 15 minutes (canary)
|
||||||
- alert: SwitchboardNoCompletions
|
- alert: ArmatureNoCompletions
|
||||||
expr: sum(rate(switchboard_completions_total[10m])) == 0
|
expr: sum(rate(armature_completions_total[10m])) == 0
|
||||||
for: 15m
|
for: 15m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
{ "type": "panel", "id": "gauge", "name": "Gauge", "version": "" }
|
{ "type": "panel", "id": "gauge", "name": "Gauge", "version": "" }
|
||||||
],
|
],
|
||||||
"id": null,
|
"id": null,
|
||||||
"uid": "switchboard-overview",
|
"uid": "armature-overview",
|
||||||
"title": "Switchboard Core — Overview",
|
"title": "Armature — Overview",
|
||||||
"description": "System overview: request rates, latency, provider health, token usage, DB pool.",
|
"description": "System overview: request rates, latency, provider health, token usage, DB pool.",
|
||||||
"tags": ["switchboard"],
|
"tags": ["armature"],
|
||||||
"timezone": "browser",
|
"timezone": "browser",
|
||||||
"refresh": "30s",
|
"refresh": "30s",
|
||||||
"schemaVersion": 38,
|
"schemaVersion": 38,
|
||||||
@@ -37,7 +37,7 @@
|
|||||||
"name": "namespace",
|
"name": "namespace",
|
||||||
"type": "query",
|
"type": "query",
|
||||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
"query": "label_values(switchboard_http_requests_total, namespace)",
|
"query": "label_values(armature_http_requests_total, namespace)",
|
||||||
"includeAll": true,
|
"includeAll": true,
|
||||||
"current": { "text": "All", "value": "$__all" }
|
"current": { "text": "All", "value": "$__all" }
|
||||||
},
|
},
|
||||||
@@ -45,7 +45,7 @@
|
|||||||
"name": "pod",
|
"name": "pod",
|
||||||
"type": "query",
|
"type": "query",
|
||||||
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
"datasource": { "type": "prometheus", "uid": "${datasource}" },
|
||||||
"query": "label_values(switchboard_http_requests_total{namespace=~\"$namespace\"}, pod)",
|
"query": "label_values(armature_http_requests_total{namespace=~\"$namespace\"}, pod)",
|
||||||
"includeAll": true,
|
"includeAll": true,
|
||||||
"current": { "text": "All", "value": "$__all" }
|
"current": { "text": "All", "value": "$__all" }
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 0 },
|
"gridPos": { "h": 8, "w": 6, "x": 0, "y": 0 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\"}[5m]))",
|
"expr": "sum(rate(armature_http_requests_total{namespace=~\"$namespace\"}[5m]))",
|
||||||
"legendFormat": "Total req/s"
|
"legendFormat": "Total req/s"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 6, "x": 6, "y": 0 },
|
"gridPos": { "h": 8, "w": 6, "x": 6, "y": 0 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\",status=~\"5..\"}[5m])) / sum(rate(switchboard_http_requests_total{namespace=~\"$namespace\"}[5m]))",
|
"expr": "sum(rate(armature_http_requests_total{namespace=~\"$namespace\",status=~\"5..\"}[5m])) / sum(rate(armature_http_requests_total{namespace=~\"$namespace\"}[5m]))",
|
||||||
"legendFormat": "5xx rate"
|
"legendFormat": "5xx rate"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -93,15 +93,15 @@
|
|||||||
"gridPos": { "h": 8, "w": 6, "x": 12, "y": 0 },
|
"gridPos": { "h": 8, "w": 6, "x": 12, "y": 0 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "histogram_quantile(0.50, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.50, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
||||||
"legendFormat": "p50"
|
"legendFormat": "p50"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"expr": "histogram_quantile(0.95, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.95, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
||||||
"legendFormat": "p95"
|
"legendFormat": "p95"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"expr": "histogram_quantile(0.99, sum(rate(switchboard_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
"expr": "histogram_quantile(0.99, sum(rate(armature_http_request_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])) by (le))",
|
||||||
"legendFormat": "p99"
|
"legendFormat": "p99"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -113,7 +113,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 6, "x": 18, "y": 0 },
|
"gridPos": { "h": 8, "w": 6, "x": 18, "y": 0 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum(switchboard_websocket_connections{namespace=~\"$namespace\"})",
|
"expr": "sum(armature_websocket_connections{namespace=~\"$namespace\"})",
|
||||||
"legendFormat": "Active"
|
"legendFormat": "Active"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum by (provider_config_id) (rate(switchboard_completions_total{namespace=~\"$namespace\"}[5m]))",
|
"expr": "sum by (provider_config_id) (rate(armature_completions_total{namespace=~\"$namespace\"}[5m]))",
|
||||||
"legendFormat": "{{provider_config_id}}"
|
"legendFormat": "{{provider_config_id}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "histogram_quantile(0.95, sum by (provider_config_id, le) (rate(switchboard_completion_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])))",
|
"expr": "histogram_quantile(0.95, sum by (provider_config_id, le) (rate(armature_completion_duration_seconds_bucket{namespace=~\"$namespace\"}[5m])))",
|
||||||
"legendFormat": "{{provider_config_id}}"
|
"legendFormat": "{{provider_config_id}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum by (model_id) (rate(switchboard_completion_tokens_total{namespace=~\"$namespace\"}[5m])) * 60",
|
"expr": "sum by (model_id) (rate(armature_completion_tokens_total{namespace=~\"$namespace\"}[5m])) * 60",
|
||||||
"legendFormat": "{{model_id}}"
|
"legendFormat": "{{model_id}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "switchboard_provider_status{namespace=~\"$namespace\"}",
|
"expr": "armature_provider_status{namespace=~\"$namespace\"}",
|
||||||
"legendFormat": "{{provider_config_id}}"
|
"legendFormat": "{{provider_config_id}}"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@@ -179,15 +179,15 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
|
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 24 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "switchboard_db_open_connections{namespace=~\"$namespace\"}",
|
"expr": "armature_db_open_connections{namespace=~\"$namespace\"}",
|
||||||
"legendFormat": "Open"
|
"legendFormat": "Open"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"expr": "switchboard_db_in_use_connections{namespace=~\"$namespace\"}",
|
"expr": "armature_db_in_use_connections{namespace=~\"$namespace\"}",
|
||||||
"legendFormat": "In Use"
|
"legendFormat": "In Use"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"expr": "switchboard_db_idle_connections{namespace=~\"$namespace\"}",
|
"expr": "armature_db_idle_connections{namespace=~\"$namespace\"}",
|
||||||
"legendFormat": "Idle"
|
"legendFormat": "Idle"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -198,7 +198,7 @@
|
|||||||
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
|
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 24 },
|
||||||
"targets": [
|
"targets": [
|
||||||
{
|
{
|
||||||
"expr": "sum by (status) (rate(switchboard_task_executions_total{namespace=~\"$namespace\"}[5m]))",
|
"expr": "sum by (status) (rate(armature_task_executions_total{namespace=~\"$namespace\"}[5m]))",
|
||||||
"legendFormat": "{{status}}"
|
"legendFormat": "{{status}}"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
Chat Switchboard {{ .Chart.AppVersion }} deployed.
|
Armature {{ .Chart.AppVersion }} deployed.
|
||||||
|
|
||||||
{{- if .Values.ingress.enabled }}
|
{{- if .Values.ingress.enabled }}
|
||||||
Access the application at:
|
Access the application at:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{/*
|
{{/*
|
||||||
Common labels
|
Common labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.labels" -}}
|
{{- define "armature.labels" -}}
|
||||||
app.kubernetes.io/name: {{ .Chart.Name }}
|
app.kubernetes.io/name: {{ .Chart.Name }}
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
app.kubernetes.io/instance: {{ .Release.Name }}
|
||||||
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
|
||||||
@@ -12,23 +12,23 @@ helm.sh/chart: {{ .Chart.Name }}-{{ .Chart.Version }}
|
|||||||
{{/*
|
{{/*
|
||||||
Backend selector labels
|
Backend selector labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.backend.labels" -}}
|
{{- define "armature.backend.labels" -}}
|
||||||
app.kubernetes.io/component: backend
|
app.kubernetes.io/component: backend
|
||||||
{{ include "switchboard.labels" . }}
|
{{ include "armature.labels" . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Frontend selector labels
|
Frontend selector labels
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.frontend.labels" -}}
|
{{- define "armature.frontend.labels" -}}
|
||||||
app.kubernetes.io/component: frontend
|
app.kubernetes.io/component: frontend
|
||||||
{{ include "switchboard.labels" . }}
|
{{ include "armature.labels" . }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Secret name
|
Secret name
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.secretName" -}}
|
{{- define "armature.secretName" -}}
|
||||||
{{- if .Values.existingSecret -}}
|
{{- if .Values.existingSecret -}}
|
||||||
{{ .Values.existingSecret }}
|
{{ .Values.existingSecret }}
|
||||||
{{- else -}}
|
{{- else -}}
|
||||||
@@ -39,21 +39,21 @@ Secret name
|
|||||||
{{/*
|
{{/*
|
||||||
Backend image
|
Backend image
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.backend.image" -}}
|
{{- define "armature.backend.image" -}}
|
||||||
{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}
|
{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Frontend image
|
Frontend image
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.frontend.image" -}}
|
{{- define "armature.frontend.image" -}}
|
||||||
{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}
|
{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag | default .Chart.AppVersion }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
{{/*
|
{{/*
|
||||||
Database URL — assembled from postgres.* if url is empty
|
Database URL — assembled from postgres.* if url is empty
|
||||||
*/}}
|
*/}}
|
||||||
{{- define "switchboard.databaseURL" -}}
|
{{- define "armature.databaseURL" -}}
|
||||||
{{- if .Values.database.url -}}
|
{{- if .Values.database.url -}}
|
||||||
{{ .Values.database.url }}
|
{{ .Values.database.url }}
|
||||||
{{- else if eq .Values.database.driver "sqlite" -}}
|
{{- else if eq .Values.database.driver "sqlite" -}}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kind: ConfigMap
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-config
|
name: {{ .Release.Name }}-config
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
data:
|
data:
|
||||||
PORT: {{ .Values.backend.port | quote }}
|
PORT: {{ .Values.backend.port | quote }}
|
||||||
BASE_PATH: {{ .Values.basePath | quote }}
|
BASE_PATH: {{ .Values.basePath | quote }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ kind: CronJob
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-backup
|
name: {{ .Release.Name }}-backup
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
app.kubernetes.io/component: backup
|
app.kubernetes.io/component: backup
|
||||||
spec:
|
spec:
|
||||||
schedule: {{ .Values.backup.schedule | quote }}
|
schedule: {{ .Values.backup.schedule | quote }}
|
||||||
@@ -35,7 +35,7 @@ spec:
|
|||||||
- |
|
- |
|
||||||
set -e
|
set -e
|
||||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||||
FILENAME="switchboard-${TIMESTAMP}.sql.gz"
|
FILENAME="armature-${TIMESTAMP}.sql.gz"
|
||||||
|
|
||||||
echo "Starting backup: ${FILENAME}"
|
echo "Starting backup: ${FILENAME}"
|
||||||
pg_dump "${DATABASE_URL}" | gzip > "/backup/${FILENAME}"
|
pg_dump "${DATABASE_URL}" | gzip > "/backup/${FILENAME}"
|
||||||
@@ -58,22 +58,22 @@ spec:
|
|||||||
|
|
||||||
# Prune old local backups beyond retention
|
# Prune old local backups beyond retention
|
||||||
cd /backup
|
cd /backup
|
||||||
ls -t switchboard-*.sql.gz 2>/dev/null | tail -n +{{ add1 .Values.backup.retention }} | xargs -r rm -v
|
ls -t armature-*.sql.gz 2>/dev/null | tail -n +{{ add1 .Values.backup.retention }} | xargs -r rm -v
|
||||||
echo "Backup complete"
|
echo "Backup complete"
|
||||||
env:
|
env:
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
value: {{ include "switchboard.databaseURL" . | quote }}
|
value: {{ include "armature.databaseURL" . | quote }}
|
||||||
{{- if .Values.backup.s3.enabled }}
|
{{- if .Values.backup.s3.enabled }}
|
||||||
- name: S3_ACCESS_KEY
|
- name: S3_ACCESS_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ include "switchboard.secretName" . }}
|
name: {{ include "armature.secretName" . }}
|
||||||
key: BACKUP_S3_ACCESS_KEY
|
key: BACKUP_S3_ACCESS_KEY
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_SECRET_KEY
|
- name: S3_SECRET_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: {{ include "switchboard.secretName" . }}
|
name: {{ include "armature.secretName" . }}
|
||||||
key: BACKUP_S3_SECRET_KEY
|
key: BACKUP_S3_SECRET_KEY
|
||||||
optional: true
|
optional: true
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kind: Deployment
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-backend
|
name: {{ .Release.Name }}-backend
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.backend.labels" . | nindent 4 }}
|
{{- include "armature.backend.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.backend.replicaCount }}
|
replicas: {{ .Values.backend.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
@@ -38,7 +38,7 @@ spec:
|
|||||||
topologyKey: kubernetes.io/hostname
|
topologyKey: kubernetes.io/hostname
|
||||||
containers:
|
containers:
|
||||||
- name: backend
|
- name: backend
|
||||||
image: {{ include "switchboard.backend.image" . }}
|
image: {{ include "armature.backend.image" . }}
|
||||||
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.backend.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
@@ -48,10 +48,10 @@ spec:
|
|||||||
- configMapRef:
|
- configMapRef:
|
||||||
name: {{ .Release.Name }}-config
|
name: {{ .Release.Name }}-config
|
||||||
- secretRef:
|
- secretRef:
|
||||||
name: {{ include "switchboard.secretName" . }}
|
name: {{ include "armature.secretName" . }}
|
||||||
env:
|
env:
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
value: {{ include "switchboard.databaseURL" . | quote }}
|
value: {{ include "armature.databaseURL" . | quote }}
|
||||||
resources:
|
resources:
|
||||||
{{- toYaml .Values.backend.resources | nindent 12 }}
|
{{- toYaml .Values.backend.resources | nindent 12 }}
|
||||||
{{- if .Values.persistence.enabled }}
|
{{- if .Values.persistence.enabled }}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kind: Deployment
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-frontend
|
name: {{ .Release.Name }}-frontend
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.frontend.labels" . | nindent 4 }}
|
{{- include "armature.frontend.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
replicas: {{ .Values.frontend.replicaCount }}
|
replicas: {{ .Values.frontend.replicaCount }}
|
||||||
selector:
|
selector:
|
||||||
@@ -24,7 +24,7 @@ spec:
|
|||||||
{{- end }}
|
{{- end }}
|
||||||
containers:
|
containers:
|
||||||
- name: frontend
|
- name: frontend
|
||||||
image: {{ include "switchboard.frontend.image" . }}
|
image: {{ include "armature.frontend.image" . }}
|
||||||
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
|
imagePullPolicy: {{ .Values.frontend.image.pullPolicy }}
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: ConfigMap
|
kind: ConfigMap
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "switchboard.fullname" . }}-grafana-dashboard
|
name: {{ include "armature.fullname" . }}-grafana-dashboard
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
{{- with .Values.monitoring.grafanaDashboard.labels }}
|
{{- with .Values.monitoring.grafanaDashboard.labels }}
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
data:
|
data:
|
||||||
switchboard-overview.json: |-
|
armature-overview.json: |-
|
||||||
{{- .Files.Get "dashboards/switchboard-overview.json" | nindent 4 }}
|
{{- .Files.Get "dashboards/armature-overview.json" | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ kind: Ingress
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-ingress
|
name: {{ .Release.Name }}-ingress
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
{{- if or .Values.ingress.annotations (and (eq (default "traefik" .Values.ingress.className) "traefik") .Values.ingress.retry.annotateIngress) }}
|
{{- if or .Values.ingress.annotations (and (eq (default "traefik" .Values.ingress.className) "traefik") .Values.ingress.retry.annotateIngress) }}
|
||||||
annotations:
|
annotations:
|
||||||
{{- with .Values.ingress.annotations }}
|
{{- with .Values.ingress.annotations }}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ kind: Middleware
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-retry
|
name: {{ .Release.Name }}-retry
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
retry:
|
retry:
|
||||||
attempts: {{ .Values.ingress.retry.attempts | default 2 }}
|
attempts: {{ .Values.ingress.retry.attempts | default 2 }}
|
||||||
|
|||||||
@@ -2,57 +2,57 @@
|
|||||||
apiVersion: monitoring.coreos.com/v1
|
apiVersion: monitoring.coreos.com/v1
|
||||||
kind: PrometheusRule
|
kind: PrometheusRule
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "switchboard.fullname" . }}-alerts
|
name: {{ include "armature.fullname" . }}-alerts
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
{{- with .Values.monitoring.prometheusRule.labels }}
|
{{- with .Values.monitoring.prometheusRule.labels }}
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
groups:
|
groups:
|
||||||
- name: switchboard.rules
|
- name: armature.rules
|
||||||
rules:
|
rules:
|
||||||
- alert: SwitchboardPodRestart
|
- alert: ArmaturePodRestart
|
||||||
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
|
expr: increase(kube_pod_container_status_restarts_total{container="backend"}[1h]) > 0
|
||||||
for: 0m
|
for: 0m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Switchboard backend pod restarted (possible OOM)"
|
summary: "Armature backend pod restarted (possible OOM)"
|
||||||
- alert: SwitchboardProviderDown
|
- alert: ArmatureProviderDown
|
||||||
expr: switchboard_provider_status > 2
|
expr: armature_provider_status > 2
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Provider {{`{{ $labels.provider_config_id }}`}} is down"
|
summary: "Provider {{`{{ $labels.provider_config_id }}`}} is down"
|
||||||
- alert: SwitchboardDBPoolExhaustion
|
- alert: ArmatureDBPoolExhaustion
|
||||||
expr: switchboard_db_in_use_connections / switchboard_db_open_connections > 0.8
|
expr: armature_db_in_use_connections / armature_db_open_connections > 0.8
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "DB connection pool >80% utilized"
|
summary: "DB connection pool >80% utilized"
|
||||||
- alert: SwitchboardHighErrorRate
|
- alert: ArmatureHighErrorRate
|
||||||
expr: |
|
expr: |
|
||||||
sum(rate(switchboard_http_requests_total{status=~"5.."}[5m]))
|
sum(rate(armature_http_requests_total{status=~"5.."}[5m]))
|
||||||
/ sum(rate(switchboard_http_requests_total[5m])) > 0.05
|
/ sum(rate(armature_http_requests_total[5m])) > 0.05
|
||||||
for: 5m
|
for: 5m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "HTTP 5xx error rate exceeds 5%"
|
summary: "HTTP 5xx error rate exceeds 5%"
|
||||||
- alert: SwitchboardTaskFailureRate
|
- alert: ArmatureTaskFailureRate
|
||||||
expr: |
|
expr: |
|
||||||
rate(switchboard_task_executions_total{status="error"}[15m])
|
rate(armature_task_executions_total{status="error"}[15m])
|
||||||
/ rate(switchboard_task_executions_total[15m]) > 0.25
|
/ rate(armature_task_executions_total[15m]) > 0.25
|
||||||
for: 10m
|
for: 10m
|
||||||
labels:
|
labels:
|
||||||
severity: warning
|
severity: warning
|
||||||
annotations:
|
annotations:
|
||||||
summary: "Task failure rate exceeds 25%"
|
summary: "Task failure rate exceeds 25%"
|
||||||
- alert: SwitchboardNoCompletions
|
- alert: ArmatureNoCompletions
|
||||||
expr: sum(rate(switchboard_completions_total[10m])) == 0
|
expr: sum(rate(armature_completions_total[10m])) == 0
|
||||||
for: 15m
|
for: 15m
|
||||||
labels:
|
labels:
|
||||||
severity: critical
|
severity: critical
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ kind: PersistentVolumeClaim
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-backup
|
name: {{ .Release.Name }}-backup
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
app.kubernetes.io/component: backup
|
app.kubernetes.io/component: backup
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ kind: PersistentVolumeClaim
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-data
|
name: {{ .Release.Name }}-data
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
accessModes:
|
accessModes:
|
||||||
- {{ .Values.persistence.accessMode }}
|
- {{ .Values.persistence.accessMode }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ kind: Secret
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-secrets
|
name: {{ .Release.Name }}-secrets
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
type: Opaque
|
type: Opaque
|
||||||
stringData:
|
stringData:
|
||||||
JWT_SECRET: {{ required "jwtSecret is required" .Values.jwtSecret | quote }}
|
JWT_SECRET: {{ required "jwtSecret is required" .Values.jwtSecret | quote }}
|
||||||
@@ -15,11 +15,11 @@ stringData:
|
|||||||
POSTGRES_PASSWORD: {{ .Values.database.postgres.password | quote }}
|
POSTGRES_PASSWORD: {{ .Values.database.postgres.password | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.admin.password }}
|
{{- if .Values.admin.password }}
|
||||||
SWITCHBOARD_ADMIN_USERNAME: {{ .Values.admin.username | quote }}
|
ARMATURE_ADMIN_USERNAME: {{ .Values.admin.username | quote }}
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: {{ .Values.admin.password | quote }}
|
ARMATURE_ADMIN_PASSWORD: {{ .Values.admin.password | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.admin.email }}
|
{{- if .Values.admin.email }}
|
||||||
SWITCHBOARD_ADMIN_EMAIL: {{ .Values.admin.email | quote }}
|
ARMATURE_ADMIN_EMAIL: {{ .Values.admin.email | quote }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
{{- if .Values.storage.s3.accessKey }}
|
{{- if .Values.storage.s3.accessKey }}
|
||||||
S3_ACCESS_KEY: {{ .Values.storage.s3.accessKey | quote }}
|
S3_ACCESS_KEY: {{ .Values.storage.s3.accessKey | quote }}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
apiVersion: monitoring.coreos.com/v1
|
apiVersion: monitoring.coreos.com/v1
|
||||||
kind: ServiceMonitor
|
kind: ServiceMonitor
|
||||||
metadata:
|
metadata:
|
||||||
name: {{ include "switchboard.fullname" . }}
|
name: {{ include "armature.fullname" . }}
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.labels" . | nindent 4 }}
|
{{- include "armature.labels" . | nindent 4 }}
|
||||||
{{- with .Values.monitoring.serviceMonitor.labels }}
|
{{- with .Values.monitoring.serviceMonitor.labels }}
|
||||||
{{- toYaml . | nindent 4 }}
|
{{- toYaml . | nindent 4 }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
{{- include "switchboard.selectorLabels" . | nindent 6 }}
|
{{- include "armature.selectorLabels" . | nindent 6 }}
|
||||||
app.kubernetes.io/component: backend
|
app.kubernetes.io/component: backend
|
||||||
endpoints:
|
endpoints:
|
||||||
- port: http
|
- port: http
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ kind: Service
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-backend
|
name: {{ .Release.Name }}-backend
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.backend.labels" . | nindent 4 }}
|
{{- include "armature.backend.labels" . | nindent 4 }}
|
||||||
annotations:
|
annotations:
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: {{ .Values.backend.port | quote }}
|
prometheus.io/port: {{ .Values.backend.port | quote }}
|
||||||
@@ -25,7 +25,7 @@ kind: Service
|
|||||||
metadata:
|
metadata:
|
||||||
name: {{ .Release.Name }}-frontend
|
name: {{ .Release.Name }}-frontend
|
||||||
labels:
|
labels:
|
||||||
{{- include "switchboard.frontend.labels" . | nindent 4 }}
|
{{- include "armature.frontend.labels" . | nindent 4 }}
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
# Switchboard Core — Helm values
|
# Armature — Helm values
|
||||||
# helm install switchboard ./chart
|
# helm install armature ./chart
|
||||||
|
|
||||||
# ── Images ─────────────────────────────────
|
# ── Images ─────────────────────────────────
|
||||||
backend:
|
backend:
|
||||||
image:
|
image:
|
||||||
repository: git.gobha.me/switchboard/core
|
repository: git.gobha.me/armature/core
|
||||||
tag: "" # defaults to Chart.appVersion
|
tag: "" # defaults to Chart.appVersion
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
replicaCount: 2 # v0.32.0: multi-replica HA
|
replicaCount: 2 # v0.32.0: multi-replica HA
|
||||||
@@ -19,7 +19,7 @@ backend:
|
|||||||
|
|
||||||
frontend:
|
frontend:
|
||||||
image:
|
image:
|
||||||
repository: git.gobha.me/switchboard/core
|
repository: git.gobha.me/armature/core
|
||||||
tag: "" # defaults to Chart.appVersion
|
tag: "" # defaults to Chart.appVersion
|
||||||
pullPolicy: IfNotPresent
|
pullPolicy: IfNotPresent
|
||||||
replicaCount: 1
|
replicaCount: 1
|
||||||
@@ -41,12 +41,12 @@ database:
|
|||||||
postgres:
|
postgres:
|
||||||
host: postgresql
|
host: postgresql
|
||||||
port: 5432
|
port: 5432
|
||||||
user: switchboard
|
user: armature
|
||||||
password: "" # set via secret
|
password: "" # set via secret
|
||||||
database: switchboard
|
database: armature
|
||||||
sslmode: disable
|
sslmode: disable
|
||||||
# For sqlite: path inside the container (requires persistence)
|
# For sqlite: path inside the container (requires persistence)
|
||||||
sqlitePath: /data/switchboard.db
|
sqlitePath: /data/armature.db
|
||||||
|
|
||||||
# ── Core ───────────────────────────────────
|
# ── Core ───────────────────────────────────
|
||||||
basePath: "" # URL prefix, e.g. "/dev"
|
basePath: "" # URL prefix, e.g. "/dev"
|
||||||
@@ -85,7 +85,7 @@ ingress:
|
|||||||
enabled: true
|
enabled: true
|
||||||
className: traefik
|
className: traefik
|
||||||
annotations: {}
|
annotations: {}
|
||||||
host: switchboard.local
|
host: armature.local
|
||||||
tls:
|
tls:
|
||||||
enabled: false
|
enabled: false
|
||||||
secretName: ""
|
secretName: ""
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ done
|
|||||||
|
|
||||||
# ── Test 4: Stop one replica → stale sweep ────
|
# ── Test 4: Stop one replica → stale sweep ────
|
||||||
echo "=== Test 4: Stop node-3, wait for sweep ==="
|
echo "=== Test 4: Stop node-3, wait for sweep ==="
|
||||||
docker compose -f docker-compose-e2e.yml stop switchboard-3
|
docker compose -f docker-compose-e2e.yml stop armature-3
|
||||||
|
|
||||||
echo " waiting ${STALE_WAIT}s for stale sweep..."
|
echo " waiting ${STALE_WAIT}s for stale sweep..."
|
||||||
sleep "$STALE_WAIT"
|
sleep "$STALE_WAIT"
|
||||||
@@ -118,7 +118,7 @@ fi
|
|||||||
|
|
||||||
# ── Test 5: Restart replica → re-registers ────
|
# ── Test 5: Restart replica → re-registers ────
|
||||||
echo "=== Test 5: Restart node-3 ==="
|
echo "=== Test 5: Restart node-3 ==="
|
||||||
docker compose -f docker-compose-e2e.yml start switchboard-3
|
docker compose -f docker-compose-e2e.yml start armature-3
|
||||||
|
|
||||||
# Wait for startup + at least one heartbeat
|
# Wait for startup + at least one heartbeat
|
||||||
for i in $(seq 1 30); do
|
for i in $(seq 1 30); do
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ events {
|
|||||||
}
|
}
|
||||||
|
|
||||||
http {
|
http {
|
||||||
upstream switchboard {
|
upstream armature {
|
||||||
server switchboard-1:80;
|
server armature-1:80;
|
||||||
server switchboard-2:80;
|
server armature-2:80;
|
||||||
server switchboard-3:80;
|
server armature-3:80;
|
||||||
}
|
}
|
||||||
|
|
||||||
# WebSocket upgrade map
|
# WebSocket upgrade map
|
||||||
@@ -19,7 +19,7 @@ http {
|
|||||||
listen 80;
|
listen 80;
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
proxy_pass http://switchboard;
|
proxy_pass http://armature;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
@@ -28,7 +28,7 @@ http {
|
|||||||
|
|
||||||
# WebSocket endpoint
|
# WebSocket endpoint
|
||||||
location /ws {
|
location /ws {
|
||||||
proxy_pass http://switchboard;
|
proxy_pass http://armature;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
proxy_set_header Connection $connection_upgrade;
|
proxy_set_header Connection $connection_upgrade;
|
||||||
|
|||||||
@@ -83,11 +83,11 @@ authed() {
|
|||||||
echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}"
|
echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}"
|
||||||
|
|
||||||
echo "Building 'old' image from HEAD..."
|
echo "Building 'old' image from HEAD..."
|
||||||
docker build -t switchboard-core:v-old . -q
|
docker build -t armature:v-old . -q
|
||||||
ok "old image built"
|
ok "old image built"
|
||||||
|
|
||||||
echo "Building 'new' image from working tree..."
|
echo "Building 'new' image from working tree..."
|
||||||
docker build -t switchboard-core:v-new . -q
|
docker build -t armature:v-new . -q
|
||||||
ok "new image built"
|
ok "new image built"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════
|
# ═══════════════════════════════════════════════
|
||||||
@@ -97,7 +97,7 @@ ok "new image built"
|
|||||||
echo -e "\n${YELLOW}═══ Phase 2: Start Cluster (Old Version) ═══${NC}"
|
echo -e "\n${YELLOW}═══ Phase 2: Start Cluster (Old Version) ═══${NC}"
|
||||||
|
|
||||||
# Override the build with old image
|
# Override the build with old image
|
||||||
SWITCHBOARD_IMAGE=switchboard-core:v-old \
|
ARMATURE_IMAGE=armature:v-old \
|
||||||
docker compose -f "$COMPOSE_FILE" up postgres -d
|
docker compose -f "$COMPOSE_FILE" up postgres -d
|
||||||
|
|
||||||
# Wait for postgres
|
# Wait for postgres
|
||||||
@@ -107,30 +107,30 @@ sleep 3
|
|||||||
docker run -d --name sb-old-1 --network "$(basename "$(pwd)")_default" \
|
docker run -d --name sb-old-1 --network "$(basename "$(pwd)")_default" \
|
||||||
-e PORT=8080 -e BASE_PATH="" \
|
-e PORT=8080 -e BASE_PATH="" \
|
||||||
-e DB_DRIVER=postgres \
|
-e DB_DRIVER=postgres \
|
||||||
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \
|
-e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
|
||||||
-e JWT_SECRET=e2e-jwt-secret \
|
-e JWT_SECRET=e2e-jwt-secret \
|
||||||
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=admin \
|
-e ARMATURE_ADMIN_PASSWORD=admin \
|
||||||
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
||||||
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
||||||
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
||||||
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
||||||
-p 8081:80 switchboard-core:v-old 2>/dev/null || true
|
-p 8081:80 armature:v-old 2>/dev/null || true
|
||||||
|
|
||||||
docker run -d --name sb-old-2 --network "$(basename "$(pwd)")_default" \
|
docker run -d --name sb-old-2 --network "$(basename "$(pwd)")_default" \
|
||||||
-e PORT=8080 -e BASE_PATH="" \
|
-e PORT=8080 -e BASE_PATH="" \
|
||||||
-e DB_DRIVER=postgres \
|
-e DB_DRIVER=postgres \
|
||||||
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \
|
-e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
|
||||||
-e JWT_SECRET=e2e-jwt-secret \
|
-e JWT_SECRET=e2e-jwt-secret \
|
||||||
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=admin \
|
-e ARMATURE_ADMIN_PASSWORD=admin \
|
||||||
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
||||||
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
||||||
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
||||||
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
||||||
-p 8082:80 switchboard-core:v-old 2>/dev/null || true
|
-p 8082:80 armature:v-old 2>/dev/null || true
|
||||||
|
|
||||||
# Start nginx LB
|
# Start nginx LB
|
||||||
docker compose -f "$COMPOSE_FILE" up lb -d
|
docker compose -f "$COMPOSE_FILE" up lb -d
|
||||||
@@ -189,16 +189,16 @@ echo "Starting new replica-1..."
|
|||||||
docker run -d --name sb-new-1 --network "$(basename "$(pwd)")_default" \
|
docker run -d --name sb-new-1 --network "$(basename "$(pwd)")_default" \
|
||||||
-e PORT=8080 -e BASE_PATH="" \
|
-e PORT=8080 -e BASE_PATH="" \
|
||||||
-e DB_DRIVER=postgres \
|
-e DB_DRIVER=postgres \
|
||||||
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \
|
-e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
|
||||||
-e JWT_SECRET=e2e-jwt-secret \
|
-e JWT_SECRET=e2e-jwt-secret \
|
||||||
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=admin \
|
-e ARMATURE_ADMIN_PASSWORD=admin \
|
||||||
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
||||||
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
||||||
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
||||||
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
||||||
-p 8081:80 switchboard-core:v-new
|
-p 8081:80 armature:v-new
|
||||||
|
|
||||||
wait_for_health "$R1" "new replica-1"
|
wait_for_health "$R1" "new replica-1"
|
||||||
|
|
||||||
@@ -246,16 +246,16 @@ echo "Starting new replica-2..."
|
|||||||
docker run -d --name sb-new-2 --network "$(basename "$(pwd)")_default" \
|
docker run -d --name sb-new-2 --network "$(basename "$(pwd)")_default" \
|
||||||
-e PORT=8080 -e BASE_PATH="" \
|
-e PORT=8080 -e BASE_PATH="" \
|
||||||
-e DB_DRIVER=postgres \
|
-e DB_DRIVER=postgres \
|
||||||
-e "DATABASE_URL=postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable" \
|
-e "DATABASE_URL=postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable" \
|
||||||
-e JWT_SECRET=e2e-jwt-secret \
|
-e JWT_SECRET=e2e-jwt-secret \
|
||||||
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
-e "ENCRYPTION_KEY=e2e-encryption-key-32chars!!!!!" \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=admin \
|
-e ARMATURE_ADMIN_PASSWORD=admin \
|
||||||
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
-e STORAGE_BACKEND=pvc -e STORAGE_PATH=/data/storage \
|
||||||
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
-e "CORS_ALLOWED_ORIGINS=*" -e EXT_ALLOW_PRIVATE_IPS=true \
|
||||||
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
-e LOG_FORMAT=text -e LOG_LEVEL=info \
|
||||||
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
-e "SEED_USERS=alice:password123:user,bob:password456:user" \
|
||||||
-p 8082:80 switchboard-core:v-new
|
-p 8082:80 armature:v-new
|
||||||
|
|
||||||
wait_for_health "$R2" "new replica-2"
|
wait_for_health "$R2" "new replica-2"
|
||||||
|
|
||||||
|
|||||||
@@ -82,13 +82,13 @@ echo -e "\n${YELLOW}═══ Phase 1: Build Images ═══${NC}"
|
|||||||
# Build "old" image from last commit (before current changes)
|
# Build "old" image from last commit (before current changes)
|
||||||
echo "Building 'old' image from HEAD commit..."
|
echo "Building 'old' image from HEAD commit..."
|
||||||
git stash -q 2>/dev/null || true
|
git stash -q 2>/dev/null || true
|
||||||
docker build --no-cache -t switchboard-core:v-old . -q
|
docker build --no-cache -t armature:v-old . -q
|
||||||
git stash pop -q 2>/dev/null || true
|
git stash pop -q 2>/dev/null || true
|
||||||
ok "old image built"
|
ok "old image built"
|
||||||
|
|
||||||
# Build "new" image from working tree (with current changes)
|
# Build "new" image from working tree (with current changes)
|
||||||
echo "Building 'new' image from working tree..."
|
echo "Building 'new' image from working tree..."
|
||||||
docker build --no-cache -t core-switchboard-new . -q
|
docker build --no-cache -t armature:v-new . -q
|
||||||
ok "new image built"
|
ok "new image built"
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════
|
# ═══════════════════════════════════════════════
|
||||||
@@ -97,7 +97,7 @@ ok "new image built"
|
|||||||
|
|
||||||
echo -e "\n${YELLOW}═══ Phase 2: Seed Data on Old Version ═══${NC}"
|
echo -e "\n${YELLOW}═══ Phase 2: Seed Data on Old Version ═══${NC}"
|
||||||
|
|
||||||
docker compose -f "$COMPOSE_FILE" up postgres switchboard-old -d
|
docker compose -f "$COMPOSE_FILE" up postgres armature-old -d
|
||||||
wait_for_health "$HOST"
|
wait_for_health "$HOST"
|
||||||
|
|
||||||
# Auth
|
# Auth
|
||||||
@@ -179,17 +179,17 @@ ok "recorded $PRE_PKG_COUNT installed packages"
|
|||||||
echo -e "\n${YELLOW}═══ Phase 3: Upgrade ═══${NC}"
|
echo -e "\n${YELLOW}═══ Phase 3: Upgrade ═══${NC}"
|
||||||
|
|
||||||
echo "Stopping old version..."
|
echo "Stopping old version..."
|
||||||
docker compose -f "$COMPOSE_FILE" stop switchboard-old
|
docker compose -f "$COMPOSE_FILE" stop armature-old
|
||||||
ok "old version stopped"
|
ok "old version stopped"
|
||||||
|
|
||||||
echo "Starting new version..."
|
echo "Starting new version..."
|
||||||
docker compose -f "$COMPOSE_FILE" up switchboard-new -d
|
docker compose -f "$COMPOSE_FILE" up armature-new -d
|
||||||
wait_for_health "$HOST"
|
wait_for_health "$HOST"
|
||||||
ok "new version started"
|
ok "new version started"
|
||||||
|
|
||||||
# Check for migration errors in logs
|
# Check for migration errors in logs
|
||||||
echo -e "\n${YELLOW}Checking startup logs...${NC}"
|
echo -e "\n${YELLOW}Checking startup logs...${NC}"
|
||||||
LOGS=$(docker compose -f "$COMPOSE_FILE" logs switchboard-new 2>&1 || echo "")
|
LOGS=$(docker compose -f "$COMPOSE_FILE" logs armature-new 2>&1 || echo "")
|
||||||
if echo "$LOGS" | grep -qi "migration.*fail\|schema.*error\|panic\|fatal"; then
|
if echo "$LOGS" | grep -qi "migration.*fail\|schema.*error\|panic\|fatal"; then
|
||||||
fail "migration errors found in logs"
|
fail "migration errors found in logs"
|
||||||
echo "$LOGS" | grep -i "migration\|schema\|panic\|fatal" | head -5
|
echo "$LOGS" | grep -i "migration\|schema\|panic\|fatal" | head -5
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"realm": "switchboard",
|
"realm": "armature",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"registrationAllowed": false,
|
"registrationAllowed": false,
|
||||||
"loginWithEmailAllowed": true,
|
"loginWithEmailAllowed": true,
|
||||||
@@ -9,11 +9,11 @@
|
|||||||
"realm": [
|
"realm": [
|
||||||
{
|
{
|
||||||
"name": "sb-admin",
|
"name": "sb-admin",
|
||||||
"description": "Switchboard admin role"
|
"description": "Armature admin role"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "sb-user",
|
"name": "sb-user",
|
||||||
"description": "Switchboard regular user role"
|
"description": "Armature regular user role"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -29,11 +29,11 @@
|
|||||||
],
|
],
|
||||||
"clients": [
|
"clients": [
|
||||||
{
|
{
|
||||||
"clientId": "switchboard",
|
"clientId": "armature",
|
||||||
"name": "Switchboard Core",
|
"name": "Armature",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"publicClient": false,
|
"publicClient": false,
|
||||||
"secret": "switchboard-secret",
|
"secret": "armature-secret",
|
||||||
"redirectUris": [
|
"redirectUris": [
|
||||||
"http://localhost:3000/*",
|
"http://localhost:3000/*",
|
||||||
"http://localhost:8080/*"
|
"http://localhost:8080/*"
|
||||||
@@ -69,7 +69,7 @@
|
|||||||
"users": [
|
"users": [
|
||||||
{
|
{
|
||||||
"username": "alice",
|
"username": "alice",
|
||||||
"email": "alice@switchboard.test",
|
"email": "alice@armature.test",
|
||||||
"firstName": "Alice",
|
"firstName": "Alice",
|
||||||
"lastName": "Engineer",
|
"lastName": "Engineer",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
],
|
],
|
||||||
"realmRoles": [
|
"realmRoles": [
|
||||||
"sb-user",
|
"sb-user",
|
||||||
"default-roles-switchboard"
|
"default-roles-armature"
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
"/engineering"
|
"/engineering"
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"username": "bob",
|
"username": "bob",
|
||||||
"email": "bob@switchboard.test",
|
"email": "bob@armature.test",
|
||||||
"firstName": "Bob",
|
"firstName": "Bob",
|
||||||
"lastName": "Lead",
|
"lastName": "Lead",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
"realmRoles": [
|
"realmRoles": [
|
||||||
"sb-admin",
|
"sb-admin",
|
||||||
"sb-user",
|
"sb-user",
|
||||||
"default-roles-switchboard"
|
"default-roles-armature"
|
||||||
],
|
],
|
||||||
"groups": [
|
"groups": [
|
||||||
"/engineering",
|
"/engineering",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# docker-compose-e2e.yml — Multi-replica E2E testing (Postgres)
|
# docker-compose-e2e.yml — Multi-replica E2E testing (Postgres)
|
||||||
#
|
#
|
||||||
# Three Switchboard replicas behind an nginx load balancer,
|
# Three Armature replicas behind an nginx load balancer,
|
||||||
# sharing a single Postgres instance. Tests cross-replica
|
# sharing a single Postgres instance. Tests cross-replica
|
||||||
# broadcast via pg_notify and cluster registry (v0.6.0).
|
# broadcast via pg_notify and cluster registry (v0.6.0).
|
||||||
#
|
#
|
||||||
@@ -14,18 +14,18 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: switchboard_e2e
|
POSTGRES_DB: armature_e2e
|
||||||
POSTGRES_USER: switchboard
|
POSTGRES_USER: armature
|
||||||
POSTGRES_PASSWORD: e2e-password
|
POSTGRES_PASSWORD: e2e-password
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U switchboard -d switchboard_e2e"]
|
test: ["CMD-SHELL", "pg_isready -U armature -d armature_e2e"]
|
||||||
interval: 2s
|
interval: 2s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
switchboard-1:
|
armature-1:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -33,11 +33,11 @@ services:
|
|||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: postgres
|
DB_DRIVER: postgres
|
||||||
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable
|
DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
|
||||||
JWT_SECRET: e2e-jwt-secret
|
JWT_SECRET: e2e-jwt-secret
|
||||||
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: admin
|
ARMATURE_ADMIN_PASSWORD: admin
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: "*"
|
CORS_ALLOWED_ORIGINS: "*"
|
||||||
@@ -48,14 +48,14 @@ services:
|
|||||||
CLUSTER_NODE_ID: "node-1"
|
CLUSTER_NODE_ID: "node-1"
|
||||||
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
||||||
CLUSTER_STALE_THRESHOLD: "15s"
|
CLUSTER_STALE_THRESHOLD: "15s"
|
||||||
CLUSTER_ENDPOINT: "http://switchboard-1:8080"
|
CLUSTER_ENDPOINT: "http://armature-1:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "8081:80"
|
- "8081:80"
|
||||||
|
|
||||||
switchboard-2:
|
armature-2:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -63,11 +63,11 @@ services:
|
|||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: postgres
|
DB_DRIVER: postgres
|
||||||
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable
|
DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
|
||||||
JWT_SECRET: e2e-jwt-secret
|
JWT_SECRET: e2e-jwt-secret
|
||||||
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: admin
|
ARMATURE_ADMIN_PASSWORD: admin
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: "*"
|
CORS_ALLOWED_ORIGINS: "*"
|
||||||
@@ -78,14 +78,14 @@ services:
|
|||||||
CLUSTER_NODE_ID: "node-2"
|
CLUSTER_NODE_ID: "node-2"
|
||||||
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
||||||
CLUSTER_STALE_THRESHOLD: "15s"
|
CLUSTER_STALE_THRESHOLD: "15s"
|
||||||
CLUSTER_ENDPOINT: "http://switchboard-2:8080"
|
CLUSTER_ENDPOINT: "http://armature-2:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "8082:80"
|
- "8082:80"
|
||||||
|
|
||||||
switchboard-3:
|
armature-3:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -93,11 +93,11 @@ services:
|
|||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: postgres
|
DB_DRIVER: postgres
|
||||||
DATABASE_URL: postgres://switchboard:e2e-password@postgres:5432/switchboard_e2e?sslmode=disable
|
DATABASE_URL: postgres://armature:e2e-password@postgres:5432/armature_e2e?sslmode=disable
|
||||||
JWT_SECRET: e2e-jwt-secret
|
JWT_SECRET: e2e-jwt-secret
|
||||||
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
ENCRYPTION_KEY: e2e-encryption-key-32chars!!!!!
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: admin
|
ARMATURE_ADMIN_PASSWORD: admin
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: "*"
|
CORS_ALLOWED_ORIGINS: "*"
|
||||||
@@ -108,7 +108,7 @@ services:
|
|||||||
CLUSTER_NODE_ID: "node-3"
|
CLUSTER_NODE_ID: "node-3"
|
||||||
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
CLUSTER_HEARTBEAT_INTERVAL: "5s"
|
||||||
CLUSTER_STALE_THRESHOLD: "15s"
|
CLUSTER_STALE_THRESHOLD: "15s"
|
||||||
CLUSTER_ENDPOINT: "http://switchboard-3:8080"
|
CLUSTER_ENDPOINT: "http://armature-3:8080"
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -122,6 +122,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:80"
|
- "3000:80"
|
||||||
depends_on:
|
depends_on:
|
||||||
- switchboard-1
|
- armature-1
|
||||||
- switchboard-2
|
- armature-2
|
||||||
- switchboard-3
|
- armature-3
|
||||||
|
|||||||
@@ -6,11 +6,11 @@
|
|||||||
# docker compose -f docker-compose.yml -f docker-compose-keycloak.yml up
|
# docker compose -f docker-compose.yml -f docker-compose-keycloak.yml up
|
||||||
#
|
#
|
||||||
# This extends the base docker-compose.yml to add:
|
# This extends the base docker-compose.yml to add:
|
||||||
# - Keycloak IdP with a pre-configured "switchboard" realm
|
# - Keycloak IdP with a pre-configured "armature" realm
|
||||||
# - Switchboard configured with AUTH_MODE=oidc pointing at Keycloak
|
# - Armature configured with AUTH_MODE=oidc pointing at Keycloak
|
||||||
#
|
#
|
||||||
# After startup:
|
# After startup:
|
||||||
# Switchboard: http://localhost:3000
|
# Armature: http://localhost:3000
|
||||||
# Keycloak: http://localhost:8180 (admin / admin)
|
# Keycloak: http://localhost:8180 (admin / admin)
|
||||||
#
|
#
|
||||||
# Pre-configured test users in Keycloak:
|
# Pre-configured test users in Keycloak:
|
||||||
@@ -21,14 +21,14 @@
|
|||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# Override switchboard to use OIDC
|
# Override armature to use OIDC
|
||||||
switchboard:
|
armature:
|
||||||
environment:
|
environment:
|
||||||
AUTH_MODE: oidc
|
AUTH_MODE: oidc
|
||||||
OIDC_ISSUER_URL: http://keycloak:8080/realms/switchboard
|
OIDC_ISSUER_URL: http://keycloak:8080/realms/armature
|
||||||
OIDC_EXTERNAL_ISSUER_URL: http://localhost:8180/realms/switchboard
|
OIDC_EXTERNAL_ISSUER_URL: http://localhost:8180/realms/armature
|
||||||
OIDC_CLIENT_ID: switchboard
|
OIDC_CLIENT_ID: armature
|
||||||
OIDC_CLIENT_SECRET: switchboard-secret
|
OIDC_CLIENT_SECRET: armature-secret
|
||||||
OIDC_REDIRECT_URL: http://localhost:3000/api/v1/auth/oidc/callback
|
OIDC_REDIRECT_URL: http://localhost:3000/api/v1/auth/oidc/callback
|
||||||
OIDC_AUTO_ACTIVATE: "true"
|
OIDC_AUTO_ACTIVATE: "true"
|
||||||
OIDC_ADMIN_ROLE: sb-admin
|
OIDC_ADMIN_ROLE: sb-admin
|
||||||
@@ -38,7 +38,7 @@ services:
|
|||||||
|
|
||||||
keycloak:
|
keycloak:
|
||||||
image: quay.io/keycloak/keycloak:26.0
|
image: quay.io/keycloak/keycloak:26.0
|
||||||
container_name: switchboard-keycloak
|
container_name: armature-keycloak
|
||||||
command:
|
command:
|
||||||
- start-dev
|
- start-dev
|
||||||
- --import-realm
|
- --import-realm
|
||||||
@@ -48,7 +48,7 @@ services:
|
|||||||
KC_HTTP_PORT: "8080"
|
KC_HTTP_PORT: "8080"
|
||||||
KC_HEALTH_ENABLED: "true"
|
KC_HEALTH_ENABLED: "true"
|
||||||
volumes:
|
volumes:
|
||||||
- ./ci/keycloak-realm.json:/opt/keycloak/data/import/switchboard-realm.json:ro
|
- ./ci/keycloak-realm.json:/opt/keycloak/data/import/armature-realm.json:ro
|
||||||
ports:
|
ports:
|
||||||
- "8180:8080"
|
- "8180:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
|||||||
@@ -7,11 +7,11 @@
|
|||||||
# ci/e2e-upgrade-test.sh (orchestrates build → seed → upgrade → verify)
|
# ci/e2e-upgrade-test.sh (orchestrates build → seed → upgrade → verify)
|
||||||
#
|
#
|
||||||
# Manual usage:
|
# Manual usage:
|
||||||
# docker build -t switchboard-core:v-old .
|
# docker build -t armature:v-old .
|
||||||
# docker compose -f docker-compose-upgrade.yml up postgres switchboard-old -d
|
# docker compose -f docker-compose-upgrade.yml up postgres armature-old -d
|
||||||
# # ... seed data ...
|
# # ... seed data ...
|
||||||
# docker compose -f docker-compose-upgrade.yml stop switchboard-old
|
# docker compose -f docker-compose-upgrade.yml stop armature-old
|
||||||
# docker compose -f docker-compose-upgrade.yml up switchboard-new -d
|
# docker compose -f docker-compose-upgrade.yml up armature-new -d
|
||||||
# # ... verify data ...
|
# # ... verify data ...
|
||||||
# docker compose -f docker-compose-upgrade.yml down -v
|
# docker compose -f docker-compose-upgrade.yml down -v
|
||||||
|
|
||||||
@@ -19,28 +19,28 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: switchboard_upgrade
|
POSTGRES_DB: armature_upgrade
|
||||||
POSTGRES_USER: switchboard
|
POSTGRES_USER: armature
|
||||||
POSTGRES_PASSWORD: upgrade-password
|
POSTGRES_PASSWORD: upgrade-password
|
||||||
ports:
|
ports:
|
||||||
- "5433:5432"
|
- "5433:5432"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U switchboard -d switchboard_upgrade"]
|
test: ["CMD-SHELL", "pg_isready -U armature -d armature_upgrade"]
|
||||||
interval: 2s
|
interval: 2s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|
||||||
switchboard-old:
|
armature-old:
|
||||||
image: switchboard-core:v-old
|
image: armature:v-old
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: postgres
|
DB_DRIVER: postgres
|
||||||
DATABASE_URL: postgres://switchboard:upgrade-password@postgres:5432/switchboard_upgrade?sslmode=disable
|
DATABASE_URL: postgres://armature:upgrade-password@postgres:5432/armature_upgrade?sslmode=disable
|
||||||
JWT_SECRET: upgrade-jwt-secret
|
JWT_SECRET: upgrade-jwt-secret
|
||||||
ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
|
ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: admin
|
ARMATURE_ADMIN_PASSWORD: admin
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: "*"
|
CORS_ALLOWED_ORIGINS: "*"
|
||||||
@@ -56,17 +56,17 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- upgrade_storage:/data/storage
|
- upgrade_storage:/data/storage
|
||||||
|
|
||||||
switchboard-new:
|
armature-new:
|
||||||
image: core-switchboard-new
|
image: core-armature-new
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: postgres
|
DB_DRIVER: postgres
|
||||||
DATABASE_URL: postgres://switchboard:upgrade-password@postgres:5432/switchboard_upgrade?sslmode=disable
|
DATABASE_URL: postgres://armature:upgrade-password@postgres:5432/armature_upgrade?sslmode=disable
|
||||||
JWT_SECRET: upgrade-jwt-secret
|
JWT_SECRET: upgrade-jwt-secret
|
||||||
ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
|
ENCRYPTION_KEY: upgrade-encryption-key-32chars!!
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: admin
|
ARMATURE_ADMIN_PASSWORD: admin
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: "*"
|
CORS_ALLOWED_ORIGINS: "*"
|
||||||
|
|||||||
@@ -13,20 +13,20 @@
|
|||||||
# For Postgres / multi-replica / k8s deployment see k8s/ and Dockerfile.
|
# For Postgres / multi-replica / k8s deployment see k8s/ and Dockerfile.
|
||||||
|
|
||||||
services:
|
services:
|
||||||
switchboard:
|
armature:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: switchboard-core
|
container_name: armature
|
||||||
environment:
|
environment:
|
||||||
PORT: "8080"
|
PORT: "8080"
|
||||||
BASE_PATH: ""
|
BASE_PATH: ""
|
||||||
DB_DRIVER: sqlite
|
DB_DRIVER: sqlite
|
||||||
DATABASE_URL: /data/switchboard.db
|
DATABASE_URL: /data/armature.db
|
||||||
JWT_SECRET: ${JWT_SECRET:-change-me-for-production}
|
JWT_SECRET: ${JWT_SECRET:-change-me-for-production}
|
||||||
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-for-production}
|
ENCRYPTION_KEY: ${ENCRYPTION_KEY:-change-me-for-production}
|
||||||
SWITCHBOARD_ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
ARMATURE_ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
|
ARMATURE_ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000}
|
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-http://localhost:3000}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Backend Launcher
|
# Armature - Backend Launcher
|
||||||
# ============================================
|
# ============================================
|
||||||
# Runs as an nginx entrypoint.d hook.
|
# Runs as an nginx entrypoint.d hook.
|
||||||
# Starts the Go backend in the background
|
# Starts the Go backend in the background
|
||||||
@@ -20,15 +20,15 @@ else
|
|||||||
-exec sed -i "s|%%BASE_PATH%%||g" {} +
|
-exec sed -i "s|%%BASE_PATH%%||g" {} +
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "🔀 Starting Switchboard Core backend..."
|
echo "🔀 Starting Armature backend..."
|
||||||
|
|
||||||
# Kill any stale backend from a previous entrypoint run
|
# Kill any stale backend from a previous entrypoint run
|
||||||
pkill -f /usr/local/bin/switchboard 2>/dev/null || true
|
pkill -f /usr/local/bin/armature 2>/dev/null || true
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
|
|
||||||
# Launch Go backend in background (from /app so migrations are found)
|
# Launch Go backend in background (from /app so migrations are found)
|
||||||
cd /app
|
cd /app
|
||||||
/usr/local/bin/switchboard &
|
/usr/local/bin/armature &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
|
|
||||||
# Wait for backend to be ready (max 60s)
|
# Wait for backend to be ready (max 60s)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Switchboard Core — Architecture
|
# Armature — Architecture
|
||||||
|
|
||||||
Switchboard Core is a self-hosted extension platform. It provides identity,
|
Armature is a self-hosted extension platform. It provides identity,
|
||||||
teams, permissions, storage, workflows, notifications, and a package system.
|
teams, permissions, storage, workflows, notifications, and a package system.
|
||||||
Everything else — chat, AI providers, personas, knowledge bases, notes,
|
Everything else — chat, AI providers, personas, knowledge bases, notes,
|
||||||
tools — ships as installable extensions.
|
tools — ships as installable extensions.
|
||||||
@@ -284,7 +284,7 @@ Single Docker image: Go binary + migrations + frontend assets + vendor
|
|||||||
libs. Kubernetes deployment with 3-node PG cluster. CI via Gitea Actions
|
libs. Kubernetes deployment with 3-node PG cluster. CI via Gitea Actions
|
||||||
with DaemonSet DinD runners testing both PG and SQLite pipelines.
|
with DaemonSet DinD runners testing both PG and SQLite pipelines.
|
||||||
|
|
||||||
Registry: `registry.gobha.me:5000/xcaliber/switchboard-core`
|
Registry: `registry.gobha.me:5000/xcaliber/armature`
|
||||||
Namespace: `gobha-ai-chat`
|
Namespace: `gobha-ai-chat`
|
||||||
|
|
||||||
### Cluster Topology
|
### Cluster Topology
|
||||||
|
|||||||
@@ -280,7 +280,7 @@ def on_fire(ctx):
|
|||||||
|
|
||||||
resp = http.post(url, body=payload, headers={
|
resp = http.post(url, body=payload, headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Switchboard-Event": data.get("event_name", "workflow.notify")
|
"X-Armature-Event": data.get("event_name", "workflow.notify")
|
||||||
})
|
})
|
||||||
|
|
||||||
# Log the delivery
|
# Log the delivery
|
||||||
|
|||||||
@@ -3,14 +3,14 @@
|
|||||||
## Docker Single-Instance
|
## Docker Single-Instance
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/switchboard-core/switchboard-core:latest
|
docker pull ghcr.io/armature/armature:latest
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=changeme \
|
-e ARMATURE_ADMIN_PASSWORD=changeme \
|
||||||
-e JWT_SECRET="$(openssl rand -hex 32)" \
|
-e JWT_SECRET="$(openssl rand -hex 32)" \
|
||||||
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
|
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
|
||||||
-v switchboard-data:/data \
|
-v armature-data:/data \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
This runs with SQLite and PVC storage. Suitable for evaluation and small teams.
|
This runs with SQLite and PVC storage. Suitable for evaluation and small teams.
|
||||||
@@ -22,22 +22,22 @@ services:
|
|||||||
postgres:
|
postgres:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: switchboard
|
POSTGRES_DB: armature
|
||||||
POSTGRES_USER: switchboard
|
POSTGRES_USER: armature
|
||||||
POSTGRES_PASSWORD: secretpassword
|
POSTGRES_PASSWORD: secretpassword
|
||||||
volumes:
|
volumes:
|
||||||
- pg_data:/var/lib/postgresql/data
|
- pg_data:/var/lib/postgresql/data
|
||||||
|
|
||||||
switchboard:
|
armature:
|
||||||
image: ghcr.io/switchboard-core/switchboard-core:latest
|
image: ghcr.io/armature/armature:latest
|
||||||
ports:
|
ports:
|
||||||
- "8080:80"
|
- "8080:80"
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "postgres://switchboard:secretpassword@postgres:5432/switchboard?sslmode=disable"
|
DATABASE_URL: "postgres://armature:secretpassword@postgres:5432/armature?sslmode=disable"
|
||||||
JWT_SECRET: "change-me-in-production"
|
JWT_SECRET: "change-me-in-production"
|
||||||
ENCRYPTION_KEY: "change-me-in-production"
|
ENCRYPTION_KEY: "change-me-in-production"
|
||||||
SWITCHBOARD_ADMIN_USERNAME: admin
|
ARMATURE_ADMIN_USERNAME: admin
|
||||||
SWITCHBOARD_ADMIN_PASSWORD: changeme
|
ARMATURE_ADMIN_PASSWORD: changeme
|
||||||
STORAGE_BACKEND: pvc
|
STORAGE_BACKEND: pvc
|
||||||
STORAGE_PATH: /data/storage
|
STORAGE_PATH: /data/storage
|
||||||
volumes:
|
volumes:
|
||||||
@@ -58,7 +58,7 @@ See the `k8s/` directory for example manifests. Key considerations:
|
|||||||
- Liveness probe: `/healthz/live`. Readiness probe: `/healthz/ready`.
|
- Liveness probe: `/healthz/live`. Readiness probe: `/healthz/ready`.
|
||||||
- Mount a PVC at `/data/storage` or configure S3.
|
- Mount a PVC at `/data/storage` or configure S3.
|
||||||
- Store `JWT_SECRET` and `ENCRYPTION_KEY` in Kubernetes Secrets.
|
- Store `JWT_SECRET` and `ENCRYPTION_KEY` in Kubernetes Secrets.
|
||||||
- Registry: `registry.gobha.me:5000/xcaliber/switchboard-core`.
|
- Registry: `registry.gobha.me:5000/xcaliber/armature`.
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -72,15 +72,15 @@ See the `k8s/` directory for example manifests. Key considerations:
|
|||||||
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, `oidc` |
|
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, `oidc` |
|
||||||
| `STORAGE_BACKEND` | auto | `pvc` or `s3` |
|
| `STORAGE_BACKEND` | auto | `pvc` or `s3` |
|
||||||
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
||||||
| `BASE_PATH` | | URL prefix (e.g., `/switchboard`) |
|
| `BASE_PATH` | | URL prefix (e.g., `/armature`) |
|
||||||
| `LOG_FORMAT` | `text` | `text` or `json` |
|
| `LOG_FORMAT` | `text` | `text` or `json` |
|
||||||
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
||||||
| `CORS_ALLOWED_ORIGINS` | | Comma-separated allowed origins |
|
| `CORS_ALLOWED_ORIGINS` | | Comma-separated allowed origins |
|
||||||
| `BUNDLED_PACKAGES` | (empty) | `""` defaults, `"*"` all, or comma-separated |
|
| `BUNDLED_PACKAGES` | (empty) | `""` defaults, `"*"` all, or comma-separated |
|
||||||
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable bundled package install |
|
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable bundled package install |
|
||||||
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Custom bundle directory |
|
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Custom bundle directory |
|
||||||
| `SWITCHBOARD_ADMIN_USERNAME` | | Bootstrap admin username |
|
| `ARMATURE_ADMIN_USERNAME` | | Bootstrap admin username |
|
||||||
| `SWITCHBOARD_ADMIN_PASSWORD` | | Bootstrap admin password |
|
| `ARMATURE_ADMIN_PASSWORD` | | Bootstrap admin password |
|
||||||
| `SEED_USERS` | | Dev seed users (ignored in production) |
|
| `SEED_USERS` | | Dev seed users (ignored in production) |
|
||||||
|
|
||||||
### S3 Storage Variables
|
### S3 Storage Variables
|
||||||
@@ -107,14 +107,14 @@ See the `k8s/` directory for example manifests. Key considerations:
|
|||||||
**PostgreSQL** (recommended for production):
|
**PostgreSQL** (recommended for production):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DATABASE_URL="postgres://user:pass@host:5432/switchboard?sslmode=require"
|
DATABASE_URL="postgres://user:pass@host:5432/armature?sslmode=require"
|
||||||
```
|
```
|
||||||
|
|
||||||
**SQLite** (dev, test, edge deployments):
|
**SQLite** (dev, test, edge deployments):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DB_DRIVER=sqlite
|
DB_DRIVER=sqlite
|
||||||
DATABASE_URL=/data/switchboard.db
|
DATABASE_URL=/data/armature.db
|
||||||
```
|
```
|
||||||
|
|
||||||
Both databases are first-class -- every query compiles and passes tests on both. The driver is auto-detected from `DATABASE_URL` if `DB_DRIVER` is not set.
|
Both databases are first-class -- every query compiles and passes tests on both. The driver is auto-detected from `DATABASE_URL` if `DB_DRIVER` is not set.
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
Switchboard-core is tightly coupled to PostgreSQL. Scaling horizontally requires instance coordination: peer discovery, health monitoring, ephemeral event routing, and (optionally) leader election. Traditional HA solutions (Raft, etcd, Consul) introduce a second consensus layer on top of PG — doubling operational complexity for a system that already has serializable transactions and LISTEN/NOTIFY.
|
Armature-core is tightly coupled to PostgreSQL. Scaling horizontally requires instance coordination: peer discovery, health monitoring, ephemeral event routing, and (optionally) leader election. Traditional HA solutions (Raft, etcd, Consul) introduce a second consensus layer on top of PG — doubling operational complexity for a system that already has serializable transactions and LISTEN/NOTIFY.
|
||||||
|
|
||||||
## Principle
|
## Principle
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ Pre-MVP: fold `CREATE UNLOGGED TABLE` into existing schema initialization. No ne
|
|||||||
|
|
||||||
### Single-Node Behavior
|
### Single-Node Behavior
|
||||||
|
|
||||||
When only one node is registered, the system behaves identically to pre-cluster switchboard-core. The registry has one row. LISTEN/NOTIFY delivers events back to the same instance. No special-casing required.
|
When only one node is registered, the system behaves identically to pre-cluster armature. The registry has one row. LISTEN/NOTIFY delivers events back to the same instance. No special-casing required.
|
||||||
|
|
||||||
### Health Endpoint Integration
|
### Health Endpoint Integration
|
||||||
|
|
||||||
|
|||||||
@@ -172,17 +172,17 @@ their own `node-N.key`.
|
|||||||
|
|
||||||
### Cert Provisioning Tooling
|
### Cert Provisioning Tooling
|
||||||
|
|
||||||
A shell script (`scripts/switchboard-ca.sh`) wrapping `openssl` is the
|
A shell script (`scripts/armature-ca.sh`) wrapping `openssl` is the
|
||||||
KISS path. No new binary, no new dependency. Three commands:
|
KISS path. No new binary, no new dependency. Three commands:
|
||||||
|
|
||||||
```
|
```
|
||||||
switchboard-ca init
|
armature-ca init
|
||||||
→ generates cluster-ca.crt + cluster-ca.key in ./ca/
|
→ generates cluster-ca.crt + cluster-ca.key in ./ca/
|
||||||
|
|
||||||
switchboard-ca issue-node --name node-1 --san "node-1.internal,10.0.0.1"
|
armature-ca issue-node --name node-1 --san "node-1.internal,10.0.0.1"
|
||||||
→ generates node-1.crt + node-1.key in ./nodes/
|
→ generates node-1.crt + node-1.key in ./nodes/
|
||||||
|
|
||||||
switchboard-ca issue-user --cn jeff [--email jeff@example.com]
|
armature-ca issue-user --cn jeff [--email jeff@example.com]
|
||||||
→ generates jeff.crt + jeff.key in ./users/
|
→ generates jeff.crt + jeff.key in ./users/
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ The new code is:
|
|||||||
| `server/auth/mtls_helpers.go` | Shared: `ParseDN`, `FingerprintCert`, `resolveOrProvision` |
|
| `server/auth/mtls_helpers.go` | Shared: `ParseDN`, `FingerprintCert`, `resolveOrProvision` |
|
||||||
| `server/auth/mtls_native_test.go` | Unit + integration tests |
|
| `server/auth/mtls_native_test.go` | Unit + integration tests |
|
||||||
| `server/config/tls.go` | `TLSConfig` struct, loader, validation |
|
| `server/config/tls.go` | `TLSConfig` struct, loader, validation |
|
||||||
| `scripts/switchboard-ca.sh` | Cert provisioning wrapper |
|
| `scripts/armature-ca.sh` | Cert provisioning wrapper |
|
||||||
|
|
||||||
`server/auth/mtls.go` (existing) is renamed to `mtls_proxy.go` for
|
`server/auth/mtls.go` (existing) is renamed to `mtls_proxy.go` for
|
||||||
clarity. No behavioral changes to the proxy provider.
|
clarity. No behavioral changes to the proxy provider.
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/switchboard-core/switchboard-core:latest
|
docker pull ghcr.io/armature/armature:latest
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e SWITCHBOARD_ADMIN_USERNAME=admin \
|
-e ARMATURE_ADMIN_USERNAME=admin \
|
||||||
-e SWITCHBOARD_ADMIN_PASSWORD=changeme \
|
-e ARMATURE_ADMIN_PASSWORD=changeme \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
On first run, bundled packages are automatically installed — workflows, surfaces, and extensions are ready to use immediately.
|
On first run, bundled packages are automatically installed — workflows, surfaces, and extensions are ready to use immediately.
|
||||||
@@ -66,12 +66,12 @@ Set `BUNDLED_PACKAGES` to control which packages are installed:
|
|||||||
# Install ALL packages (everything in the image)
|
# Install ALL packages (everything in the image)
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e BUNDLED_PACKAGES="*" \
|
-e BUNDLED_PACKAGES="*" \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
|
|
||||||
# Install specific packages only
|
# Install specific packages only
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e BUNDLED_PACKAGES="notes,tasks,schedules" \
|
-e BUNDLED_PACKAGES="notes,tasks,schedules" \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
Empty (default) installs the curated default set. Use `*` to install all packages. This is useful for Helm charts where different environments need different packages.
|
Empty (default) installs the curated default set. Use `*` to install all packages. This is useful for Helm charts where different environments need different packages.
|
||||||
@@ -83,7 +83,7 @@ Set `SKIP_BUNDLED_PACKAGES=true` to prevent bundled packages from being installe
|
|||||||
```bash
|
```bash
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e SKIP_BUNDLED_PACKAGES=true \
|
-e SKIP_BUNDLED_PACKAGES=true \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Custom Bundle Directory
|
### Custom Bundle Directory
|
||||||
@@ -94,7 +94,7 @@ Override the default bundled packages location with `BUNDLED_PACKAGES_DIR`:
|
|||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e BUNDLED_PACKAGES_DIR=/custom/packages \
|
-e BUNDLED_PACKAGES_DIR=/custom/packages \
|
||||||
-v /host/packages:/custom/packages \
|
-v /host/packages:/custom/packages \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
## Builder Image
|
## Builder Image
|
||||||
@@ -102,7 +102,7 @@ docker run -p 8080:80 \
|
|||||||
The builder image pre-caches Go modules and Node dependencies for faster custom builds.
|
The builder image pre-caches Go modules and Node dependencies for faster custom builds.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker pull ghcr.io/switchboard-core/builder:latest
|
docker pull ghcr.io/armature/builder:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### What It Caches
|
### What It Caches
|
||||||
@@ -117,16 +117,16 @@ docker pull ghcr.io/switchboard-core/builder:latest
|
|||||||
Reference the builder image as a base stage in your Dockerfile:
|
Reference the builder image as a base stage in your Dockerfile:
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM ghcr.io/switchboard-core/builder:latest AS builder
|
FROM ghcr.io/armature/builder:latest AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY server/ .
|
COPY server/ .
|
||||||
RUN go build -ldflags="-s -w" -o /bin/switchboard .
|
RUN go build -ldflags="-s -w" -o /bin/armature .
|
||||||
```
|
```
|
||||||
|
|
||||||
### Building Locally
|
### Building Locally
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -f Dockerfile.builder -t switchboard-builder .
|
docker build -f Dockerfile.builder -t armature-builder .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Custom Build Guide
|
## Custom Build Guide
|
||||||
@@ -135,7 +135,7 @@ docker build -f Dockerfile.builder -t switchboard-builder .
|
|||||||
|
|
||||||
1. Create your package in `packages/your-package/` with a `manifest.json`
|
1. Create your package in `packages/your-package/` with a `manifest.json`
|
||||||
2. Build all packages: `cd packages && bash build.sh all`
|
2. Build all packages: `cd packages && bash build.sh all`
|
||||||
3. Build the Docker image: `docker build -t my-switchboard .`
|
3. Build the Docker image: `docker build -t my-armature .`
|
||||||
|
|
||||||
The Dockerfile automatically builds all packages in the `packages/` directory and bundles them into the production image.
|
The Dockerfile automatically builds all packages in the `packages/` directory and bundles them into the production image.
|
||||||
|
|
||||||
@@ -148,14 +148,14 @@ To exclude specific packages from the bundle, either:
|
|||||||
### Forking for Custom Builds
|
### Forking for Custom Builds
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/switchboard-core/switchboard-core.git
|
git clone https://github.com/armature/armature.git
|
||||||
cd switchboard-core
|
cd armature
|
||||||
|
|
||||||
# Add/modify packages
|
# Add/modify packages
|
||||||
cp -r my-extension packages/my-extension/
|
cp -r my-extension packages/my-extension/
|
||||||
|
|
||||||
# Build with builder image for faster compilation
|
# Build with builder image for faster compilation
|
||||||
docker build -t my-switchboard .
|
docker build -t my-armature .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Production Deployment
|
## Production Deployment
|
||||||
@@ -172,7 +172,7 @@ docker build -t my-switchboard .
|
|||||||
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
|
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
|
||||||
| `STORAGE_BACKEND` | (auto) | `pvc` or `s3` |
|
| `STORAGE_BACKEND` | (auto) | `pvc` or `s3` |
|
||||||
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
||||||
| `BASE_PATH` | | URL prefix (e.g. `/switchboard`) |
|
| `BASE_PATH` | | URL prefix (e.g. `/armature`) |
|
||||||
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install of bundled packages |
|
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install of bundled packages |
|
||||||
| `BUNDLED_PACKAGES` | (empty = defaults) | `""` curated defaults, `"*"` all, or comma-separated IDs |
|
| `BUNDLED_PACKAGES` | (empty = defaults) | `""` curated defaults, `"*"` all, or comma-separated IDs |
|
||||||
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Override bundled packages location |
|
| `BUNDLED_PACKAGES_DIR` | `/app/bundled-packages` | Override bundled packages location |
|
||||||
@@ -186,16 +186,16 @@ PostgreSQL is recommended for production. SQLite is suitable for single-instance
|
|||||||
```bash
|
```bash
|
||||||
# PostgreSQL (recommended)
|
# PostgreSQL (recommended)
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e DATABASE_URL="postgres://user:pass@host:5432/switchboard?sslmode=require" \
|
-e DATABASE_URL="postgres://user:pass@host:5432/armature?sslmode=require" \
|
||||||
-e JWT_SECRET="$(openssl rand -hex 32)" \
|
-e JWT_SECRET="$(openssl rand -hex 32)" \
|
||||||
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
|
-e ENCRYPTION_KEY="$(openssl rand -hex 32)" \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
|
|
||||||
# SQLite (evaluation only)
|
# SQLite (evaluation only)
|
||||||
docker run -p 8080:80 \
|
docker run -p 8080:80 \
|
||||||
-e DB_DRIVER=sqlite \
|
-e DB_DRIVER=sqlite \
|
||||||
-v switchboard-data:/data \
|
-v armature-data:/data \
|
||||||
ghcr.io/switchboard-core/switchboard-core:latest
|
ghcr.io/armature/armature:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Storage
|
### Storage
|
||||||
@@ -204,12 +204,12 @@ Object storage is required for file uploads and package asset extraction.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# PVC (auto-detected if path is writable)
|
# PVC (auto-detected if path is writable)
|
||||||
docker run -v switchboard-storage:/data/storage ...
|
docker run -v armature-storage:/data/storage ...
|
||||||
|
|
||||||
# S3-compatible (MinIO, AWS S3, Ceph)
|
# S3-compatible (MinIO, AWS S3, Ceph)
|
||||||
docker run \
|
docker run \
|
||||||
-e STORAGE_BACKEND=s3 \
|
-e STORAGE_BACKEND=s3 \
|
||||||
-e S3_BUCKET=switchboard \
|
-e S3_BUCKET=armature \
|
||||||
-e S3_ENDPOINT=https://minio.corp:9000 \
|
-e S3_ENDPOINT=https://minio.corp:9000 \
|
||||||
-e S3_ACCESS_KEY=... \
|
-e S3_ACCESS_KEY=... \
|
||||||
-e S3_SECRET_KEY=... \
|
-e S3_SECRET_KEY=... \
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ docker compose down -v
|
|||||||
|
|
||||||
## First Boot
|
## First Boot
|
||||||
|
|
||||||
On first start, Switchboard Core will:
|
On first start, Armature will:
|
||||||
|
|
||||||
1. Run database migrations (SQLite by default in compose).
|
1. Run database migrations (SQLite by default in compose).
|
||||||
2. Create the admin user from `SWITCHBOARD_ADMIN_USERNAME` / `SWITCHBOARD_ADMIN_PASSWORD` env vars.
|
2. Create the admin user from `ARMATURE_ADMIN_USERNAME` / `ARMATURE_ADMIN_PASSWORD` env vars.
|
||||||
3. Auto-install the curated default package set (notes, chat-core, dashboard, workflow demos, etc.).
|
3. Auto-install the curated default package set (notes, chat-core, dashboard, workflow demos, etc.).
|
||||||
|
|
||||||
No manual setup steps are required.
|
No manual setup steps are required.
|
||||||
@@ -67,8 +67,8 @@ You can also upload `.pkg` archives through the Admin > Packages page.
|
|||||||
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
|
| `AUTH_MODE` | `builtin` | `builtin`, `mtls`, or `oidc` |
|
||||||
| `STORAGE_BACKEND` | auto | `pvc` or `s3` |
|
| `STORAGE_BACKEND` | auto | `pvc` or `s3` |
|
||||||
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
| `STORAGE_PATH` | `/data/storage` | PVC mount point |
|
||||||
| `SWITCHBOARD_ADMIN_USERNAME` | | Bootstrap admin username |
|
| `ARMATURE_ADMIN_USERNAME` | | Bootstrap admin username |
|
||||||
| `SWITCHBOARD_ADMIN_PASSWORD` | | Bootstrap admin password |
|
| `ARMATURE_ADMIN_PASSWORD` | | Bootstrap admin password |
|
||||||
| `BUNDLED_PACKAGES` | (empty) | `""` = curated defaults, `"*"` = all, or comma-separated IDs |
|
| `BUNDLED_PACKAGES` | (empty) | `""` = curated defaults, `"*"` = all, or comma-separated IDs |
|
||||||
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install entirely |
|
| `SKIP_BUNDLED_PACKAGES` | `false` | Disable auto-install entirely |
|
||||||
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
| `LOG_LEVEL` | `info` | `debug`, `info`, `warn`, `error` |
|
||||||
@@ -77,7 +77,7 @@ You can also upload `.pkg` archives through the Admin > Packages page.
|
|||||||
## From Source
|
## From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo-url> && cd switchboard-core
|
git clone <repo-url> && cd armature
|
||||||
cp server/.env.example server/.env # edit DB credentials
|
cp server/.env.example server/.env # edit DB credentials
|
||||||
cd server && go run .
|
cd server && go run .
|
||||||
# Backend on http://localhost:8080
|
# Backend on http://localhost:8080
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Package Format
|
# Package Format
|
||||||
|
|
||||||
Switchboard packages are distributed as `.pkg` files -- ZIP archives with a standard internal structure.
|
Armature packages are distributed as `.pkg` files -- ZIP archives with a standard internal structure.
|
||||||
|
|
||||||
## ZIP Structure
|
## ZIP Structure
|
||||||
|
|
||||||
@@ -125,6 +125,6 @@ To bundle custom packages into a Docker image:
|
|||||||
|
|
||||||
1. Place your package in `packages/your-package/` with a `manifest.json`.
|
1. Place your package in `packages/your-package/` with a `manifest.json`.
|
||||||
2. Run `cd packages && bash build.sh all`.
|
2. Run `cd packages && bash build.sh all`.
|
||||||
3. Build the image: `docker build -t my-switchboard .`
|
3. Build the image: `docker build -t my-armature .`
|
||||||
|
|
||||||
The Dockerfile builds all packages and copies them into the bundled packages directory.
|
The Dockerfile builds all packages and copies them into the bundled packages directory.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ The registry is a static JSON file matching the `RegistryResponse` struct:
|
|||||||
"title": "Notes",
|
"title": "Notes",
|
||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"description": "Markdown notes with backlinks and graph view",
|
"description": "Markdown notes with backlinks and graph view",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "core",
|
"tier": "core",
|
||||||
"download_url": "https://cdn.example.com/pkg/notes.pkg",
|
"download_url": "https://cdn.example.com/pkg/notes.pkg",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
# Build Your First Browser Extension
|
# Build Your First Browser Extension
|
||||||
|
|
||||||
This tutorial walks through building a browser extension that renders custom
|
This tutorial walks through building a browser extension that renders custom
|
||||||
code blocks, modeled on the CSV Table Viewer that ships with Switchboard.
|
code blocks, modeled on the CSV Table Viewer that ships with Armature.
|
||||||
|
|
||||||
**Prerequisites:** A running Switchboard instance, a text editor, and `zip`.
|
**Prerequisites:** A running Armature instance, a text editor, and `zip`.
|
||||||
|
|
||||||
## Step 1: Create the Directory
|
## Step 1: Create the Directory
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
# k8s/switchboard.yaml
|
# k8s/armature.yaml
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Deployment
|
# Armature Core - Deployment
|
||||||
# ============================================
|
# ============================================
|
||||||
# Secrets:
|
# Secrets:
|
||||||
# switchboard-db-credentials - POSTGRES_USER, POSTGRES_PASSWORD
|
# armature-db-credentials - POSTGRES_USER, POSTGRES_PASSWORD
|
||||||
# switchboard-admin - username, password, email (optional)
|
# armature-admin - username, password, email (optional)
|
||||||
# switchboard-encryption - ENCRYPTION_KEY (required for v0.9.4+)
|
# armature-encryption - ENCRYPTION_KEY (required for v0.9.4+)
|
||||||
#
|
#
|
||||||
# PVC:
|
# PVC:
|
||||||
# switchboard-storage - Extraction scratch (always needed)
|
# armature-storage - Extraction scratch (always needed)
|
||||||
# Requires ReadWriteMany (RWX) — see k8s/storage-pvc.yaml
|
# Requires ReadWriteMany (RWX) — see k8s/storage-pvc.yaml
|
||||||
# With S3 backend, PVC is small (extraction status only, not blobs)
|
# With S3 backend, PVC is small (extraction status only, not blobs)
|
||||||
#
|
#
|
||||||
@@ -23,32 +23,32 @@
|
|||||||
# S3_ACCESS_KEY - S3 access key
|
# S3_ACCESS_KEY - S3 access key
|
||||||
# S3_SECRET_KEY - S3 secret key
|
# S3_SECRET_KEY - S3 secret key
|
||||||
#
|
#
|
||||||
# Admin bootstrap: on every pod start, if SWITCHBOARD_ADMIN_* env vars
|
# Admin bootstrap: on every pod start, if ARMATURE_ADMIN_* env vars
|
||||||
# are set, the backend upserts an admin user. Change the secret and
|
# are set, the backend upserts an admin user. Change the secret and
|
||||||
# restart the pod to reset the admin password.
|
# restart the pod to reset the admin password.
|
||||||
# ============================================
|
# ============================================
|
||||||
apiVersion: apps/v1
|
apiVersion: apps/v1
|
||||||
kind: Deployment
|
kind: Deployment
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard${DEPLOY_SUFFIX}
|
name: armature${DEPLOY_SUFFIX}
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
replicas: ${REPLICAS}
|
replicas: ${REPLICAS}
|
||||||
selector:
|
selector:
|
||||||
matchLabels:
|
matchLabels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
containers:
|
containers:
|
||||||
- name: switchboard
|
- name: armature
|
||||||
image: ${IMAGE}:${IMAGE_TAG}
|
image: ${IMAGE}:${IMAGE_TAG}
|
||||||
imagePullPolicy: Always
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
@@ -72,43 +72,43 @@ spec:
|
|||||||
- name: POSTGRES_USER
|
- name: POSTGRES_USER
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-db-credentials
|
name: armature-db-credentials
|
||||||
key: POSTGRES_USER
|
key: POSTGRES_USER
|
||||||
- name: POSTGRES_PASSWORD
|
- name: POSTGRES_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-db-credentials
|
name: armature-db-credentials
|
||||||
key: POSTGRES_PASSWORD
|
key: POSTGRES_PASSWORD
|
||||||
- name: DATABASE_URL
|
- name: DATABASE_URL
|
||||||
value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable"
|
value: "postgres://$(POSTGRES_USER):$(POSTGRES_PASSWORD)@$(POSTGRES_HOST):$(POSTGRES_PORT)/$(POSTGRES_DB)?sslmode=disable"
|
||||||
- name: SWITCHBOARD_ADMIN_USERNAME
|
- name: ARMATURE_ADMIN_USERNAME
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-admin
|
name: armature-admin
|
||||||
key: username
|
key: username
|
||||||
optional: true
|
optional: true
|
||||||
- name: SWITCHBOARD_ADMIN_PASSWORD
|
- name: ARMATURE_ADMIN_PASSWORD
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-admin
|
name: armature-admin
|
||||||
key: password
|
key: password
|
||||||
optional: true
|
optional: true
|
||||||
- name: SWITCHBOARD_ADMIN_EMAIL
|
- name: ARMATURE_ADMIN_EMAIL
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-admin
|
name: armature-admin
|
||||||
key: email
|
key: email
|
||||||
optional: true
|
optional: true
|
||||||
- name: SEED_USERS
|
- name: SEED_USERS
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-seed-users
|
name: armature-seed-users
|
||||||
key: users
|
key: users
|
||||||
optional: true
|
optional: true
|
||||||
- name: ENCRYPTION_KEY
|
- name: ENCRYPTION_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-encryption
|
name: armature-encryption
|
||||||
key: ENCRYPTION_KEY
|
key: ENCRYPTION_KEY
|
||||||
optional: true
|
optional: true
|
||||||
# File storage (v0.12.0+)
|
# File storage (v0.12.0+)
|
||||||
@@ -120,37 +120,37 @@ spec:
|
|||||||
- name: S3_ENDPOINT
|
- name: S3_ENDPOINT
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_ENDPOINT
|
key: S3_ENDPOINT
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_BUCKET
|
- name: S3_BUCKET
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_BUCKET
|
key: S3_BUCKET
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_ACCESS_KEY
|
- name: S3_ACCESS_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_ACCESS_KEY
|
key: S3_ACCESS_KEY
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_SECRET_KEY
|
- name: S3_SECRET_KEY
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_SECRET_KEY
|
key: S3_SECRET_KEY
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_REGION
|
- name: S3_REGION
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_REGION
|
key: S3_REGION
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_PREFIX
|
- name: S3_PREFIX
|
||||||
valueFrom:
|
valueFrom:
|
||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: switchboard-s3
|
name: armature-s3
|
||||||
key: S3_PREFIX
|
key: S3_PREFIX
|
||||||
optional: true
|
optional: true
|
||||||
- name: S3_FORCE_PATH_STYLE
|
- name: S3_FORCE_PATH_STYLE
|
||||||
@@ -191,19 +191,19 @@ spec:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: storage
|
- name: storage
|
||||||
persistentVolumeClaim:
|
persistentVolumeClaim:
|
||||||
claimName: switchboard-storage${DEPLOY_SUFFIX}
|
claimName: armature-storage${DEPLOY_SUFFIX}
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard${DEPLOY_SUFFIX}
|
name: armature${DEPLOY_SUFFIX}
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
selector:
|
selector:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
ports:
|
ports:
|
||||||
- protocol: TCP
|
- protocol: TCP
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
# k8s/ingress.yaml
|
# k8s/ingress.yaml
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Ingress
|
# Armature Core - Ingress
|
||||||
# ============================================
|
# ============================================
|
||||||
# Path-based routing on a single domain:
|
# Path-based routing on a single domain:
|
||||||
# switchboard.DOMAIN/ → production
|
# armature.DOMAIN/ → production
|
||||||
# switchboard.DOMAIN/test/ → test (main branch)
|
# armature.DOMAIN/test/ → test (main branch)
|
||||||
# switchboard.DOMAIN/dev/ → dev (PR branches)
|
# armature.DOMAIN/dev/ → dev (PR branches)
|
||||||
#
|
#
|
||||||
# Each environment deploys its own Ingress resource.
|
# Each environment deploys its own Ingress resource.
|
||||||
# Traefik merges rules for the same host automatically.
|
# Traefik merges rules for the same host automatically.
|
||||||
@@ -18,29 +18,29 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
apiVersion: networking.k8s.io/v1
|
||||||
kind: Ingress
|
kind: Ingress
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard${DEPLOY_SUFFIX}
|
name: armature${DEPLOY_SUFFIX}
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
annotations:
|
annotations:
|
||||||
cert-manager.io/cluster-issuer: "${CERT_ISSUER}"
|
cert-manager.io/cluster-issuer: "${CERT_ISSUER}"
|
||||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
ingressClassName: "traefik"
|
ingressClassName: "traefik"
|
||||||
tls:
|
tls:
|
||||||
- hosts:
|
- hosts:
|
||||||
- ${DEPLOY_HOST}
|
- ${DEPLOY_HOST}
|
||||||
secretName: switchboard-tls
|
secretName: armature-tls
|
||||||
rules:
|
rules:
|
||||||
- host: "${DEPLOY_HOST}"
|
- host: "${DEPLOY_HOST}"
|
||||||
http:
|
http:
|
||||||
paths:
|
paths:
|
||||||
# All traffic → switchboard (unified image)
|
# All traffic → armature (unified image)
|
||||||
- path: ${BASE_PATH}/
|
- path: ${BASE_PATH}/
|
||||||
pathType: Prefix
|
pathType: Prefix
|
||||||
backend:
|
backend:
|
||||||
service:
|
service:
|
||||||
name: switchboard${DEPLOY_SUFFIX}
|
name: armature${DEPLOY_SUFFIX}
|
||||||
port:
|
port:
|
||||||
number: 80
|
number: 80
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# k8s/middleware-retry.yaml
|
# k8s/middleware-retry.yaml
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Traefik Retry Middleware
|
# Armature Core - Traefik Retry Middleware
|
||||||
# ============================================
|
# ============================================
|
||||||
# Retries failed requests (502, connection reset) once before giving up.
|
# Retries failed requests (502, connection reset) once before giving up.
|
||||||
# Applies to API + WebSocket paths via the Ingress annotation.
|
# Applies to API + WebSocket paths via the Ingress annotation.
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
apiVersion: traefik.io/v1alpha1
|
apiVersion: traefik.io/v1alpha1
|
||||||
kind: Middleware
|
kind: Middleware
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard-retry${DEPLOY_SUFFIX}
|
name: armature-retry${DEPLOY_SUFFIX}
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
retry:
|
retry:
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ metadata:
|
|||||||
name: traefik-middleware-manager
|
name: traefik-middleware-manager
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
rules:
|
rules:
|
||||||
- apiGroups: ["traefik.io"]
|
- apiGroups: ["traefik.io"]
|
||||||
resources: ["middlewares"]
|
resources: ["middlewares"]
|
||||||
@@ -28,7 +28,7 @@ metadata:
|
|||||||
name: gitea-runner-traefik-middleware
|
name: gitea-runner-traefik-middleware
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
subjects:
|
subjects:
|
||||||
- kind: ServiceAccount
|
- kind: ServiceAccount
|
||||||
name: gitea-runner-sa
|
name: gitea-runner-sa
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# k8s/storage-pvc.yaml
|
# k8s/storage-pvc.yaml
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Storage PVC
|
# Armature Core - Storage PVC
|
||||||
# ============================================
|
# ============================================
|
||||||
# Persistent storage for file attachments, extraction queue,
|
# Persistent storage for file attachments, extraction queue,
|
||||||
# and future knowledge base / compaction data.
|
# and future knowledge base / compaction data.
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: PersistentVolumeClaim
|
kind: PersistentVolumeClaim
|
||||||
metadata:
|
metadata:
|
||||||
name: switchboard-storage${DEPLOY_SUFFIX}
|
name: armature-storage${DEPLOY_SUFFIX}
|
||||||
namespace: ${NAMESPACE}
|
namespace: ${NAMESPACE}
|
||||||
labels:
|
labels:
|
||||||
app: switchboard
|
app: armature
|
||||||
component: storage
|
component: storage
|
||||||
env: ${ENVIRONMENT}
|
env: ${ENVIRONMENT}
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Packages
|
# Packages
|
||||||
|
|
||||||
Installable `.pkg` archives for Chat Switchboard. Each subdirectory is a
|
Installable `.pkg` archives for Chat Armature. Each subdirectory is a
|
||||||
package — either a surface (routable UI), an extension (hooks/tools/pipes),
|
package — either a surface (routable UI), an extension (hooks/tools/pipes),
|
||||||
or both (`full` type).
|
or both (`full` type).
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"icon": "🐛",
|
"icon": "🐛",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"description": "Public bug report submission with severity-based routing, team assignment, and SLA timers.",
|
"description": "Public bug report submission with severity-based routing, team assignment, and SLA timers.",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tier": "starlark",
|
"tier": "starlark",
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"description": "Core chat library — conversations, messages, participants, read cursors. Usable by other packages via lib.require().",
|
"description": "Core chat library — conversations, messages, participants, read cursors. Usable by other packages via lib.require().",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["db.write", "realtime.publish"],
|
"permissions": ["db.write", "realtime.publish"],
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"icon": "\ud83d\udcac",
|
"icon": "\ud83d\udcac",
|
||||||
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
|
"description": "Chat surface — conversations, messaging, typing indicators, read receipts.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["db.write", "realtime.publish"],
|
"permissions": ["db.write", "realtime.publish"],
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"icon": "📝",
|
"icon": "📝",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"description": "Multi-party content review with quorum signoff and revision cycle.",
|
"description": "Multi-party content review with quorum signoff and revision cycle.",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Renders ```csv code blocks as sortable, interactive HTML tables",
|
"description": "Renders ```csv code blocks as sortable, interactive HTML tables",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Switchboard Core — Dashboard Package (v0.31.1)
|
// Armature — Dashboard Package (v0.31.1)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// SDK Exercise Surface. Exercises every sw.* primitive with ZERO
|
// SDK Exercise Surface. Exercises every sw.* primitive with ZERO
|
||||||
// component CSS overrides. All styling comes from the SDK itself.
|
// component CSS overrides. All styling comes from the SDK itself.
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "full",
|
"type": "full",
|
||||||
"version": "0.31.1",
|
"version": "0.31.1",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"icon": "📊",
|
"icon": "📊",
|
||||||
"description": "Project dashboard exercising all SDK primitives (requires legacy sw.* SDK — dormant until rewritten)",
|
"description": "Project dashboard exercising all SDK primitives (requires legacy sw.* SDK — dormant until rewritten)",
|
||||||
"requires": ["legacy-sdk"],
|
"requires": ["legacy-sdk"],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines",
|
"description": "Renders ```diff code blocks with syntax-highlighted additions, deletions, and context lines",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ==========================================
|
/* ==========================================
|
||||||
Switchboard Core — Editor Surface (v0.25.0)
|
Armature — Editor Surface (v0.25.0)
|
||||||
==========================================
|
==========================================
|
||||||
Replaces editor-mode.css for the pane-based editor.
|
Replaces editor-mode.css for the pane-based editor.
|
||||||
Covers: topbar, bootstrap, and component-specific
|
Covers: topbar, bootstrap, and component-specific
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Switchboard Core — Editor Package (v0.31.0)
|
// Armature — Editor Package (v0.31.0)
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Installable .pkg that provides the code editor surface.
|
// Installable .pkg that provides the code editor surface.
|
||||||
// Mounts into #extension-mount (surface-extension template).
|
// Mounts into #extension-mount (surface-extension template).
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "full",
|
"type": "full",
|
||||||
"version": "0.31.0",
|
"version": "0.31.0",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"icon": "✏️",
|
"icon": "✏️",
|
||||||
"description": "Code editor with workspace management, file tree, and AI assist (requires legacy sw.* SDK — dormant until rewritten)",
|
"description": "Code editor with workspace management, file tree, and AI assist (requires legacy sw.* SDK — dormant until rewritten)",
|
||||||
"requires": ["legacy-sdk"],
|
"requires": ["legacy-sdk"],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "starlark",
|
"tier": "starlark",
|
||||||
"icon": "🏢",
|
"icon": "🏢",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"description": "Employee onboarding with automated provisioning, manager signoff, and welcome notification.",
|
"description": "Employee onboarding with automated provisioning, manager signoff, and welcome notification.",
|
||||||
"permissions": ["db.write", "notifications.send"],
|
"permissions": ["db.write", "notifications.send"],
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"icon": "🔀",
|
"icon": "🔀",
|
||||||
"description": "Gitea issue and PR board powered by the gitea-client library. Uses extension connections for authentication.",
|
"description": "Gitea issue and PR board powered by the gitea-client library. Uses extension connections for authentication.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["connections.read"],
|
"permissions": ["connections.read"],
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tier": "starlark",
|
"tier": "starlark",
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"description": "Shared Gitea client — connections, API helpers, optional data caching.",
|
"description": "Shared Gitea client — connections, API helpers, optional data caching.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["api.http", "connections.read", "db.read", "db.write"],
|
"permissions": ["api.http", "connections.read", "db.read", "db.write"],
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
type: 'full',
|
type: 'full',
|
||||||
version: '0.31.1',
|
version: '0.31.1',
|
||||||
tier: 'browser',
|
tier: 'browser',
|
||||||
author: 'Switchboard Core',
|
author: 'Armature',
|
||||||
description: 'Project dashboard exercising all SDK primitives',
|
description: 'Project dashboard exercising all SDK primitives',
|
||||||
route: '/s/dashboard',
|
route: '/s/dashboard',
|
||||||
permissions: [],
|
permissions: [],
|
||||||
|
|||||||
@@ -27,7 +27,7 @@
|
|||||||
type: 'full',
|
type: 'full',
|
||||||
version: '0.31.0',
|
version: '0.31.0',
|
||||||
tier: 'browser',
|
tier: 'browser',
|
||||||
author: 'Switchboard Core',
|
author: 'Armature',
|
||||||
description: 'Code editor with workspace management',
|
description: 'Code editor with workspace management',
|
||||||
route: '/s/editor',
|
route: '/s/editor',
|
||||||
layout: 'editor',
|
layout: 'editor',
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
var resp = await fetch(T.base + '/metrics');
|
var resp = await fetch(T.base + '/metrics');
|
||||||
T.assert(resp.ok, 'expected 200 from /metrics, got ' + resp.status);
|
T.assert(resp.ok, 'expected 200 from /metrics, got ' + resp.status);
|
||||||
var text = await resp.text();
|
var text = await resp.text();
|
||||||
T.assert(text.indexOf('switchboard_http_requests_total') !== -1, 'expected switchboard_http_requests_total in /metrics');
|
T.assert(text.indexOf('armature_http_requests_total') !== -1, 'expected armature_http_requests_total in /metrics');
|
||||||
T.assert(text.indexOf('switchboard_http_request_duration_seconds') !== -1, 'expected switchboard_http_request_duration_seconds in /metrics');
|
T.assert(text.indexOf('armature_http_request_duration_seconds') !== -1, 'expected armature_http_request_duration_seconds in /metrics');
|
||||||
T.assert(text.indexOf('switchboard_websocket_connections') !== -1, 'expected switchboard_websocket_connections in /metrics');
|
T.assert(text.indexOf('armature_websocket_connections') !== -1, 'expected armature_websocket_connections in /metrics');
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- GET /api/docs (Swagger UI) --
|
// -- GET /api/docs (Swagger UI) --
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
|
|
||||||
function getAuthTokenFromStorage() {
|
function getAuthTokenFromStorage() {
|
||||||
var env = (T.base || '').replace(/^\//, '') || 'default';
|
var env = (T.base || '').replace(/^\//, '') || 'default';
|
||||||
var keys = ['sb_auth_' + env, 'sb_auth', 'sb_token'];
|
var keys = ['arm_auth_' + env, 'arm_auth', 'arm_token'];
|
||||||
for (var i = 0; i < keys.length; i++) {
|
for (var i = 0; i < keys.length; i++) {
|
||||||
try {
|
try {
|
||||||
var raw = localStorage.getItem(keys[i]);
|
var raw = localStorage.getItem(keys[i]);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
* 8. tier-security.js — Adversarial red-team tests (auth, cross-tenant, input validation)
|
* 8. tier-security.js — Adversarial red-team tests (auth, cross-tenant, input validation)
|
||||||
* 9. tier-providers.js — Three-tier provider CRUD + live completions
|
* 9. tier-providers.js — Three-tier provider CRUD + live completions
|
||||||
* 10. tier-packaging.js — Extension permission lifecycle + secrets (v0.29.0)
|
* 10. tier-packaging.js — Extension permission lifecycle + secrets (v0.29.0)
|
||||||
* 11. tier-sdk.js — Switchboard SDK contract tests
|
* 11. tier-sdk.js — Armature SDK contract tests
|
||||||
* 12. ui.js — Render functions, export, provider setup panel
|
* 12. ui.js — Render functions, export, provider setup panel
|
||||||
* 11. (this file) — Boot
|
* 11. (this file) — Boot
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Validates Preact SDK (sw/sdk/index.js) contract: boot, identity,
|
* Validates Preact SDK (sw/sdk/index.js) contract: boot, identity,
|
||||||
* REST client, events, theme, pipe registration, execution ordering,
|
* REST client, events, theme, pipe registration, execution ordering,
|
||||||
* scoping, halt semantics, error isolation, introspection.
|
* scoping, halt semantics, error isolation, introspection.
|
||||||
* v0.37.14: Updated from Switchboard.init() to boot() API.
|
* v0.37.14: Updated from Armature.init() to boot() API.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|||||||
@@ -236,7 +236,7 @@
|
|||||||
}, 'Providers');
|
}, 'Providers');
|
||||||
T.el.controls.appendChild(btnProv);
|
T.el.controls.appendChild(btnProv);
|
||||||
|
|
||||||
// SDK tier button — tests switchboard-sdk.js contract
|
// SDK tier button — tests armature-sdk.js contract
|
||||||
var btnSdk = $('button', {
|
var btnSdk = $('button', {
|
||||||
className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
|
className: (typeof window.sw !== 'undefined') ? 'btn-secondary' : 'btn-ghost',
|
||||||
style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' },
|
style: { opacity: (typeof window.sw !== 'undefined') ? '1' : '0.5' },
|
||||||
@@ -368,7 +368,7 @@
|
|||||||
var totalMs = T.results.reduce(function (s, r) { return s + r.duration; }, 0);
|
var totalMs = T.results.reduce(function (s, r) { return s + r.duration; }, 0);
|
||||||
|
|
||||||
var lines = [];
|
var lines = [];
|
||||||
lines.push('=== Switchboard Core ICD Test Report ===');
|
lines.push('=== Armature ICD Test Report ===');
|
||||||
lines.push('Generated: ' + now);
|
lines.push('Generated: ' + now);
|
||||||
lines.push('Platform: v' + (T.manifest.version || '0.28.0'));
|
lines.push('Platform: v' + (T.manifest.version || '0.28.0'));
|
||||||
lines.push('User: ' + T.user.username + ' (' + T.user.role + ')');
|
lines.push('User: ' + T.user.username + ' (' + T.user.role + ')');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
|
"description": "Executes JavaScript in a sandboxed iframe. Allows the LLM to run code, verify calculations, and transform data — no server compute required.",
|
||||||
"requires": ["chat"],
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax",
|
"description": "Renders LaTeX math expressions: ```latex blocks and inline $...$ / $$...$$ syntax",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy",
|
"description": "Renders ```mermaid code blocks as interactive SVG diagrams with zoom/pan, SVG/PNG export, and source copy",
|
||||||
"requires": [],
|
"requires": [],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.8.0",
|
"version": "0.8.0",
|
||||||
"icon": "📝",
|
"icon": "📝",
|
||||||
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
|
"description": "Markdown notes surface with rich editor, folders, tags, backlinks, import/export, sidebar tabs, document outline, and note graph.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["db.write"],
|
"permissions": ["db.write"],
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"type": "extension",
|
"type": "extension",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
"description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.",
|
"description": "Tests regular expressions against input strings. Allows the LLM to verify regex patterns and see matches, groups, and indices.",
|
||||||
"requires": ["chat"],
|
"requires": ["chat"],
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"type": "full",
|
"type": "full",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"icon": "⏰",
|
"icon": "⏰",
|
||||||
"description": "Manage cron jobs and scheduled automations",
|
"description": "Manage cron jobs and scheduled automations",
|
||||||
"route": "/s/schedules",
|
"route": "/s/schedules",
|
||||||
|
|||||||
@@ -141,7 +141,7 @@
|
|||||||
|
|
||||||
function _readToken() {
|
function _readToken() {
|
||||||
var env = (T.base || '').replace(/^\//, '') || 'default';
|
var env = (T.base || '').replace(/^\//, '') || 'default';
|
||||||
var keys = ['sb_auth_' + env, 'sb_auth', 'sb_token'];
|
var keys = ['arm_auth_' + env, 'arm_auth', 'arm_token'];
|
||||||
for (var i = 0; i < keys.length; i++) {
|
for (var i = 0; i < keys.length; i++) {
|
||||||
try {
|
try {
|
||||||
var raw = localStorage.getItem(keys[i]);
|
var raw = localStorage.getItem(keys[i]);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"icon": "✅",
|
"icon": "✅",
|
||||||
"description": "Task management surface with lifecycle events, webhook integration, and scheduled reminders.",
|
"description": "Task management surface with lifecycle events, webhook integration, and scheduled reminders.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["db.write", "notifications.send"],
|
"permissions": ["db.write", "notifications.send"],
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
"layout": "single",
|
"layout": "single",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Example surface demonstrating Starlark API routes, extension-owned DB tables, admin settings, and SDK integration.",
|
"description": "Example surface demonstrating Starlark API routes, extension-owned DB tables, admin settings, and SDK integration.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["api.http", "db.read", "db.write"],
|
"permissions": ["api.http", "db.read", "db.write"],
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "starlark",
|
"tier": "starlark",
|
||||||
"icon": "🔔",
|
"icon": "🔔",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"description": "Outbound webhook delivery with connection credential support and delivery logging.",
|
"description": "Outbound webhook delivery with connection credential support and delivery logging.",
|
||||||
"permissions": ["api.http", "connections.read", "db.write"],
|
"permissions": ["api.http", "connections.read", "db.write"],
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def on_fire(ctx):
|
|||||||
|
|
||||||
resp = http.post(url, body=payload, headers={
|
resp = http.post(url, body=payload, headers={
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"X-Switchboard-Event": data.get("event_name", "workflow.notify"),
|
"X-Armature-Event": data.get("event_name", "workflow.notify"),
|
||||||
})
|
})
|
||||||
|
|
||||||
# Log the delivery
|
# Log the delivery
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"tier": "starlark",
|
"tier": "starlark",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Creates scoped chat conversations when workflow stages require team collaboration. Wire into stage_config as an on_advance hook.",
|
"description": "Creates scoped chat conversations when workflow stages require team collaboration. Wire into stage_config as an on_advance hook.",
|
||||||
"author": "switchboard",
|
"author": "armature",
|
||||||
|
|
||||||
"permissions": ["db.write", "realtime.publish"],
|
"permissions": ["db.write", "realtime.publish"],
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// ==========================================
|
// ==========================================
|
||||||
// Switchboard Core — Workflow Demo Surface
|
// Armature — Workflow Demo Surface
|
||||||
// ==========================================
|
// ==========================================
|
||||||
// Interactive walkthrough of the four example workflow packages.
|
// Interactive walkthrough of the four example workflow packages.
|
||||||
// Shows workflow cards with feature badges, stage flow diagrams,
|
// Shows workflow cards with feature badges, stage flow diagrams,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"tier": "browser",
|
"tier": "browser",
|
||||||
"icon": "🎓",
|
"icon": "🎓",
|
||||||
"author": "Switchboard Core",
|
"author": "Armature",
|
||||||
"description": "Interactive demo surface for example workflow packages. Cards, stage diagrams, and API walkthroughs.",
|
"description": "Interactive demo surface for example workflow packages. Cards, stage diagrams, and API walkthroughs.",
|
||||||
"route": "/s/workflow-demo",
|
"route": "/s/workflow-demo",
|
||||||
"auth": "authenticated",
|
"auth": "authenticated",
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
#
|
#
|
||||||
# switchboard-ca.sh — Certificate provisioning for Switchboard Core mTLS.
|
# armature-ca.sh — Certificate provisioning for Armature mTLS.
|
||||||
#
|
#
|
||||||
# Wraps openssl to generate a cluster CA, node certificates (ServerAuth +
|
# Wraps openssl to generate a cluster CA, node certificates (ServerAuth +
|
||||||
# ClientAuth), and user certificates (ClientAuth only). All output is PEM.
|
# ClientAuth), and user certificates (ClientAuth only). All output is PEM.
|
||||||
#
|
#
|
||||||
# Usage:
|
# Usage:
|
||||||
# switchboard-ca init
|
# armature-ca init
|
||||||
# switchboard-ca issue-node --name <node-name> --san <dns1,ip1,...>
|
# armature-ca issue-node --name <node-name> --san <dns1,ip1,...>
|
||||||
# switchboard-ca issue-user --cn <username> [--email <email>]
|
# armature-ca issue-user --cn <username> [--email <email>]
|
||||||
#
|
#
|
||||||
# Files are written to the current directory under ca/, nodes/, users/.
|
# Files are written to the current directory under ca/, nodes/, users/.
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ cmd_init() {
|
|||||||
-key "$CA_DIR/cluster-ca.key" \
|
-key "$CA_DIR/cluster-ca.key" \
|
||||||
-out "$CA_DIR/cluster-ca.crt" \
|
-out "$CA_DIR/cluster-ca.crt" \
|
||||||
-days "$CA_DAYS" \
|
-days "$CA_DAYS" \
|
||||||
-subj "/CN=Switchboard Cluster CA" \
|
-subj "/CN=Armature Cluster CA" \
|
||||||
-addext "basicConstraints=critical,CA:TRUE" \
|
-addext "basicConstraints=critical,CA:TRUE" \
|
||||||
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
-addext "keyUsage=critical,keyCertSign,cRLSign" \
|
||||||
2>/dev/null
|
2>/dev/null
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Database Bootstrap
|
# Armature - Database Bootstrap
|
||||||
# ============================================
|
# ============================================
|
||||||
# Idempotent: safe to run on every CI build.
|
# Idempotent: safe to run on every CI build.
|
||||||
# Creates the app role and database if they
|
# Creates the app role and database if they
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Database Migration Runner
|
# Armature - Database Migration Runner
|
||||||
# ============================================
|
# ============================================
|
||||||
# NOTE: The Go backend auto-migrates on startup.
|
# NOTE: The Go backend auto-migrates on startup.
|
||||||
# This script is for manual/emergency use only.
|
# This script is for manual/emergency use only.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ============================================
|
# ============================================
|
||||||
# Switchboard Core - Schema Validation (v0.16)
|
# Armature - Schema Validation (v0.16)
|
||||||
# ============================================
|
# ============================================
|
||||||
# Verifies the database schema is correct after
|
# Verifies the database schema is correct after
|
||||||
# migration. Checks expected tables, key columns,
|
# migration. Checks expected tables, key columns,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Switchboard Core - Server Environment Variables
|
# Armature - Server Environment Variables
|
||||||
# Copy this file to .env and fill in the values
|
# Copy this file to .env and fill in the values
|
||||||
|
|
||||||
# ── Server ───────────────────────────────────
|
# ── Server ───────────────────────────────────
|
||||||
@@ -9,9 +9,9 @@ BASE_PATH= # e.g. /chat for path-based routing
|
|||||||
# ── Database (PostgreSQL) ────────────────────
|
# ── Database (PostgreSQL) ────────────────────
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
DB_PORT=5432
|
DB_PORT=5432
|
||||||
DB_USER=switchboard_core
|
DB_USER=armature
|
||||||
DB_PASSWORD=your-secure-password-here
|
DB_PASSWORD=your-secure-password-here
|
||||||
DB_NAME=switchboard_core
|
DB_NAME=armature
|
||||||
DB_SSL_MODE=disable
|
DB_SSL_MODE=disable
|
||||||
DB_MAX_CONNS=25
|
DB_MAX_CONNS=25
|
||||||
|
|
||||||
@@ -20,19 +20,19 @@ JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
|||||||
# JWT durations (hardcoded in handlers/auth.go — not configurable via env):
|
# JWT durations (hardcoded in handlers/auth.go — not configurable via env):
|
||||||
# Access token: 15 minutes
|
# Access token: 15 minutes
|
||||||
# Refresh token: 7 days
|
# Refresh token: 7 days
|
||||||
JWT_ISSUER=switchboard-core
|
JWT_ISSUER=armature
|
||||||
|
|
||||||
# ── Bootstrap Admin ──────────────────────────
|
# ── Bootstrap Admin ──────────────────────────
|
||||||
# Creates or updates admin account on every startup.
|
# Creates or updates admin account on every startup.
|
||||||
# Set via K8s secret or env. Leave blank to skip.
|
# Set via K8s secret or env. Leave blank to skip.
|
||||||
SWITCHBOARD_ADMIN_USERNAME=
|
ARMATURE_ADMIN_USERNAME=
|
||||||
SWITCHBOARD_ADMIN_PASSWORD=
|
ARMATURE_ADMIN_PASSWORD=
|
||||||
|
|
||||||
# ── Seed Users (dev/test only) ──────────────
|
# ── Seed Users (dev/test only) ──────────────
|
||||||
# Pre-create users on startup. Ignored in production.
|
# Pre-create users on startup. Ignored in production.
|
||||||
# Format: user:pass:role,user2:pass2:role2 (role = admin|user, default: user)
|
# Format: user:pass:role,user2:pass2:role2 (role = admin|user, default: user)
|
||||||
# Idempotent: existing usernames are skipped.
|
# Idempotent: existing usernames are skipped.
|
||||||
# K8s: create secret "switchboard-seed-users" with key "users"
|
# K8s: create secret "armature-seed-users" with key "users"
|
||||||
SEED_USERS=
|
SEED_USERS=
|
||||||
|
|
||||||
# ── CORS ─────────────────────────────────────
|
# ── CORS ─────────────────────────────────────
|
||||||
@@ -57,7 +57,7 @@ STORAGE_PATH=/data/storage
|
|||||||
# S3 settings (only when STORAGE_BACKEND=s3)
|
# S3 settings (only when STORAGE_BACKEND=s3)
|
||||||
# Works with MinIO, Ceph RGW, AWS S3.
|
# Works with MinIO, Ceph RGW, AWS S3.
|
||||||
# S3_ENDPOINT=http://minio:9000
|
# S3_ENDPOINT=http://minio:9000
|
||||||
# S3_BUCKET=switchboard-storage
|
# S3_BUCKET=armature-storage
|
||||||
# S3_ACCESS_KEY=
|
# S3_ACCESS_KEY=
|
||||||
# S3_SECRET_KEY=
|
# S3_SECRET_KEY=
|
||||||
# S3_REGION=us-east-1
|
# S3_REGION=us-east-1
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"switchboard-core/models"
|
"armature/models"
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Mode string
|
type Mode string
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
"switchboard-core/models"
|
"armature/models"
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
const bcryptCost = 12
|
const bcryptCost = 12
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func (m *mockIdP) issueToken(sub, email, name string, groups []string, roles []s
|
|||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"iss": m.server.URL,
|
"iss": m.server.URL,
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
"aud": "switchboard",
|
"aud": "armature",
|
||||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
"email": email,
|
"email": email,
|
||||||
@@ -140,7 +140,7 @@ func (m *mockIdP) issueExpiredToken(sub string) string {
|
|||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"iss": m.server.URL,
|
"iss": m.server.URL,
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
"aud": "switchboard",
|
"aud": "armature",
|
||||||
"exp": time.Now().Add(-1 * time.Hour).Unix(),
|
"exp": time.Now().Add(-1 * time.Hour).Unix(),
|
||||||
"iat": time.Now().Add(-2 * time.Hour).Unix(),
|
"iat": time.Now().Add(-2 * time.Hour).Unix(),
|
||||||
}
|
}
|
||||||
@@ -160,7 +160,7 @@ func (m *mockIdP) issueTokenBadIssuer(sub string) string {
|
|||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"iss": "https://evil.example.com",
|
"iss": "https://evil.example.com",
|
||||||
"sub": sub,
|
"sub": sub,
|
||||||
"aud": "switchboard",
|
"aud": "armature",
|
||||||
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
"exp": time.Now().Add(1 * time.Hour).Unix(),
|
||||||
"iat": time.Now().Unix(),
|
"iat": time.Now().Unix(),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"switchboard-core/models"
|
"armature/models"
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs.
|
// ParseDN parses an RFC 2253 / RFC 4514 distinguished name into key-value pairs.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ErrNoCert is returned when no client certificate is presented on a
|
// ErrNoCert is returned when no client certificate is presented on a
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Test CA helpers ──────────────────────────────────────────────────
|
// ── Test CA helpers ──────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider.
|
// MTLSProxyConfig holds configuration for the proxy-terminated mTLS provider.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package auth
|
|||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"switchboard-core/store"
|
"armature/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestParseDN(t *testing.T) {
|
func TestParseDN(t *testing.T) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user