[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." }