Phase 2 references for the host-agent Dune adapter, moved out of volatile /tmp
into docs/reference-repos/ (per Commander). Three upstream projects, .git +
node_modules + compiled binaries stripped (16MB source). Nested AI-instruction
files (.claude/, CLAUDE.md) removed so they don't pollute Corrosion sessions.
- icehunter/ dune-admin (Go+React) — 4 control planes; SETUP_DOCKER.md is the
closest analog to our agent's Dune docker control plane (compose
lifecycle, docker logs, RabbitMQ-via-exec, dune Postgres schema)
- adainrivers/ Rust/Tauri desktop — SSH+k8s BattleGroup control, maintenance
daemon, in-game admin console (Rust idiom reference)
- the4rchangel/ Node web UI replacing battlegroup.bat — matches the Commander's
Hyper-V self-host path + game-config schema
See docs/reference-repos/README.md for the full index + how we use each.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
313 lines
11 KiB
Bash
Executable File
313 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
cd "$ROOT_DIR"
|
|
|
|
VM_USER="${VM_USER:-dune}"
|
|
VM_HOST="${VM_HOST:-192.168.0.72}"
|
|
SSH_KEY_PATH="${SSH_KEY_PATH:-}"
|
|
KUBECONFIG_PATH="${KUBECONFIG_PATH:-$HOME/.kube/dune-external.yaml}"
|
|
IMAGE="${IMAGE:-}"
|
|
NAMESPACE="${NAMESPACE:-dune-admin}"
|
|
MANIFEST="${MANIFEST:-deploy/k8s/dune-admin.rendered.yaml}"
|
|
SKIP_KUBECONFIG=0
|
|
SKIP_BUILD=0
|
|
SKIP_IMAGE_IMPORT=0
|
|
NO_PORT_FORWARD=1
|
|
|
|
usage() {
|
|
cat <<'EOF'
|
|
Usage: ./deploy.sh [options]
|
|
|
|
Options:
|
|
--vm-user <user> VM SSH user (default: dune)
|
|
--vm-host <host> VM host/IP (default: 192.168.0.72)
|
|
--ssh-key <path> SSH key path (default: ./sshKey when present)
|
|
--kubeconfig <path> Local kubeconfig path (default: ~/.kube/dune-external.yaml)
|
|
--image <name:tag> Image tag to build/deploy
|
|
default: dune-admin:local-<timestamp>
|
|
when skipping build/import: dune-admin:local
|
|
--namespace <ns> K8s namespace (default: dune-admin)
|
|
--manifest <path> Rendered manifest path (default: deploy/k8s/dune-admin.rendered.yaml)
|
|
--skip-kubeconfig Reuse existing kubeconfig without pulling from VM
|
|
--skip-build Skip docker build step
|
|
--skip-image-import Skip VM k3s image import step
|
|
--port-forward Open kubectl port-forward after deploy
|
|
--no-port-forward Skip kubectl port-forward after deploy (default)
|
|
-h, --help Show this help
|
|
EOF
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--vm-user) VM_USER="$2"; shift 2 ;;
|
|
--vm-host) VM_HOST="$2"; shift 2 ;;
|
|
--ssh-key) SSH_KEY_PATH="$2"; shift 2 ;;
|
|
--kubeconfig) KUBECONFIG_PATH="$2"; shift 2 ;;
|
|
--image) IMAGE="$2"; shift 2 ;;
|
|
--namespace) NAMESPACE="$2"; shift 2 ;;
|
|
--manifest) MANIFEST="$2"; shift 2 ;;
|
|
--skip-kubeconfig) SKIP_KUBECONFIG=1; shift ;;
|
|
--skip-build) SKIP_BUILD=1; shift ;;
|
|
--skip-image-import) SKIP_IMAGE_IMPORT=1; shift ;;
|
|
--port-forward) NO_PORT_FORWARD=0; shift ;;
|
|
--no-port-forward) NO_PORT_FORWARD=1; shift ;;
|
|
-h|--help) usage; exit 0 ;;
|
|
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
|
esac
|
|
done
|
|
|
|
require_cmd() {
|
|
command -v "$1" >/dev/null 2>&1 || {
|
|
echo "Missing required command: $1" >&2
|
|
exit 1
|
|
}
|
|
}
|
|
|
|
require_cmd kubectl
|
|
require_cmd ssh
|
|
require_cmd scp
|
|
require_cmd docker
|
|
require_cmd make
|
|
|
|
if [[ -z "$SSH_KEY_PATH" && -f "$ROOT_DIR/sshKey" ]]; then
|
|
SSH_KEY_PATH="$ROOT_DIR/sshKey"
|
|
fi
|
|
if [[ -z "$IMAGE" ]]; then
|
|
if [[ "$SKIP_BUILD" -eq 1 || "$SKIP_IMAGE_IMPORT" -eq 1 ]]; then
|
|
IMAGE="dune-admin:local"
|
|
else
|
|
IMAGE="dune-admin:local-$(date +%Y%m%d%H%M%S)"
|
|
fi
|
|
fi
|
|
|
|
SSH_OPTS=(-o PreferredAuthentications=publickey,password -o PubkeyAuthentication=yes -o PasswordAuthentication=yes)
|
|
SCP_OPTS=(-o PreferredAuthentications=publickey,password -o PubkeyAuthentication=yes -o PasswordAuthentication=yes)
|
|
if [[ -n "$SSH_KEY_PATH" ]]; then
|
|
if [[ ! -f "$SSH_KEY_PATH" ]]; then
|
|
echo "SSH key not found: $SSH_KEY_PATH" >&2
|
|
exit 1
|
|
fi
|
|
SSH_OPTS+=(-i "$SSH_KEY_PATH" -o IdentitiesOnly=yes)
|
|
SCP_OPTS+=(-i "$SSH_KEY_PATH" -o IdentitiesOnly=yes)
|
|
echo "Using SSH key: $SSH_KEY_PATH (fallback to password enabled)"
|
|
else
|
|
echo "No SSH key provided/found; using password auth (or agent) for SSH."
|
|
fi
|
|
|
|
if [[ "$SKIP_KUBECONFIG" -eq 0 ]]; then
|
|
mkdir -p "$(dirname "$KUBECONFIG_PATH")"
|
|
echo "Pulling kubeconfig from ${VM_USER}@${VM_HOST}..."
|
|
ssh "${SSH_OPTS[@]}" "${VM_USER}@${VM_HOST}" "sudo cat /etc/rancher/k3s/k3s.yaml" > "$KUBECONFIG_PATH"
|
|
awk -v host="$VM_HOST" '{gsub(/127\.0\.0\.1/, host); print}' "$KUBECONFIG_PATH" > "${KUBECONFIG_PATH}.tmp"
|
|
mv "${KUBECONFIG_PATH}.tmp" "$KUBECONFIG_PATH"
|
|
fi
|
|
|
|
export KUBECONFIG="$KUBECONFIG_PATH"
|
|
echo "Using KUBECONFIG=$KUBECONFIG"
|
|
kubectl get nodes
|
|
|
|
if [[ "$SKIP_BUILD" -eq 0 ]]; then
|
|
echo "Building image: $IMAGE"
|
|
APP_VERSION="$(cat "$ROOT_DIR/VERSION" 2>/dev/null || echo "unknown")"
|
|
GIT_COMMIT="$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown")"
|
|
BUILD_TIME="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
docker buildx build \
|
|
--platform linux/amd64 \
|
|
-f deploy/Dockerfile \
|
|
--build-arg APP_VERSION="$APP_VERSION" \
|
|
--build-arg GIT_COMMIT="$GIT_COMMIT" \
|
|
--build-arg BUILD_TIME="$BUILD_TIME" \
|
|
-t "$IMAGE" --load .
|
|
fi
|
|
|
|
if [[ "$SKIP_IMAGE_IMPORT" -eq 0 ]]; then
|
|
echo "Importing image into k3s runtime on ${VM_HOST}..."
|
|
tmp_tar="$(mktemp /tmp/dune-admin-image.XXXXXX.tar)"
|
|
trap 'rm -f "$tmp_tar"' EXIT
|
|
docker save -o "$tmp_tar" "$IMAGE"
|
|
scp "${SCP_OPTS[@]}" "$tmp_tar" "${VM_USER}@${VM_HOST}:/tmp/dune-admin-image.tar"
|
|
ssh "${SSH_OPTS[@]}" "${VM_USER}@${VM_HOST}" "sudo k3s ctr images import /tmp/dune-admin-image.tar && rm -f /tmp/dune-admin-image.tar"
|
|
rm -f "$tmp_tar"
|
|
trap - EXIT
|
|
fi
|
|
|
|
echo "Rendering k8s manifest..."
|
|
make render-k8s
|
|
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
echo "Manifest not found: $MANIFEST" >&2
|
|
exit 1
|
|
fi
|
|
|
|
control_ns="$(awk -F':[[:space:]]*' '/^[[:space:]]*control_namespace:[[:space:]]*/{gsub(/"/,"",$2); print $2; exit}' "$MANIFEST")"
|
|
db_host_override=""
|
|
if [[ -n "$control_ns" ]]; then
|
|
svc_rows="$(kubectl -n "$control_ns" get svc -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range .spec.ports[*]}{.port}{" "}{end}{"\n"}{end}' 2>/dev/null || true)"
|
|
db_svc="$(printf '%s\n' "$svc_rows" | awk '$0 ~ /(^|[[:space:]])15432([[:space:]]|$)/{print $1; exit}')"
|
|
if [[ -n "$db_svc" ]]; then
|
|
db_host_override="${db_svc}.${control_ns}.svc.cluster.local"
|
|
echo "Using in-cluster DB host: $db_host_override"
|
|
fi
|
|
fi
|
|
|
|
awk -v image="$IMAGE" -v dbhost="$db_host_override" '
|
|
BEGIN { image_changed = 0 }
|
|
/^[[:space:]]*image:[[:space:]]*(ghcr\.io\/icehunter\/dune-admin|dune-admin)/ {
|
|
sub(/image:.*/, "image: " image)
|
|
image_changed = 1
|
|
}
|
|
/^[[:space:]]*CONTROL:[[:space:]]*/ {
|
|
sub(/CONTROL:.*/, "CONTROL: \"local\"")
|
|
}
|
|
/^[[:space:]]*control:[[:space:]]*/ {
|
|
sub(/control:.*/, "control: local")
|
|
}
|
|
/^[[:space:]]*cmd_status:[[:space:]]*/ { next }
|
|
/^[[:space:]]*cmd_start:[[:space:]]*/ { next }
|
|
/^[[:space:]]*cmd_stop:[[:space:]]*/ { next }
|
|
/^[[:space:]]*cmd_restart:[[:space:]]*/ { next }
|
|
dbhost != "" && /^[[:space:]]*DB_HOST:[[:space:]]*/ {
|
|
sub(/DB_HOST:.*/, "DB_HOST: \"" dbhost "\"")
|
|
}
|
|
dbhost != "" && /^[[:space:]]*db_host:[[:space:]]*/ {
|
|
sub(/db_host:.*/, "db_host: " dbhost)
|
|
}
|
|
/^[[:space:]]*ssh_host:[[:space:]]*/ { next }
|
|
/^[[:space:]]*ssh_user:[[:space:]]*/ { next }
|
|
/^[[:space:]]*ssh_key:[[:space:]]*/ { next }
|
|
/^[[:space:]]*MARKET_BOT_ENABLED:[[:space:]]*/ {
|
|
sub(/MARKET_BOT_ENABLED:.*/, "MARKET_BOT_ENABLED: \"true\"")
|
|
}
|
|
/^[[:space:]]*market_bot_enabled:[[:space:]]*/ {
|
|
sub(/market_bot_enabled:.*/, "market_bot_enabled: true")
|
|
}
|
|
/^[[:space:]]*market_bot_item_data:[[:space:]]*/ {
|
|
sub(/market_bot_item_data:.*/, "market_bot_item_data: /app/item-data.json")
|
|
}
|
|
/^[[:space:]]*market_bot_cache_db:[[:space:]]*/ {
|
|
sub(/market_bot_cache_db:.*/, "market_bot_cache_db: /data/market-bot-cache.db")
|
|
}
|
|
{ print }
|
|
END {
|
|
if (image_changed == 0) {
|
|
print "No image: field found to patch in manifest" > "/dev/stderr"
|
|
exit 1
|
|
}
|
|
}
|
|
' "$MANIFEST" > "${MANIFEST}.tmp"
|
|
mv "${MANIFEST}.tmp" "$MANIFEST"
|
|
|
|
echo "Applying manifest..."
|
|
kubectl apply -f "$MANIFEST"
|
|
if [[ -n "$control_ns" ]]; then
|
|
echo "Applying RBAC for in-cluster control access..."
|
|
cat <<EOF | kubectl apply -f -
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: Role
|
|
metadata:
|
|
name: dune-admin-runtime
|
|
namespace: $control_ns
|
|
rules:
|
|
- apiGroups: [""]
|
|
resources: ["pods", "pods/log", "services", "endpoints", "persistentvolumeclaims"]
|
|
verbs: ["get", "list", "watch"]
|
|
- apiGroups: [""]
|
|
resources: ["pods/exec"]
|
|
verbs: ["create", "get"]
|
|
- apiGroups: ["igw.funcom.com"]
|
|
resources: ["battlegroups", "serverstats"]
|
|
verbs: ["get", "list", "watch", "patch"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: RoleBinding
|
|
metadata:
|
|
name: dune-admin-runtime
|
|
namespace: $control_ns
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: default
|
|
namespace: $NAMESPACE
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: Role
|
|
name: dune-admin-runtime
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: Role
|
|
metadata:
|
|
name: dune-admin-operators-logs
|
|
namespace: funcom-operators
|
|
rules:
|
|
- apiGroups: [""]
|
|
resources: ["pods", "pods/log"]
|
|
verbs: ["get", "list", "watch"]
|
|
---
|
|
apiVersion: rbac.authorization.k8s.io/v1
|
|
kind: RoleBinding
|
|
metadata:
|
|
name: dune-admin-operators-logs
|
|
namespace: funcom-operators
|
|
subjects:
|
|
- kind: ServiceAccount
|
|
name: default
|
|
namespace: $NAMESPACE
|
|
roleRef:
|
|
apiGroup: rbac.authorization.k8s.io
|
|
kind: Role
|
|
name: dune-admin-operators-logs
|
|
EOF
|
|
fi
|
|
kubectl -n "$NAMESPACE" rollout restart deploy/dune-admin
|
|
kubectl -n "$NAMESPACE" rollout status deploy/dune-admin
|
|
kubectl -n "$NAMESPACE" get pods,svc
|
|
|
|
echo "Running in-cluster health checks (fast-fail, no pod waits)..."
|
|
stale_pods="$(kubectl -n "$NAMESPACE" get pods --no-headers 2>/dev/null | awk '/^curl($|-check-)/{print $1}')"
|
|
if [[ -n "$stale_pods" ]]; then
|
|
while IFS= read -r p; do
|
|
[[ -n "$p" ]] && kubectl -n "$NAMESPACE" delete pod "$p" --ignore-not-found >/dev/null 2>&1 || true
|
|
done <<< "$stale_pods"
|
|
fi
|
|
status_path="/api/v1/namespaces/$NAMESPACE/services/http:dune-admin:8080/proxy/api/v1/status"
|
|
bot_path="/api/v1/namespaces/$NAMESPACE/services/http:dune-admin:8080/proxy/api/v1/market-bot/status"
|
|
bg_path="/api/v1/namespaces/$NAMESPACE/services/http:dune-admin:8080/proxy/api/v1/battlegroup/status"
|
|
health_ok=0
|
|
last_status=""
|
|
last_bot=""
|
|
last_bg=""
|
|
for i in $(seq 1 30); do
|
|
last_status="$(kubectl --request-timeout=5s get --raw "$status_path" 2>/dev/null || true)"
|
|
last_bot="$(kubectl --request-timeout=5s get --raw "$bot_path" 2>/dev/null || true)"
|
|
last_bg="$(kubectl --request-timeout=5s get --raw "$bg_path" 2>/dev/null || true)"
|
|
if [[ -n "$last_status" ]] &&
|
|
echo "$last_bot" | grep -q "\"enabled\":true" &&
|
|
! echo "$last_bg" | grep -q "does not support GetStatus"; then
|
|
echo "$last_bot"
|
|
health_ok=1
|
|
break
|
|
fi
|
|
if (( i % 5 == 0 )); then
|
|
echo "Health check retry $i/30..."
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [[ "$health_ok" -ne 1 ]]; then
|
|
echo "health check failed: API or embedded market-bot not ready" >&2
|
|
echo "last /api/v1/status: $last_status" >&2
|
|
echo "last /api/v1/market-bot/status: $last_bot" >&2
|
|
echo "last /api/v1/battlegroup/status: $last_bg" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Deploy complete (image: $IMAGE)."
|
|
|
|
if [[ "$NO_PORT_FORWARD" -eq 0 ]]; then
|
|
echo "Opening API port-forward at http://127.0.0.1:8080 ..."
|
|
kubectl -n "$NAMESPACE" port-forward svc/dune-admin 8080:8080
|
|
else
|
|
NODE_PORT=$(kubectl get svc dune-admin -n "$NAMESPACE" -o jsonpath='{.spec.ports[0].nodePort}')
|
|
echo "Deploy complete. Access your dashboard at http://${VM_HOST}:${NODE_PORT} (or via API port-forward if enabled)."
|
|
fi
|