358 lines
13 KiB
Bash
Executable File
358 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
REPO="${REPO:-Sybil-2}"
|
|
WORKLOAD="${WORKLOAD:-sybil-dev}"
|
|
CLUSTER="${CLUSTER:-viper}"
|
|
NAMESPACE="${NAMESPACE:-buzzert}"
|
|
QUOTA="${QUOTA:-research-api}"
|
|
SECRET="${SECRET:-sybil-dev-env}"
|
|
REMOTE_ROOT="${REMOTE_ROOT:-/root/code/$REPO}"
|
|
INGRESS_PORT="${INGRESS_PORT:-5173}"
|
|
CODEX_IMAGE="${CODEX_IMAGE:-openai.azurecr.io/rcall:crow-1b540b5e15f9-2026-01-22-2404-312}"
|
|
CODEX_SERVICE="${CODEX_SERVICE:-sybil-codex-devbox}"
|
|
CODEX_CONTAINER="${CODEX_CONTAINER:-codex-devbox}"
|
|
CODEX_SSH_SECRET="${CODEX_SSH_SECRET:-sybil-codex-ssh}"
|
|
CODEX_SSH_PORT="${CODEX_SSH_PORT:-2222}"
|
|
CODEX_WORKDIR="${CODEX_WORKDIR:-/workspace/sybil-codex}"
|
|
CODEX_SSH_KEY_PATH="${CODEX_SSH_KEY_PATH:-/run/secrets/sybil-codex-ssh/id_ed25519}"
|
|
|
|
echo "Syncing $REPO to $WORKLOAD on $CLUSTER/$NAMESPACE..."
|
|
brix create \
|
|
--project-root "$(dirname "$ROOT")" \
|
|
--path "$REPO=$ROOT" \
|
|
--repositories "$REPO" \
|
|
--include-untracked \
|
|
--reconcile-policy continuous \
|
|
-- \
|
|
"cluster=$CLUSTER" \
|
|
"namespace=$NAMESPACE" \
|
|
"name=$WORKLOAD" \
|
|
"pool.spec.quota=$QUOTA" \
|
|
"pool.spec.size.resource=cpu" \
|
|
"pool.spec.size.quantity=1" \
|
|
"pool.spec.priority=low"
|
|
|
|
if [[ "$REPO" != "openai" ]]; then
|
|
if kubectl --context "$CLUSTER" -n "$NAMESPACE" get git "$WORKLOAD" -o json 2>/dev/null | jq -e '.spec.repositories.openai?' >/dev/null; then
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" patch git "$WORKLOAD" --type=json -p='[{"op":"remove","path":"/spec/repositories/openai"}]'
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "${OPENAI_API_KEY:-}" && -n "${EXA_API_KEY:-}" ]]; then
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" create secret generic "$SECRET" \
|
|
--from-literal=OPENAI_API_KEY="$OPENAI_API_KEY" \
|
|
--from-literal=EXA_API_KEY="$EXA_API_KEY" \
|
|
--dry-run=client -o yaml | kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f -
|
|
elif [[ -n "${OPENAI_API_KEY:-}" || -n "${EXA_API_KEY:-}" ]]; then
|
|
echo "Only one API key env var is set; keeping existing Kubernetes secret $SECRET." >&2
|
|
fi
|
|
|
|
if [[ "$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get secret "$SECRET" -o name 2>/dev/null || true)" == "" ]]; then
|
|
echo "Secret $SECRET does not exist. Re-run with OPENAI_API_KEY and EXA_API_KEY set." >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get secret "$CODEX_SSH_SECRET" -o name 2>/dev/null || true)" == "" ]]; then
|
|
tmpdir="$(mktemp -d)"
|
|
trap 'rm -rf "$tmpdir"' EXIT
|
|
ssh-keygen -t ed25519 -N "" -C "sybil-codex@$WORKLOAD" -f "$tmpdir/id_ed25519" >/dev/null
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" create secret generic "$CODEX_SSH_SECRET" \
|
|
--from-file=id_ed25519="$tmpdir/id_ed25519" \
|
|
--from-file=id_ed25519.pub="$tmpdir/id_ed25519.pub" \
|
|
--dry-run=client -o yaml | kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f -
|
|
fi
|
|
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" patch secret "$SECRET" --type=merge -p "$(jq -n \
|
|
--arg host "$CODEX_SERVICE" \
|
|
--arg port "$CODEX_SSH_PORT" \
|
|
--arg workdir "$CODEX_WORKDIR" \
|
|
--arg keyPath "$CODEX_SSH_KEY_PATH" \
|
|
'{
|
|
stringData: {
|
|
CHAT_CODEX_TOOL_ENABLED: "true",
|
|
CHAT_CODEX_REMOTE_HOST: $host,
|
|
CHAT_CODEX_REMOTE_USER: "root",
|
|
CHAT_CODEX_REMOTE_PORT: $port,
|
|
CHAT_CODEX_REMOTE_WORKDIR: $workdir,
|
|
CHAT_CODEX_SSH_KEY_PATH: $keyPath,
|
|
CHAT_CODEX_EXEC_TIMEOUT_MS: "600000",
|
|
CHAT_SHELL_TOOL_ENABLED: "true"
|
|
}
|
|
}')"
|
|
|
|
ensure_codex_deployment() {
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" apply -f - <<KUBE_MANIFEST
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: $CODEX_SERVICE
|
|
labels:
|
|
app.kubernetes.io/name: $CODEX_SERVICE
|
|
app.kubernetes.io/part-of: sybil-dev
|
|
spec:
|
|
replicas: 1
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: $CODEX_SERVICE
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: $CODEX_SERVICE
|
|
app.kubernetes.io/part-of: sybil-dev
|
|
spec:
|
|
tolerations:
|
|
- key: openai.com/team
|
|
operator: Exists
|
|
effect: NoSchedule
|
|
containers:
|
|
- name: $CODEX_CONTAINER
|
|
image: $CODEX_IMAGE
|
|
imagePullPolicy: IfNotPresent
|
|
command: ["/bin/bash", "-lc"]
|
|
args:
|
|
- |
|
|
set -euo pipefail
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
if ! command -v sshd >/dev/null 2>&1; then
|
|
apt-get update
|
|
apt-get install -y --no-install-recommends openssh-server ca-certificates git curl
|
|
rm -rf /var/lib/apt/lists/*
|
|
fi
|
|
if ! command -v codex >/dev/null 2>&1; then
|
|
npm install -g @openai/codex
|
|
fi
|
|
mkdir -p /run/sshd /root/.ssh "$CODEX_WORKDIR"
|
|
cp /run/secrets/sybil-codex-ssh/id_ed25519.pub /root/.ssh/authorized_keys
|
|
chmod 700 /root/.ssh
|
|
chmod 600 /root/.ssh/authorized_keys
|
|
{
|
|
if [[ -n "\${OPENAI_API_KEY:-}" ]]; then
|
|
printf 'export OPENAI_API_KEY=%q\n' "\$OPENAI_API_KEY"
|
|
fi
|
|
if [[ -n "\${EXA_API_KEY:-}" ]]; then
|
|
printf 'export EXA_API_KEY=%q\n' "\$EXA_API_KEY"
|
|
fi
|
|
} >/root/.sybil-codex-env
|
|
chmod 600 /root/.sybil-codex-env
|
|
cat >/usr/local/bin/sybil-codex-ssh-command <<'SSH_COMMAND'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
if [[ -f /root/.sybil-codex-env ]]; then
|
|
source /root/.sybil-codex-env
|
|
fi
|
|
if [[ -n "\${SSH_ORIGINAL_COMMAND:-}" ]]; then
|
|
exec /bin/bash -lc "\$SSH_ORIGINAL_COMMAND"
|
|
fi
|
|
exec /bin/bash -l
|
|
SSH_COMMAND
|
|
chmod 700 /usr/local/bin/sybil-codex-ssh-command
|
|
ssh-keygen -A
|
|
cat >/tmp/sybil-codex-sshd_config <<SSHD_CONFIG
|
|
Port $CODEX_SSH_PORT
|
|
ListenAddress 0.0.0.0
|
|
PermitRootLogin prohibit-password
|
|
PasswordAuthentication no
|
|
PubkeyAuthentication yes
|
|
AcceptEnv OPENAI_API_KEY EXA_API_KEY
|
|
AuthorizedKeysFile .ssh/authorized_keys
|
|
PermitUserEnvironment no
|
|
ForceCommand /usr/local/bin/sybil-codex-ssh-command
|
|
AllowTcpForwarding no
|
|
X11Forwarding no
|
|
Subsystem sftp internal-sftp
|
|
PidFile /run/sshd/sybil-codex.pid
|
|
SSHD_CONFIG
|
|
exec \$(command -v sshd) -D -e -f /tmp/sybil-codex-sshd_config
|
|
ports:
|
|
- name: ssh
|
|
containerPort: $CODEX_SSH_PORT
|
|
envFrom:
|
|
- secretRef:
|
|
name: $SECRET
|
|
resources:
|
|
requests:
|
|
cpu: "1"
|
|
memory: 4Gi
|
|
limits:
|
|
cpu: "1"
|
|
memory: 4Gi
|
|
volumeMounts:
|
|
- name: sybil-codex-ssh
|
|
mountPath: /run/secrets/sybil-codex-ssh
|
|
readOnly: true
|
|
- name: sybil-codex-workspace
|
|
mountPath: /workspace
|
|
volumes:
|
|
- name: sybil-codex-ssh
|
|
secret:
|
|
secretName: $CODEX_SSH_SECRET
|
|
defaultMode: 384
|
|
- name: sybil-codex-workspace
|
|
emptyDir: {}
|
|
---
|
|
apiVersion: v1
|
|
kind: Service
|
|
metadata:
|
|
name: $CODEX_SERVICE
|
|
labels:
|
|
app.kubernetes.io/name: $CODEX_SERVICE
|
|
app.kubernetes.io/part-of: sybil-dev
|
|
spec:
|
|
type: ClusterIP
|
|
selector:
|
|
app.kubernetes.io/name: $CODEX_SERVICE
|
|
ports:
|
|
- name: ssh
|
|
port: $CODEX_SSH_PORT
|
|
targetPort: ssh
|
|
KUBE_MANIFEST
|
|
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" rollout status "deployment/$CODEX_SERVICE" --timeout=300s
|
|
}
|
|
|
|
ensure_codex_deployment
|
|
|
|
ensure_env_from() {
|
|
local resource="$1"
|
|
local name="$2"
|
|
local jsonpath="$3"
|
|
local patch_path="$4"
|
|
local existing_env_from
|
|
|
|
existing_env_from="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get "$resource" "$name" \
|
|
-o "jsonpath=$jsonpath" 2>/dev/null || true)"
|
|
|
|
if [[ "$existing_env_from" == *"$SECRET"* ]]; then
|
|
return
|
|
fi
|
|
|
|
local patch
|
|
if [[ -z "$existing_env_from" ]]; then
|
|
patch="[{\"op\":\"add\",\"path\":\"$patch_path\",\"value\":[{\"secretRef\":{\"name\":\"$SECRET\"}}]}]"
|
|
else
|
|
patch="[{\"op\":\"add\",\"path\":\"$patch_path/-\",\"value\":{\"secretRef\":{\"name\":\"$SECRET\"}}}]"
|
|
fi
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" patch "$resource" "$name" --type=json -p "$patch"
|
|
}
|
|
|
|
ensure_env_from \
|
|
workload \
|
|
"$WORKLOAD" \
|
|
"{.spec.pools[0].template.template.spec.workers.template.spec.containers[0].envFrom}" \
|
|
"/spec/pools/0/template/template/spec/workers/template/spec/containers/0/envFrom"
|
|
|
|
ensure_env_from \
|
|
pool \
|
|
"$WORKLOAD" \
|
|
"{.spec.workers.template.spec.containers[0].envFrom}" \
|
|
"/spec/workers/template/spec/containers/0/envFrom"
|
|
|
|
codex_ssh_volume_json="$(jq -n --arg secret "$CODEX_SSH_SECRET" '{
|
|
name: "sybil-codex-ssh",
|
|
secret: {
|
|
secretName: $secret,
|
|
defaultMode: 384
|
|
}
|
|
}')"
|
|
codex_main_mount_json="$(jq -n '{name: "sybil-codex-ssh", mountPath: "/run/secrets/sybil-codex-ssh", readOnly: true}')"
|
|
|
|
patch_pool_codex_mount() {
|
|
local pool_json containers volumes patch
|
|
pool_json="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pool "$WORKLOAD" -o json)"
|
|
containers="$(jq \
|
|
--arg sidecarName "$CODEX_CONTAINER" \
|
|
--argjson mainMount "$codex_main_mount_json" \
|
|
'
|
|
def ensure_mount($mount):
|
|
.volumeMounts = (((.volumeMounts // []) | map(select(.name != $mount.name))) + [$mount]);
|
|
(.spec.workers.template.spec.containers // [])
|
|
| map(if .name == "main" then ensure_mount($mainMount) else . end)
|
|
| map(select(.name != $sidecarName))
|
|
' <<<"$pool_json")"
|
|
volumes="$(jq \
|
|
--argjson sshVolume "$codex_ssh_volume_json" \
|
|
'
|
|
(.spec.workers.template.spec.volumes // [])
|
|
| map(select(.name != $sshVolume.name and .name != "sybil-codex-workspace"))
|
|
+ [$sshVolume]
|
|
' <<<"$pool_json")"
|
|
patch="$(jq -n --argjson containers "$containers" --argjson volumes "$volumes" '{
|
|
spec: {
|
|
workers: {
|
|
template: {
|
|
spec: {
|
|
containers: $containers,
|
|
volumes: $volumes
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}')"
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" patch pool "$WORKLOAD" --type=merge -p "$patch"
|
|
}
|
|
|
|
patch_workload_codex_mount() {
|
|
local workload_json pools patch
|
|
workload_json="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get workload "$WORKLOAD" -o json)"
|
|
pools="$(jq \
|
|
--arg groupName "$WORKLOAD" \
|
|
--arg sidecarName "$CODEX_CONTAINER" \
|
|
--argjson mainMount "$codex_main_mount_json" \
|
|
--argjson sshVolume "$codex_ssh_volume_json" \
|
|
'
|
|
def ensure_mount($mount):
|
|
.volumeMounts = (((.volumeMounts // []) | map(select(.name != $mount.name))) + [$mount]);
|
|
.spec.pools
|
|
| map(
|
|
if .groupName == $groupName then
|
|
.template.template.spec.workers.template.spec.containers =
|
|
((.template.template.spec.workers.template.spec.containers // [])
|
|
| map(if .name == "main" then ensure_mount($mainMount) else . end)
|
|
| map(select(.name != $sidecarName)))
|
|
| .template.template.spec.workers.template.spec.volumes =
|
|
((.template.template.spec.workers.template.spec.volumes // [])
|
|
| map(select(.name != $sshVolume.name and .name != "sybil-codex-workspace"))
|
|
+ [$sshVolume])
|
|
else
|
|
.
|
|
end
|
|
)
|
|
' <<<"$workload_json")"
|
|
patch="$(jq -n --argjson pools "$pools" '{spec: {pools: $pools}}')"
|
|
kubectl --context "$CLUSTER" -n "$NAMESPACE" patch workload "$WORKLOAD" --type=merge -p "$patch"
|
|
}
|
|
|
|
patch_workload_codex_mount
|
|
patch_pool_codex_mount
|
|
|
|
echo "Waiting for $WORKLOAD-0 to run the current pool revision..."
|
|
deadline=$((SECONDS + 300))
|
|
while true; do
|
|
pool_revision="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pool "$WORKLOAD" -o jsonpath='{.status.revision}' 2>/dev/null || true)"
|
|
pod_revision="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.metadata.labels.brix\.openai\.com/revision}' 2>/dev/null || true)"
|
|
pod_ready="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || true)"
|
|
pod_deletion_timestamp="$(kubectl --context "$CLUSTER" -n "$NAMESPACE" get pod "$WORKLOAD-0" -o jsonpath='{.metadata.deletionTimestamp}' 2>/dev/null || true)"
|
|
|
|
if [[ -n "$pool_revision" && "$pod_revision" == "$pool_revision" && "$pod_ready" == "True" && -z "$pod_deletion_timestamp" ]]; then
|
|
break
|
|
fi
|
|
|
|
if (( SECONDS >= deadline )); then
|
|
echo "Timed out waiting for current ready pod. pool_revision=$pool_revision pod_revision=$pod_revision pod_ready=$pod_ready deleting=$pod_deletion_timestamp" >&2
|
|
exit 1
|
|
fi
|
|
|
|
sleep 5
|
|
done
|
|
|
|
brix run --clusters "$CLUSTER" --namespace "$NAMESPACE" --pools "$WORKLOAD" --dir "$REMOTE_ROOT" -- ./scripts/devbox-start.sh
|
|
|
|
ssh_check="ssh -n -i '$CODEX_SSH_KEY_PATH' -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/tmp/sybil-codex-known-hosts -p '$CODEX_SSH_PORT' root@'$CODEX_SERVICE' true"
|
|
brix run --clusters "$CLUSTER" --namespace "$NAMESPACE" --pools "$WORKLOAD" --dir "$REMOTE_ROOT" -- /bin/bash -lc "$ssh_check"
|
|
|
|
url="$(brix ingress create --cluster "$CLUSTER" --namespace "$NAMESPACE" --pod "$WORKLOAD-0" --ports "web:$INGRESS_PORT" --root web --timeout 300 "$WORKLOAD" | tail -1)"
|
|
|
|
echo "Devbox update complete: $url"
|