Skip to content

Commit f101124

Browse files
committed
feat(deployments): add config-driven Basilica tenant lifecycle
Adds a stateless multi-tenant provisioning layer for LLMTrace on Basilica, with a thin GitHub Actions surface for triggering from the product API. - deployments/basilica/lifecycle.py: ComponentSpec + TenantSpec dataclasses driving provision / update / deprovision / status. Caller owns the tenant_id -> UUID mapping (Basilica's SDK exposes deployment state only by server-assigned UUID, so identity cannot live in the library). - deployments/basilica/cli.py: loads YAML/JSON config (or inline JSON via --config-json), substitutes ${VAR} / ${VAR:-default} from environment for secret injection, emits JSON to stdout with exit 0 / 2 / 3 codes. - deployments/basilica/configs/examples/{starter,pro}.yaml: example tenant configs covering single- and multi-replica plans. - .github/workflows/tenant-lifecycle.yml: workflow_dispatch trigger exposing the four lifecycle actions, surfacing proxy/dashboard UUIDs and URLs as step outputs so the app can persist them. Validated end-to-end against a live Basilica account (tenant rt-919126): provision -> status -> recreate -> HTTP 200 on both URLs -> deprovision -> idempotent re-deprovision. update --strategy=restart surfaces a Basilica server-side limitation (k8s Deployment CR not materialised on fresh instances) cleanly via exit 3 + structured JSON error.
1 parent fdb6e22 commit f101124

6 files changed

Lines changed: 915 additions & 0 deletions

File tree

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
name: tenant-lifecycle
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
tenant_id:
7+
description: "DNS-safe tenant slug (^[a-z0-9][a-z0-9-]{0,29}$)"
8+
required: true
9+
type: string
10+
action:
11+
description: "Lifecycle action"
12+
required: true
13+
type: choice
14+
options:
15+
- provision
16+
- update
17+
- status
18+
- deprovision
19+
config_path:
20+
description: "Path in repo to a YAML/JSON tenant config (provision/update only)"
21+
required: false
22+
type: string
23+
default: ""
24+
config_json:
25+
description: "Inline JSON tenant config (provision/update only, takes precedence over config_path)"
26+
required: false
27+
type: string
28+
default: ""
29+
proxy_instance_id:
30+
description: "Basilica UUID of existing proxy (required for update; optional for status/deprovision)"
31+
required: false
32+
type: string
33+
default: ""
34+
dashboard_instance_id:
35+
description: "Basilica UUID of existing dashboard (required for update; optional for status/deprovision)"
36+
required: false
37+
type: string
38+
default: ""
39+
strategy:
40+
description: "Update strategy (update only): recreate (URL changes) or restart (URL stable)"
41+
required: false
42+
type: choice
43+
default: recreate
44+
options:
45+
- recreate
46+
- restart
47+
48+
permissions:
49+
contents: read
50+
51+
jobs:
52+
lifecycle:
53+
runs-on: ubuntu-latest
54+
timeout-minutes: 25
55+
env:
56+
# Secrets used both for the API client and for ${VAR} substitution
57+
# inside the loaded tenant config. Add per-tenant secrets here as
58+
# needed — they will be resolved at deploy time.
59+
BASILICA_API_TOKEN: ${{ secrets.BASILICA_API_TOKEN }}
60+
LLMTRACE_UPSTREAM_URL: ${{ secrets.LLMTRACE_UPSTREAM_URL }}
61+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
62+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
63+
LLMTRACE_AUTH_ADMIN_KEY: ${{ secrets.LLMTRACE_AUTH_ADMIN_KEY }}
64+
steps:
65+
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
66+
67+
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
68+
with:
69+
python-version: "3.12"
70+
71+
- name: Install deps
72+
run: |
73+
python3 -m pip install --upgrade pip
74+
python3 -m pip install basilica-sdk PyYAML
75+
76+
- name: Validate inputs
77+
env:
78+
ACTION: ${{ inputs.action }}
79+
CONFIG_PATH: ${{ inputs.config_path }}
80+
CONFIG_JSON: ${{ inputs.config_json }}
81+
PROXY_ID: ${{ inputs.proxy_instance_id }}
82+
DASHBOARD_ID: ${{ inputs.dashboard_instance_id }}
83+
run: |
84+
set -euo pipefail
85+
if [[ -z "${BASILICA_API_TOKEN:-}" ]]; then
86+
echo "::error::BASILICA_API_TOKEN secret is not configured"
87+
exit 2
88+
fi
89+
case "${ACTION}" in
90+
provision)
91+
if [[ -z "${CONFIG_PATH}" && -z "${CONFIG_JSON}" ]]; then
92+
echo "::error::action=provision requires config_path or config_json"
93+
exit 2
94+
fi
95+
;;
96+
update)
97+
if [[ -z "${CONFIG_PATH}" && -z "${CONFIG_JSON}" ]]; then
98+
echo "::error::action=update requires config_path or config_json"
99+
exit 2
100+
fi
101+
if [[ -z "${PROXY_ID}" || -z "${DASHBOARD_ID}" ]]; then
102+
echo "::error::action=update requires proxy_instance_id and dashboard_instance_id"
103+
exit 2
104+
fi
105+
;;
106+
esac
107+
108+
- name: Run lifecycle action
109+
id: run
110+
env:
111+
TENANT_ID: ${{ inputs.tenant_id }}
112+
ACTION: ${{ inputs.action }}
113+
CONFIG_PATH: ${{ inputs.config_path }}
114+
CONFIG_JSON: ${{ inputs.config_json }}
115+
PROXY_ID: ${{ inputs.proxy_instance_id }}
116+
DASHBOARD_ID: ${{ inputs.dashboard_instance_id }}
117+
STRATEGY: ${{ inputs.strategy }}
118+
run: |
119+
set -euo pipefail
120+
args=("--tenant-id" "${TENANT_ID}")
121+
case "${ACTION}" in
122+
provision)
123+
if [[ -n "${CONFIG_JSON}" ]]; then
124+
args+=("--config-json" "${CONFIG_JSON}")
125+
else
126+
args+=("--config" "${CONFIG_PATH}")
127+
fi
128+
;;
129+
update)
130+
args+=("--strategy" "${STRATEGY}")
131+
args+=("--proxy-instance-id" "${PROXY_ID}")
132+
args+=("--dashboard-instance-id" "${DASHBOARD_ID}")
133+
if [[ -n "${CONFIG_JSON}" ]]; then
134+
args+=("--config-json" "${CONFIG_JSON}")
135+
else
136+
args+=("--config" "${CONFIG_PATH}")
137+
fi
138+
;;
139+
status|deprovision)
140+
[[ -n "${PROXY_ID}" ]] && args+=("--proxy-instance-id" "${PROXY_ID}")
141+
[[ -n "${DASHBOARD_ID}" ]] && args+=("--dashboard-instance-id" "${DASHBOARD_ID}")
142+
;;
143+
*)
144+
echo "::error::unknown action ${ACTION}"
145+
exit 2
146+
;;
147+
esac
148+
149+
set +e
150+
python3 -m deployments.basilica.cli "${ACTION}" "${args[@]}" > result.json
151+
code=$?
152+
set -e
153+
cat result.json
154+
echo "exit_code=${code}" >> "${GITHUB_OUTPUT}"
155+
python3 - <<'PY' >> "${GITHUB_OUTPUT}"
156+
import json, pathlib
157+
data = json.loads(pathlib.Path("result.json").read_text() or "{}")
158+
print(f"proxy_instance_id={data.get('proxy_instance_id') or ''}")
159+
print(f"dashboard_instance_id={data.get('dashboard_instance_id') or ''}")
160+
print(f"proxy_url={data.get('proxy_url') or ''}")
161+
print(f"dashboard_url={data.get('dashboard_url') or ''}")
162+
PY
163+
if [[ "${code}" -ne 0 ]]; then
164+
exit "${code}"
165+
fi
166+
167+
- name: Write step summary
168+
if: always()
169+
env:
170+
ACTION: ${{ inputs.action }}
171+
TENANT_ID: ${{ inputs.tenant_id }}
172+
run: |
173+
set -euo pipefail
174+
{
175+
echo "## tenant-lifecycle: ${ACTION} ${TENANT_ID}"
176+
echo ""
177+
echo '```json'
178+
cat result.json 2>/dev/null || echo "{}"
179+
echo '```'
180+
} >> "${GITHUB_STEP_SUMMARY}"

deployments/basilica/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""LLMTrace deployment automation for the Basilica platform."""

0 commit comments

Comments
 (0)