Enforce coherent Idunn swarm deployments
This commit is contained in:
parent
8d110c3f4b
commit
dccac65ea7
12 changed files with 200 additions and 23 deletions
|
|
@ -53,6 +53,11 @@ Idunn now shares Odin's Rust body:
|
|||
through `health-muninn.cmd` and `restart-muninn.cmd`, and Nightwing Gjallar
|
||||
through `health-nightwing-gjallar.cmd`,
|
||||
`deploy-nightwing-gjallar.cmd`, and `restart-nightwing-gjallar.cmd`.
|
||||
- `scripts/deploy-yggdrasil-source-app.ps1` and
|
||||
`scripts/health-yggdrasil-source-app.ps1` are the generic Yggdrasil source
|
||||
artifact lane. They package committed local `HEAD` with `git archive`, run
|
||||
the existing ops-owned deploy/check scripts on Yggdrasil, and stamp a remote
|
||||
`gamecult.idunn.deployment_manifest.v1` only after the remote check passes.
|
||||
- `scripts/idunn-deployment-targets.ps1` is the current swarm deployment target
|
||||
catalog. Every known deployable target is either `enforced`, `blocked`,
|
||||
`external-owned`, or `not-runtime` with an explicit reason.
|
||||
|
|
@ -137,6 +142,13 @@ idunn.operator_alarm.v1
|
|||
deploy command must remain `blocked` or `external-owned` with the missing
|
||||
authority named until a wrapper can produce a deployment manifest and route
|
||||
through Idunn's typed deployment request/result path.
|
||||
- Yggdrasil Heimdall, repixelizer, and StreamPixels are enforced through the
|
||||
generic source artifact lane and their existing `gamecult-ops` runbooks.
|
||||
Bifrost is explicitly blocked as of 2026-06-09: committed Bifrost `HEAD`
|
||||
expects `UserAccounts.HeimdallAccountId`, while Yggdrasil's production
|
||||
database lacks that column and EF reports no pending migration. Idunn must
|
||||
not claim Bifrost deployment freshness until Bifrost owns that schema
|
||||
migration path.
|
||||
|
||||
## First Runtime Direction
|
||||
|
||||
|
|
|
|||
2
scripts/deploy-yggdrasil-heimdall.cmd
Normal file
2
scripts/deploy-yggdrasil-heimdall.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-heimdall -RepoRoot E:\Projects\Heimdall -AppUser heimdall -RemoteAppHome /srv/heimdall -RemoteTarballName heimdall-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-heimdall.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-heimdall.sh
|
||||
2
scripts/deploy-yggdrasil-repixelizer.cmd
Normal file
2
scripts/deploy-yggdrasil-repixelizer.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-repixelizer -RepoRoot E:\Projects\repixelizer -AppUser repixelizer -RemoteAppHome /srv/repixelizer -RemoteTarballName repixelizer-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-repixelizer-gui.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-repixelizer-gui.sh
|
||||
84
scripts/deploy-yggdrasil-source-app.ps1
Normal file
84
scripts/deploy-yggdrasil-source-app.ps1
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $AppId,
|
||||
[Parameter(Mandatory = $true)] [string] $RepoRoot,
|
||||
[Parameter(Mandatory = $true)] [string] $AppUser,
|
||||
[Parameter(Mandatory = $true)] [string] $RemoteAppHome,
|
||||
[Parameter(Mandatory = $true)] [string] $RemoteTarballName,
|
||||
[Parameter(Mandatory = $true)] [string] $DeployScript,
|
||||
[Parameter(Mandatory = $true)] [string] $CheckScript,
|
||||
[string] $SshTarget = "yggdrasil"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $RepoRoot)) {
|
||||
throw "Repo root not found: $RepoRoot"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $DeployScript)) {
|
||||
throw "Deploy script not found: $DeployScript"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $CheckScript)) {
|
||||
throw "Check script not found: $CheckScript"
|
||||
}
|
||||
|
||||
$commit = (git -C $RepoRoot rev-parse HEAD).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($commit)) {
|
||||
throw "Could not determine git revision for $RepoRoot"
|
||||
}
|
||||
|
||||
$scratch = Join-Path "E:\Projects\Odin\scratch\idunn-deploy" $AppId
|
||||
$tarPath = Join-Path $scratch $RemoteTarballName
|
||||
$manifestPath = Join-Path $scratch "deployment-manifest.txt"
|
||||
New-Item -ItemType Directory -Force -Path $scratch | Out-Null
|
||||
|
||||
git -C $RepoRoot archive --format=tar --output=$tarPath HEAD
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "git archive failed for $RepoRoot"
|
||||
}
|
||||
|
||||
$hash = (Get-FileHash $tarPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
$deployedAt = [DateTimeOffset]::UtcNow.ToString("O")
|
||||
@(
|
||||
"schema=gamecult.idunn.deployment_manifest.v1"
|
||||
"appId=$AppId"
|
||||
"repoRoot=$RepoRoot"
|
||||
"gitCommit=$commit"
|
||||
"artifact=$RemoteTarballName"
|
||||
"sha256=$hash"
|
||||
"deployedAtUtc=$deployedAt"
|
||||
) | Set-Content -Encoding ASCII -LiteralPath $manifestPath
|
||||
|
||||
$remoteDeploy = "/home/gamecultadmin/$([IO.Path]::GetFileName($DeployScript))"
|
||||
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
|
||||
$remoteTarball = "/home/gamecultadmin/$RemoteTarballName"
|
||||
$remoteManifest = "/home/gamecultadmin/$AppId-deployment-manifest.txt"
|
||||
$targetTarball = "$RemoteAppHome/$RemoteTarballName"
|
||||
$targetManifest = "$RemoteAppHome/deployment-manifest.txt"
|
||||
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget)
|
||||
$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10")
|
||||
|
||||
scp.exe @scpArgs $tarPath "${SshTarget}:$remoteTarball"
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp source tarball failed for $AppId" }
|
||||
scp.exe @scpArgs $DeployScript "${SshTarget}:$remoteDeploy"
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp deploy script failed for $AppId" }
|
||||
scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck"
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" }
|
||||
scp.exe @scpArgs $manifestPath "${SshTarget}:$remoteManifest"
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp manifest failed for $AppId" }
|
||||
|
||||
$remoteCommand = @(
|
||||
"sudo -n install -d -o '$AppUser' -g '$AppUser' '$RemoteAppHome'",
|
||||
"sudo -n install -o '$AppUser' -g '$AppUser' -m 600 '$remoteTarball' '$targetTarball'",
|
||||
"chmod +x '$remoteDeploy' '$remoteCheck'",
|
||||
"sudo -n bash '$remoteDeploy'",
|
||||
"check_ok=0; for i in `$(seq 1 30); do if sudo -n bash '$remoteCheck'; then check_ok=1; break; fi; sleep 2; done; if [ `$check_ok -ne 1 ]; then sudo -n bash '$remoteCheck'; fi",
|
||||
"sudo -n install -m 644 '$remoteManifest' '$targetManifest'"
|
||||
) -join " && "
|
||||
|
||||
ssh.exe @sshArgs $remoteCommand
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "remote deploy/check failed for $AppId"
|
||||
}
|
||||
|
||||
Write-Host "$AppId deployed to Yggdrasil at $commit."
|
||||
Write-Host " artifactSha256=$hash"
|
||||
2
scripts/deploy-yggdrasil-streampixels.cmd
Normal file
2
scripts/deploy-yggdrasil-streampixels.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\deploy-yggdrasil-source-app.ps1 -AppId yggdrasil-streampixels -RepoRoot E:\Projects\StreamPixels -AppUser streampixels -RemoteAppHome /srv/streampixels -RemoteTarballName streampixels-source.tar -DeployScript E:\Projects\gamecult-ops\scripts\deploy-streampixels-preview.sh -CheckScript E:\Projects\gamecult-ops\scripts\check-streampixels-preview.sh
|
||||
2
scripts/health-yggdrasil-heimdall.cmd
Normal file
2
scripts/health-yggdrasil-heimdall.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-heimdall -RepoRoot E:\Projects\Heimdall -RemoteAppHome /srv/heimdall -CheckScript E:\Projects\gamecult-ops\scripts\check-heimdall.sh
|
||||
2
scripts/health-yggdrasil-repixelizer.cmd
Normal file
2
scripts/health-yggdrasil-repixelizer.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-repixelizer -RepoRoot E:\Projects\repixelizer -RemoteAppHome /srv/repixelizer -CheckScript E:\Projects\gamecult-ops\scripts\check-repixelizer-gui.sh
|
||||
43
scripts/health-yggdrasil-source-app.ps1
Normal file
43
scripts/health-yggdrasil-source-app.ps1
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
param(
|
||||
[Parameter(Mandatory = $true)] [string] $AppId,
|
||||
[Parameter(Mandatory = $true)] [string] $RepoRoot,
|
||||
[Parameter(Mandatory = $true)] [string] $RemoteAppHome,
|
||||
[Parameter(Mandatory = $true)] [string] $CheckScript,
|
||||
[string] $SshTarget = "yggdrasil"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$localCommit = (git -C $RepoRoot rev-parse HEAD).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($localCommit)) {
|
||||
throw "Could not determine git revision for $RepoRoot"
|
||||
}
|
||||
|
||||
$remoteCheck = "/home/gamecultadmin/$([IO.Path]::GetFileName($CheckScript))"
|
||||
$scpArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10")
|
||||
$sshArgs = @("-o", "BatchMode=yes", "-o", "ConnectTimeout=10", $SshTarget)
|
||||
scp.exe @scpArgs $CheckScript "${SshTarget}:$remoteCheck" | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) { throw "scp check script failed for $AppId" }
|
||||
|
||||
$manifest = ssh.exe @sshArgs "cat '$RemoteAppHome/deployment-manifest.txt' 2>/dev/null"
|
||||
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($manifest)) {
|
||||
throw "$AppId deploy manifest is missing on Yggdrasil."
|
||||
}
|
||||
|
||||
$remoteCommit = ""
|
||||
foreach ($line in $manifest) {
|
||||
if ($line -like "gitCommit=*") {
|
||||
$remoteCommit = $line.Substring("gitCommit=".Length).Trim()
|
||||
}
|
||||
}
|
||||
|
||||
if ($remoteCommit -ne $localCommit) {
|
||||
throw "$AppId deploy is stale. remote=$remoteCommit local=$localCommit"
|
||||
}
|
||||
|
||||
ssh.exe @sshArgs "chmod +x '$remoteCheck' && sudo -n bash '$remoteCheck'"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "$AppId remote health check failed."
|
||||
}
|
||||
|
||||
Write-Host "$AppId is active and deployed at $localCommit."
|
||||
2
scripts/health-yggdrasil-streampixels.cmd
Normal file
2
scripts/health-yggdrasil-streampixels.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-yggdrasil-source-app.ps1 -AppId yggdrasil-streampixels -RepoRoot E:\Projects\StreamPixels -RemoteAppHome /srv/streampixels -CheckScript E:\Projects\gamecult-ops\scripts\check-streampixels-preview.sh
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||
$opsRoot = "E:\Projects\gamecult-ops"
|
||||
|
||||
$IdunnDeploymentTargets = @(
|
||||
[pscustomobject]@{
|
||||
|
|
@ -19,12 +18,12 @@ $IdunnDeploymentTargets = @(
|
|||
Repo = "Bifrost"
|
||||
LocalPath = "E:\Projects\Bifrost"
|
||||
Host = "yggdrasil"
|
||||
Service = "bifrost.service/bifrost-web"
|
||||
Service = "bifrost.service"
|
||||
Status = "blocked"
|
||||
Health = "$opsRoot\scripts\check-bifrost-alpha.sh"
|
||||
Deploy = "$opsRoot\scripts\deploy-bifrost-container.ps1"
|
||||
Health = $null
|
||||
Deploy = $null
|
||||
Restart = $null
|
||||
Reason = "Deploy authority exists in gamecult-ops, but desired image digest/tag is not yet published as an Idunn deployment target record."
|
||||
Reason = "Committed Bifrost HEAD currently expects UserAccounts.HeimdallAccountId, but Yggdrasil's production database lacks that column and EF reports no pending migration. Idunn must not enforce Bifrost freshness until the Bifrost schema migration path is fixed."
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-streampixels"
|
||||
|
|
@ -32,11 +31,11 @@ $IdunnDeploymentTargets = @(
|
|||
LocalPath = "E:\Projects\StreamPixels"
|
||||
Host = "yggdrasil"
|
||||
Service = "streampixels-service/streampixels-web"
|
||||
Status = "blocked"
|
||||
Health = "$opsRoot\scripts\check-streampixels-preview.sh"
|
||||
Deploy = "$opsRoot\scripts\deploy-streampixels-preview.sh"
|
||||
Status = "enforced"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-streampixels.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-streampixels.cmd"
|
||||
Restart = $null
|
||||
Reason = "Deploy script is remote-root/runbook shaped and needs a committed artifact manifest plus Idunn-safe wrapper before automatic execution."
|
||||
Reason = "Idunn packages committed local StreamPixels HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest."
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-heimdall"
|
||||
|
|
@ -44,11 +43,11 @@ $IdunnDeploymentTargets = @(
|
|||
LocalPath = "E:\Projects\Heimdall"
|
||||
Host = "yggdrasil"
|
||||
Service = "heimdall.service"
|
||||
Status = "blocked"
|
||||
Health = "$opsRoot\scripts\check-heimdall.sh"
|
||||
Deploy = "$opsRoot\scripts\deploy-heimdall.sh"
|
||||
Status = "enforced"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-heimdall.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-heimdall.cmd"
|
||||
Restart = $null
|
||||
Reason = "Deploy script uses a workstation-built source tarball; Idunn needs a manifest-producing wrapper before automatic execution."
|
||||
Reason = "Idunn packages committed local Heimdall HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest."
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-repixelizer"
|
||||
|
|
@ -56,11 +55,11 @@ $IdunnDeploymentTargets = @(
|
|||
LocalPath = "E:\Projects\repixelizer"
|
||||
Host = "yggdrasil"
|
||||
Service = "repixelizer-gui.service"
|
||||
Status = "blocked"
|
||||
Health = "$opsRoot\scripts\check-repixelizer-gui.sh"
|
||||
Deploy = "$opsRoot\scripts\deploy-repixelizer-gui.sh"
|
||||
Status = "enforced"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-repixelizer.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-repixelizer.cmd"
|
||||
Restart = $null
|
||||
Reason = "Deploy script uses a committed source tarball and Python environment mutation; Idunn needs an artifact manifest wrapper before automatic execution."
|
||||
Reason = "Idunn packages committed local repixelizer HEAD, runs the existing ops deploy/check scripts, and verifies the remote manifest."
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "github-pages-gamecult-site"
|
||||
|
|
|
|||
|
|
@ -69,6 +69,30 @@ $watchdogs = @(
|
|||
Health = "$repoRoot\scripts\health-idunn-swarm-deployment-coverage.cmd"
|
||||
Restart = $null
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-heimdall"
|
||||
Name = "Yggdrasil Heimdall"
|
||||
Verse = "yggdrasil.local"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-heimdall.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-heimdall.cmd"
|
||||
IntervalSeconds = 300
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-repixelizer"
|
||||
Name = "Yggdrasil Repixelizer"
|
||||
Verse = "yggdrasil.local"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-repixelizer.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-repixelizer.cmd"
|
||||
IntervalSeconds = 300
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "yggdrasil-streampixels"
|
||||
Name = "Yggdrasil StreamPixels"
|
||||
Verse = "yggdrasil.local"
|
||||
Health = "$repoRoot\scripts\health-yggdrasil-streampixels.cmd"
|
||||
Deploy = "$repoRoot\scripts\deploy-yggdrasil-streampixels.cmd"
|
||||
IntervalSeconds = 300
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "nightwing-gjallar"
|
||||
Name = "Nightwing Gjallar framebuffer compositor"
|
||||
|
|
|
|||
|
|
@ -75,9 +75,10 @@ Install the local Starfire boot watchdogs:
|
|||
.\scripts\install-idunn-startup.ps1
|
||||
```
|
||||
|
||||
That task starts Idunn loops for Odin, Stonks, and the Nightwing display
|
||||
services at user logon. The current Mimir dashboard is observed but not
|
||||
restarted until its restart authority is named.
|
||||
That task starts Idunn loops for Odin, Stonks, the enforced Yggdrasil source
|
||||
artifact targets, and the Nightwing display services at user logon. The current
|
||||
Mimir dashboard is observed but not restarted until its restart authority is
|
||||
named.
|
||||
|
||||
It also starts `idunn-swarm-deployment-coverage`, which watches the deployment
|
||||
target catalog in `scripts/idunn-deployment-targets.ps1`. A repo/service is not
|
||||
|
|
@ -129,9 +130,11 @@ For the repo swarm, deployment ownership begins with catalog coverage. Use:
|
|||
.\scripts\health-idunn-swarm-deployment-coverage.cmd
|
||||
```
|
||||
|
||||
The first fully enforced target is Nightwing Gjallar. Other targets remain
|
||||
blocked or external-owned until their deployment commands can produce manifests
|
||||
and run safely through Idunn.
|
||||
The enforced targets are Nightwing Gjallar plus the Yggdrasil source artifact
|
||||
lanes whose ops runbooks can produce and verify deployment manifests. Bifrost
|
||||
is blocked until its production database migration path is coherent; mobile
|
||||
device installs remain blocked at their physical approval boundaries; GitHub
|
||||
Pages remains external-owned.
|
||||
|
||||
## Typed Records
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue