docs(reference): import Dune: Awakening server-manager references
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>
This commit is contained in:
271
docs/reference-repos/icehunter/deploy.ps1
Normal file
271
docs/reference-repos/icehunter/deploy.ps1
Normal file
@@ -0,0 +1,271 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$VmUser = "dune",
|
||||
[string]$VmHost = "192.168.0.72",
|
||||
[string]$SshKeyPath = "",
|
||||
[string]$KubeconfigPath = "$HOME/.kube/dune-external.yaml",
|
||||
[string]$Image = "",
|
||||
[string]$Namespace = "dune-admin",
|
||||
[string]$Manifest = "deploy/k8s/dune-admin.rendered.yaml",
|
||||
[switch]$SkipKubeconfig,
|
||||
[switch]$SkipBuild,
|
||||
[switch]$SkipImageImport,
|
||||
[switch]$PortForward,
|
||||
[switch]$NoPortForward
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Require-Command([string]$Name) {
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Missing required command: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Invoke-Step([string]$Label, [scriptblock]$Action) {
|
||||
Write-Host "==> $Label"
|
||||
& $Action
|
||||
}
|
||||
|
||||
function Get-SshOptionArgs([string]$KeyPath) {
|
||||
$args = @(
|
||||
"-o", "PreferredAuthentications=publickey,password",
|
||||
"-o", "PubkeyAuthentication=yes",
|
||||
"-o", "PasswordAuthentication=yes"
|
||||
)
|
||||
if ($KeyPath) {
|
||||
$args += @("-i", $KeyPath, "-o", "IdentitiesOnly=yes")
|
||||
}
|
||||
return $args
|
||||
}
|
||||
|
||||
$repoRoot = Split-Path -Parent $PSCommandPath
|
||||
Set-Location $repoRoot
|
||||
|
||||
Require-Command kubectl
|
||||
Require-Command ssh
|
||||
Require-Command scp
|
||||
Require-Command docker
|
||||
Require-Command make
|
||||
|
||||
if (-not $SshKeyPath) {
|
||||
$localKey = Join-Path $repoRoot "sshKey"
|
||||
if (Test-Path $localKey) {
|
||||
$SshKeyPath = $localKey
|
||||
}
|
||||
}
|
||||
if (-not $Image) {
|
||||
if ($SkipBuild -or $SkipImageImport) {
|
||||
$Image = "dune-admin:local"
|
||||
} else {
|
||||
$Image = "dune-admin:local-$(Get-Date -Format 'yyyyMMddHHmmss')"
|
||||
}
|
||||
}
|
||||
if ($SshKeyPath -and -not (Test-Path $SshKeyPath)) {
|
||||
throw "SSH key not found: $SshKeyPath"
|
||||
}
|
||||
if ($SshKeyPath) {
|
||||
Write-Host "Using SSH key: $SshKeyPath (fallback to password enabled)"
|
||||
} else {
|
||||
Write-Host "No SSH key provided/found; using password auth (or agent) for SSH."
|
||||
}
|
||||
$sshOpts = Get-SshOptionArgs -KeyPath $SshKeyPath
|
||||
|
||||
if (-not $SkipKubeconfig) {
|
||||
Invoke-Step "Pulling kubeconfig from $VmUser@$VmHost" {
|
||||
$dir = Split-Path -Parent $KubeconfigPath
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -Path $dir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
& ssh @sshOpts "$VmUser@$VmHost" "sudo cat /etc/rancher/k3s/k3s.yaml" | Out-File -FilePath $KubeconfigPath -Encoding utf8NoBOM
|
||||
$raw = Get-Content -Path $KubeconfigPath -Raw
|
||||
$raw.Replace("127.0.0.1", $VmHost) | Set-Content -Path $KubeconfigPath -Encoding utf8NoBOM
|
||||
}
|
||||
}
|
||||
|
||||
$env:KUBECONFIG = $KubeconfigPath
|
||||
Write-Host "Using KUBECONFIG=$($env:KUBECONFIG)"
|
||||
kubectl get nodes
|
||||
|
||||
if (-not $SkipBuild) {
|
||||
Invoke-Step "Building image $Image" {
|
||||
$AppVersion = if (Test-Path "$PSScriptRoot/VERSION") { Get-Content "$PSScriptRoot/VERSION" -Raw } else { "unknown" }
|
||||
$AppVersion = $AppVersion.Trim()
|
||||
$GitCommitRaw = git -C $PSScriptRoot rev-parse --short HEAD 2>$null
|
||||
$GitCommit = if ($GitCommitRaw) { $GitCommitRaw } else { "unknown" }
|
||||
$BuildTime = [System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
docker buildx build --platform linux/amd64 -f deploy/Dockerfile `
|
||||
--build-arg APP_VERSION=$AppVersion `
|
||||
--build-arg GIT_COMMIT=$GitCommit `
|
||||
--build-arg BUILD_TIME=$BuildTime `
|
||||
-t $Image --load .
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $SkipImageImport) {
|
||||
Invoke-Step "Importing image into k3s runtime on $VmHost" {
|
||||
$tmpTar = Join-Path $env:TEMP "dune-admin-image.tar"
|
||||
if (Test-Path $tmpTar) { Remove-Item $tmpTar -Force }
|
||||
docker save -o $tmpTar $Image
|
||||
& scp @sshOpts $tmpTar "$VmUser@${VmHost}:/tmp/dune-admin-image.tar"
|
||||
& ssh @sshOpts "$VmUser@$VmHost" "sudo k3s ctr images import /tmp/dune-admin-image.tar && rm -f /tmp/dune-admin-image.tar"
|
||||
Remove-Item $tmpTar -Force
|
||||
}
|
||||
}
|
||||
|
||||
Invoke-Step "Rendering manifest" {
|
||||
make render-k8s
|
||||
}
|
||||
|
||||
if (-not (Test-Path $Manifest)) {
|
||||
throw "Manifest not found: $Manifest"
|
||||
}
|
||||
|
||||
$manifestText = Get-Content -Path $Manifest -Raw
|
||||
# Patch all image: fields that reference a dune-admin image (main container
|
||||
# and seed-binary init container both need the same locally-built image tag).
|
||||
$patched = [regex]::Replace($manifestText, '(?m)^(\s*image:\s*)(?:ghcr\.io/icehunter/dune-admin|dune-admin)\S*$', "`$1$Image")
|
||||
if ($patched -eq $manifestText) {
|
||||
throw "No dune-admin image: field found to patch in manifest"
|
||||
}
|
||||
|
||||
$dbHostOverride = ""
|
||||
$controlNsMatch = [regex]::Match($patched, '(?m)^\s*control_namespace:\s*"?([^\s"]+)"?\s*$')
|
||||
if ($controlNsMatch.Success) {
|
||||
$controlNs = $controlNsMatch.Groups[1].Value
|
||||
$svcRows = kubectl -n $controlNs get svc -o jsonpath='{range .items[*]}{.metadata.name}{"`t"}{range .spec.ports[*]}{.port}{" "}{end}{"`n"}{end}' 2>$null
|
||||
$dbSvcRow = $svcRows | Where-Object { $_ -match '(^|[ \t])15432([ \t]|$)' } | Select-Object -First 1
|
||||
if ($dbSvcRow) {
|
||||
$dbSvc = ($dbSvcRow -split '\s+')[0]
|
||||
if ($dbSvc) {
|
||||
$dbHostOverride = "$dbSvc.$controlNs.svc.cluster.local"
|
||||
Write-Host "Using in-cluster DB host: $dbHostOverride"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*CONTROL:\s*).*$', '${1}"local"')
|
||||
$patched = [regex]::Replace($patched, '(?m)^\s*cmd_(status|start|stop|restart):\s*.*\r?\n', '')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*control:\s*).*$', '${1}local')
|
||||
if ($dbHostOverride) {
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*DB_HOST:\s*).*$', '${1}"' + $dbHostOverride + '"')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*db_host:\s*).*$', '${1}' + $dbHostOverride)
|
||||
}
|
||||
$patched = [regex]::Replace($patched, '(?m)^\s*ssh_host:\s*.*\r?\n', '')
|
||||
$patched = [regex]::Replace($patched, '(?m)^\s*ssh_user:\s*.*\r?\n', '')
|
||||
$patched = [regex]::Replace($patched, '(?m)^\s*ssh_key:\s*.*\r?\n', '')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*MARKET_BOT_ENABLED:\s*).*$', '${1}"true"')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*market_bot_enabled:\s*).*$', '${1}true')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*market_bot_item_data:\s*).*$', '${1}/app/item-data.json')
|
||||
$patched = [regex]::Replace($patched, '(?m)^(\s*market_bot_cache_db:\s*).*$', '${1}/data/market-bot-cache.db')
|
||||
$patched | Set-Content -Path $Manifest -Encoding utf8NoBOM
|
||||
|
||||
Invoke-Step "Applying manifest" {
|
||||
kubectl apply -f $Manifest
|
||||
if ($controlNsMatch.Success) {
|
||||
$rbac = @"
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: dune-admin-runtime
|
||||
namespace: $controlNs
|
||||
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: $controlNs
|
||||
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
|
||||
"@
|
||||
$rbac | kubectl apply -f -
|
||||
}
|
||||
kubectl -n $Namespace rollout restart deploy/dune-admin
|
||||
kubectl -n $Namespace rollout status deploy/dune-admin
|
||||
kubectl -n $Namespace get pods,svc
|
||||
}
|
||||
|
||||
Invoke-Step "In-cluster health checks (fast-fail, no pod waits)" {
|
||||
$stalePods = kubectl -n $Namespace get pods --no-headers 2>$null | `
|
||||
Where-Object { $_ -match '^(curl|curl-check-)' } | `
|
||||
ForEach-Object { ($_ -split '\s+')[0] }
|
||||
foreach ($p in $stalePods) {
|
||||
if ($p) { kubectl -n $Namespace delete pod $p --ignore-not-found | Out-Null }
|
||||
}
|
||||
|
||||
$statusPath = "/api/v1/namespaces/$Namespace/services/http:dune-admin:8080/proxy/api/v1/status"
|
||||
$botPath = "/api/v1/namespaces/$Namespace/services/http:dune-admin:8080/proxy/api/v1/market-bot/status"
|
||||
$bgPath = "/api/v1/namespaces/$Namespace/services/http:dune-admin:8080/proxy/api/v1/battlegroup/status"
|
||||
|
||||
$healthOk = $false
|
||||
$lastStatus = ""
|
||||
$lastBot = ""
|
||||
$lastBg = ""
|
||||
for ($i = 1; $i -le 30; $i++) {
|
||||
$lastStatus = (kubectl --request-timeout=5s get --raw $statusPath 2>$null)
|
||||
$lastBot = (kubectl --request-timeout=5s get --raw $botPath 2>$null)
|
||||
$lastBg = (kubectl --request-timeout=5s get --raw $bgPath 2>$null)
|
||||
|
||||
$botOk = $lastBot -match '"enabled":true'
|
||||
$bgOk = $lastBg -notmatch "does not support GetStatus"
|
||||
if ($lastStatus -and $botOk -and $bgOk) {
|
||||
Write-Host $lastBot
|
||||
$healthOk = $true
|
||||
break
|
||||
}
|
||||
if (($i % 5) -eq 0) {
|
||||
Write-Host "Health check retry $i/30..."
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if (-not $healthOk) {
|
||||
throw "health check failed: API or embedded market-bot not ready`nlast /api/v1/status: $lastStatus`nlast /api/v1/market-bot/status: $lastBot`nlast /api/v1/battlegroup/status: $lastBg"
|
||||
}
|
||||
}
|
||||
|
||||
if ($PortForward -and -not $NoPortForward) {
|
||||
Write-Host "Opening API port-forward at http://127.0.0.1:8080 ..."
|
||||
kubectl -n $Namespace port-forward svc/dune-admin 8080:8080
|
||||
} else {
|
||||
Write-Host "Deploy complete. Run ./listen.ps1 to open API port-forward."
|
||||
}
|
||||
Reference in New Issue
Block a user