Panel — public source drop (v0.9.0)
Self-hostable game-server control panel: controller + agent + 26 game modules. One-line install (prebuilt release, no Go required): curl -fsSL https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.sh | sudo bash Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+273
@@ -0,0 +1,273 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Panel one-line installer for Windows — controller + agent + Postgres on one box.
|
||||
|
||||
.DESCRIPTION
|
||||
PowerShell 5+ equivalent of install.sh. Checks Docker Desktop, starts a
|
||||
panel-postgres container, builds (or downloads) the panel binaries with the
|
||||
version baked in via ldflags, boots the controller once to bootstrap the CA
|
||||
and admin account, pairs the local agent, and registers both as logon-time
|
||||
Scheduled Tasks so they survive reboots.
|
||||
|
||||
For a hardened always-on Windows service, wrap the same ExecStart lines with
|
||||
nssm (https://nssm.cc): `nssm install panel-controller <InstallDir>\bin\controller.exe ...`.
|
||||
|
||||
.EXAMPLE
|
||||
powershell -ExecutionPolicy Bypass -File install.ps1 -AdminPassword secret123
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$RepoUrl = $(if ($env:PANEL_REPO_URL) { $env:PANEL_REPO_URL } else { 'https://git.pdxtechs.com/dbledeez/panel' }),
|
||||
# Defaults to the published v0.9.0 windows/amd64 prebuilt zip so the installer
|
||||
# needs no Go toolchain. Pass -PrebuiltUrl '' (empty) to force a source build.
|
||||
# A local checkout (running install.ps1 from inside the repo) wins over this.
|
||||
[string]$PrebuiltUrl = $(if ($null -ne $env:PANEL_PREBUILT_URL) { $env:PANEL_PREBUILT_URL } else { 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.0/panel-v0.9.0-windows-amd64.zip' }),
|
||||
[string]$Version = $env:PANEL_VERSION,
|
||||
[string]$InstallDir = $(if ($env:PANEL_DIR) { $env:PANEL_DIR } else { 'C:\Panel' }),
|
||||
[int] $HttpPort = $(if ($env:PANEL_HTTP_PORT) { [int]$env:PANEL_HTTP_PORT } else { 8080 }),
|
||||
[int] $GrpcPort = $(if ($env:PANEL_GRPC_PORT) { [int]$env:PANEL_GRPC_PORT } else { 8443 }),
|
||||
[string]$AdminPassword = $env:PANEL_ADMIN_PASSWORD,
|
||||
[string]$DbUrl = $env:PANEL_DB_URL,
|
||||
[string]$DbPassword = $env:PANEL_DB_PASSWORD,
|
||||
[string]$DbContainer = $(if ($env:PANEL_DB_CONTAINER) { $env:PANEL_DB_CONTAINER } else { 'panel-postgres' }),
|
||||
[int] $DbPort = $(if ($env:PANEL_DB_PORT) { [int]$env:PANEL_DB_PORT } else { 5432 }),
|
||||
[string]$DbVolume = $(if ($env:PANEL_DB_VOLUME) { $env:PANEL_DB_VOLUME } else { 'panel_pgdata' }),
|
||||
[switch]$SkipAgent
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
# Run two independent panels on one host by giving each install distinct
|
||||
# -InstallDir, -HttpPort, -GrpcPort, -DbContainer, -DbPort and -DbVolume.
|
||||
$PgContainer = $DbContainer
|
||||
|
||||
function Log($msg) { Write-Host "[panel] $msg" -ForegroundColor Green }
|
||||
function Warn($msg) { Write-Host "[panel] $msg" -ForegroundColor Yellow }
|
||||
function Fail($msg) { Write-Host "[panel] ERROR: $msg" -ForegroundColor Red; exit 1 }
|
||||
|
||||
# ---- 1. Docker Desktop -----------------------------------------------------
|
||||
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
|
||||
Fail "Docker not found. Install Docker Desktop (https://www.docker.com/products/docker-desktop/), start it, then re-run."
|
||||
}
|
||||
docker info *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "Docker daemon is not running. Start Docker Desktop and re-run."
|
||||
}
|
||||
Log "docker present: $(docker --version)"
|
||||
|
||||
# ---- 2. Postgres -----------------------------------------------------------
|
||||
New-Item -ItemType Directory -Force -Path "$InstallDir\bin", "$InstallDir\data" | Out-Null
|
||||
$EnvFile = Join-Path $InstallDir 'panel.env'
|
||||
|
||||
if ($DbUrl) {
|
||||
Log "using external Postgres from -DbUrl"
|
||||
} else {
|
||||
docker inspect $PgContainer *> $null
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Log "postgres container '$PgContainer' already exists - starting"
|
||||
docker start $PgContainer | Out-Null
|
||||
if (-not $DbPassword -and (Test-Path $EnvFile)) {
|
||||
$DbPassword = (Get-Content $EnvFile | Where-Object { $_ -match '^PANEL_DB_PASSWORD=' } |
|
||||
Select-Object -First 1) -replace '^PANEL_DB_PASSWORD=', ''
|
||||
}
|
||||
if (-not $DbPassword) { Fail "existing $PgContainer but no DbPassword known (pass -DbPassword, or 'docker rm -f $PgContainer' to start fresh)" }
|
||||
} else {
|
||||
if (-not $DbPassword) {
|
||||
$DbPassword = -join ((1..32) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
|
||||
}
|
||||
Log "starting postgres container '$PgContainer' on 127.0.0.1:$DbPort (volume $DbVolume)"
|
||||
docker run -d --name $PgContainer --restart unless-stopped `
|
||||
-e POSTGRES_USER=panel -e "POSTGRES_PASSWORD=$DbPassword" -e POSTGRES_DB=panel `
|
||||
-p "127.0.0.1:${DbPort}:5432" `
|
||||
-v "${DbVolume}:/var/lib/postgresql/data" `
|
||||
postgres:17-alpine | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { Fail "docker run for postgres failed" }
|
||||
}
|
||||
$DbUrl = "postgres://panel:$DbPassword@127.0.0.1:$DbPort/panel?sslmode=disable"
|
||||
Log "waiting for postgres"
|
||||
$ready = $false
|
||||
foreach ($i in 1..30) {
|
||||
docker exec $PgContainer pg_isready -U panel -d panel *> $null
|
||||
if ($LASTEXITCODE -eq 0) { $ready = $true; break }
|
||||
Start-Sleep -Seconds 1
|
||||
}
|
||||
if (-not $ready) { Fail "postgres did not become ready" }
|
||||
}
|
||||
|
||||
# ---- 3. Source / binaries ---------------------------------------------------
|
||||
$Src = $null
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
if ($ScriptDir -and (Test-Path (Join-Path $ScriptDir 'go.work')) -and (Test-Path (Join-Path $ScriptDir 'controller'))) {
|
||||
$Src = $ScriptDir
|
||||
Log "using local checkout at $Src"
|
||||
# Running from inside the repo means build THIS tree. Don't let the prebuilt
|
||||
# default override it. An explicit -PrebuiltUrl or $env:PANEL_PREBUILT_URL still
|
||||
# forces prebuilt (it won't equal the untouched default / the env is non-null).
|
||||
$prebuiltDefault = 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.0/panel-v0.9.0-windows-amd64.zip'
|
||||
if (($null -eq $env:PANEL_PREBUILT_URL) -and ($PrebuiltUrl -eq $prebuiltDefault)) {
|
||||
$PrebuiltUrl = ''
|
||||
}
|
||||
}
|
||||
|
||||
if ($PrebuiltUrl) {
|
||||
Log "downloading prebuilt binaries: $PrebuiltUrl"
|
||||
$zip = Join-Path $env:TEMP 'panel-prebuilt.zip'
|
||||
Invoke-WebRequest -Uri $PrebuiltUrl -OutFile $zip -UseBasicParsing
|
||||
Expand-Archive -Path $zip -DestinationPath $InstallDir -Force
|
||||
Remove-Item $zip -Force
|
||||
if (-not (Test-Path "$InstallDir\bin\controller.exe")) { Fail "prebuilt archive did not contain bin\controller.exe" }
|
||||
} else {
|
||||
if (-not $Src) {
|
||||
if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Fail "git required to fetch sources (or pass -PrebuiltUrl)" }
|
||||
$Src = Join-Path $InstallDir 'src'
|
||||
if (Test-Path (Join-Path $Src '.git')) {
|
||||
Log "updating source at $Src"
|
||||
git -C $Src pull --ff-only
|
||||
} else {
|
||||
Log "cloning $RepoUrl"
|
||||
git clone --depth 1 $RepoUrl $Src
|
||||
}
|
||||
}
|
||||
if (-not (Get-Command go -ErrorAction SilentlyContinue)) { Fail "Go toolchain not found. Install Go >= 1.26 (https://go.dev/dl/) or pass -PrebuiltUrl." }
|
||||
if (-not $Version) {
|
||||
$Version = (git -C $Src describe --tags --always --dirty 2>$null)
|
||||
if (-not $Version) { $Version = 'dev' }
|
||||
}
|
||||
$ldflags = "-s -w -X github.com/dbledeez/panel/pkg/version.Version=$Version"
|
||||
Log "building panel $Version"
|
||||
Push-Location $Src
|
||||
try {
|
||||
$env:CGO_ENABLED = '0'
|
||||
go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\controller.exe" ./controller/cmd/controller
|
||||
if ($LASTEXITCODE -ne 0) { Fail 'controller build failed' }
|
||||
go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\agent.exe" ./agent/cmd/agent
|
||||
if ($LASTEXITCODE -ne 0) { Fail 'agent build failed' }
|
||||
go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\panelctl.exe" ./controller/cmd/panelctl
|
||||
if ($LASTEXITCODE -ne 0) { Fail 'panelctl build failed' }
|
||||
} finally { Pop-Location }
|
||||
Log "syncing game modules"
|
||||
if (Test-Path "$InstallDir\modules") { Remove-Item "$InstallDir\modules" -Recurse -Force }
|
||||
Copy-Item -Recurse "$Src\modules" "$InstallDir\modules"
|
||||
}
|
||||
|
||||
# ---- 4. Env file ------------------------------------------------------------
|
||||
$envLines = @("PANEL_DB_URL=$DbUrl")
|
||||
if ($DbPassword) { $envLines += "PANEL_DB_PASSWORD=$DbPassword" }
|
||||
if ($AdminPassword) { $envLines += "PANEL_ADMIN_PASSWORD=$AdminPassword" }
|
||||
$envLines += "PANEL_HTTP_PORT=$HttpPort"
|
||||
$envLines += "PANEL_GRPC_PORT=$GrpcPort"
|
||||
Set-Content -Path $EnvFile -Value $envLines -Encoding ascii
|
||||
|
||||
# ---- 5. Start controller (background) + bootstrap ---------------------------
|
||||
$ctrlLog = Join-Path $InstallDir 'controller.log'
|
||||
$firstBoot = -not (Test-Path "$InstallDir\data\ca")
|
||||
|
||||
# Stop a previous run of this script's processes, if any.
|
||||
Get-Process -Name controller, agent -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Path -like "$InstallDir*" } | Stop-Process -Force -ErrorAction SilentlyContinue
|
||||
|
||||
$env:PANEL_DB_URL = $DbUrl
|
||||
if ($AdminPassword) { $env:PANEL_ADMIN_PASSWORD = $AdminPassword }
|
||||
Log "starting controller"
|
||||
Start-Process -FilePath "$InstallDir\bin\controller.exe" `
|
||||
-ArgumentList @('-http', ":$HttpPort", '-grpc', ":$GrpcPort", '-ca-dir', './data/ca') `
|
||||
-WorkingDirectory $InstallDir -WindowStyle Hidden `
|
||||
-RedirectStandardError $ctrlLog
|
||||
|
||||
$up = $false
|
||||
foreach ($i in 1..60) {
|
||||
try {
|
||||
Invoke-WebRequest -Uri "http://127.0.0.1:$HttpPort/login" -UseBasicParsing -TimeoutSec 2 | Out-Null
|
||||
$up = $true; break
|
||||
} catch { Start-Sleep -Seconds 1 }
|
||||
}
|
||||
if (-not $up) { Fail "controller did not come up - see $ctrlLog" }
|
||||
|
||||
$adminEmail = 'admin@panel.local'
|
||||
# BUG-5b: always give the operator a path to the password.
|
||||
$adminShown = "(the one you set via -AdminPassword; also in $EnvFile)"
|
||||
if (-not $AdminPassword) {
|
||||
if ($firstBoot) {
|
||||
$m = Select-String -Path $ctrlLog -Pattern 'password: ([a-f0-9]{16})' | Select-Object -First 1
|
||||
if ($m) {
|
||||
$AdminPassword = $m.Matches[0].Groups[1].Value
|
||||
$adminShown = $AdminPassword
|
||||
} else {
|
||||
$adminShown = "(see $ctrlLog, line 'password:')"
|
||||
}
|
||||
} else {
|
||||
$adminShown = '(unchanged from previous install)'
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 6. Pair token + local agent ---------------------------------------------
|
||||
$pairCmd = "$InstallDir\bin\agent.exe --pair-url http://<controller-host>:$HttpPort --pair-token <token-from-dashboard> --cert-dir ./data/certs"
|
||||
if (-not $SkipAgent) {
|
||||
$paired = Test-Path "$InstallDir\data\certs\agent.crt"
|
||||
if (-not $paired -and $AdminPassword) {
|
||||
try {
|
||||
$session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
|
||||
$body = @{ email = $adminEmail; password = $AdminPassword } | ConvertTo-Json
|
||||
Invoke-RestMethod -Uri "http://127.0.0.1:$HttpPort/api/login" -Method Post `
|
||||
-ContentType 'application/json' -Body $body -WebSession $session | Out-Null
|
||||
$tokBody = @{ description = 'install.ps1 local agent'; expires_minutes = 60 } | ConvertTo-Json
|
||||
$tok = Invoke-RestMethod -Uri "http://127.0.0.1:$HttpPort/api/admin/pair-tokens" -Method Post `
|
||||
-ContentType 'application/json' -Body $tokBody -WebSession $session
|
||||
Log "pairing local agent"
|
||||
Push-Location $InstallDir
|
||||
try {
|
||||
& "$InstallDir\bin\agent.exe" --pair-url "http://127.0.0.1:$HttpPort" --pair-token $tok.token --cert-dir ./data/certs
|
||||
if ($LASTEXITCODE -eq 0) { $paired = $true } else { Warn 'agent pairing exited non-zero' }
|
||||
} finally { Pop-Location }
|
||||
} catch {
|
||||
Warn "auto-pairing failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
if ($paired) {
|
||||
Log "starting agent"
|
||||
Start-Process -FilePath "$InstallDir\bin\agent.exe" `
|
||||
-ArgumentList @('--controller', "localhost:$GrpcPort", '--modules-dir', './modules',
|
||||
'--data-root', './data/instances', '--meta-dir', './data/instance-meta',
|
||||
'--backup-dir', './data/backups', '--cert-dir', './data/certs') `
|
||||
-WorkingDirectory $InstallDir -WindowStyle Hidden `
|
||||
-RedirectStandardError (Join-Path $InstallDir 'agent.log')
|
||||
} else {
|
||||
Warn "agent not paired. Create a pair token in the dashboard (Admin -> Pair tokens) and run:"
|
||||
Warn " cd $InstallDir; $pairCmd"
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 7. Scheduled tasks (survive reboots) -------------------------------------
|
||||
Log "registering logon Scheduled Tasks (panel-controller / panel-agent)"
|
||||
try {
|
||||
$ctrlAction = New-ScheduledTaskAction -Execute "$InstallDir\bin\controller.exe" `
|
||||
-Argument "-http :$HttpPort -grpc :$GrpcPort -ca-dir ./data/ca" -WorkingDirectory $InstallDir
|
||||
$agentAction = New-ScheduledTaskAction -Execute "$InstallDir\bin\agent.exe" `
|
||||
-Argument "--controller localhost:$GrpcPort --modules-dir ./modules --data-root ./data/instances --meta-dir ./data/instance-meta --backup-dir ./data/backups --cert-dir ./data/certs" `
|
||||
-WorkingDirectory $InstallDir
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Days 3650) -StartWhenAvailable
|
||||
Register-ScheduledTask -TaskName 'panel-controller' -Action $ctrlAction -Trigger $trigger -Settings $settings -Force | Out-Null
|
||||
Register-ScheduledTask -TaskName 'panel-agent' -Action $agentAction -Trigger $trigger -Settings $settings -Force | Out-Null
|
||||
Warn "NOTE: the scheduled tasks read PANEL_DB_URL etc. from the machine environment;"
|
||||
Warn "for a true always-on service (no logon needed), use nssm: https://nssm.cc"
|
||||
# Persist the DB URL machine-wide so the scheduled-task runs see it.
|
||||
[Environment]::SetEnvironmentVariable('PANEL_DB_URL', $DbUrl, 'Machine')
|
||||
} catch {
|
||||
Warn "scheduled-task registration failed (not admin?): $($_.Exception.Message)"
|
||||
Warn "the controller/agent started by this script keep running until reboot."
|
||||
}
|
||||
|
||||
# ---- 8. Summary ----------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Log "------------------------------------------------------------"
|
||||
Log " Panel is installed."
|
||||
Log " Dashboard : http://localhost:$HttpPort"
|
||||
Log " Login : $adminEmail"
|
||||
Log " Password : $adminShown"
|
||||
Log " (log in once and change the password in the UI)"
|
||||
Log ""
|
||||
Log " To pair an agent on ANOTHER machine, create a pair token in the"
|
||||
Log " dashboard, copy the panel binaries + modules\ there, then run:"
|
||||
Log " $pairCmd"
|
||||
Log " and start it: .\bin\agent.exe --controller <this-host>:$GrpcPort --cert-dir ./data/certs"
|
||||
Log "------------------------------------------------------------"
|
||||
Reference in New Issue
Block a user