Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
129be23a9d | ||
|
|
d31486ae1b | ||
|
|
4f3f3601d2 |
@@ -68,12 +68,23 @@ jobs:
|
||||
cache: true
|
||||
cache-dependency-path: agent/go.sum
|
||||
|
||||
- name: Extract version
|
||||
id: version
|
||||
shell: pwsh
|
||||
run: |
|
||||
$v = "${{ github.ref_name }}" -replace '^agent/v', ''
|
||||
"VERSION=$v" | Out-File -Append $env:GITHUB_OUTPUT
|
||||
# MSI ProductVersion must be numeric x.x.x.x
|
||||
"MSIVERSION=$v.0" | Out-File -Append $env:GITHUB_OUTPUT
|
||||
|
||||
- name: Build agent exe
|
||||
working-directory: agent
|
||||
shell: pwsh
|
||||
env:
|
||||
VERSION: ${{ steps.version.outputs.VERSION }}
|
||||
run: |
|
||||
$env:GOOS = "windows"; $env:GOARCH = "amd64"
|
||||
go build -o ../installer/vantage-agent-windows-amd64.exe ./cmd
|
||||
go build -ldflags="-s -w -X main.Version=$env:VERSION" -o ../installer/vantage-agent-windows-amd64.exe ./cmd
|
||||
|
||||
- name: Cache nssm
|
||||
id: cache-nssm
|
||||
@@ -100,7 +111,7 @@ jobs:
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:PATH = "$env:PATH;$env:USERPROFILE\.dotnet\tools"
|
||||
wix build vantage-agent.wxs -o vantage-agent.msi
|
||||
wix build vantage-agent.wxs -d Version=${{ steps.version.outputs.MSIVERSION }} -o vantage-agent.msi
|
||||
(Get-FileHash vantage-agent.msi -Algorithm SHA256).Hash.ToLower() + " vantage-agent.msi" | Out-File -Encoding ascii checksums-msi.txt
|
||||
|
||||
- name: Attach MSI to release
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -265,6 +266,11 @@ func handleDeleteKey(cmd *pb.ServerCommand) {
|
||||
}
|
||||
|
||||
func handleUpdateAgent(cmd *pb.ServerCommand) {
|
||||
if runtime.GOOS == "windows" {
|
||||
handleUpdateAgentWindows(cmd)
|
||||
return
|
||||
}
|
||||
|
||||
u := cmd.UpdateAgent
|
||||
arch := runtime.GOARCH // "amd64" or "arm64"
|
||||
tag := "agent%2Fv" + u.Version
|
||||
@@ -305,6 +311,47 @@ func handleUpdateAgent(cmd *pb.ServerCommand) {
|
||||
exec.Command("systemctl", "restart", "vantage-agent").Run()
|
||||
}
|
||||
|
||||
// handleUpdateAgentWindows downloads the latest MSI and launches msiexec to
|
||||
// perform a MajorUpgrade. msiexec is started DETACHED (via "cmd /c start") so
|
||||
// that when the upgrade stops the VantageAgent service, nssm's process-tree
|
||||
// kill of this agent does not also kill the installer mid-flight. Config
|
||||
// (server_id, agent_token) is preserved by setup.ps1 on upgrade.
|
||||
func handleUpdateAgentWindows(cmd *pb.ServerCommand) {
|
||||
u := cmd.UpdateAgent
|
||||
tag := "agent%2Fv" + u.Version
|
||||
msiURL := fmt.Sprintf("%s/mrhid6/vantage/releases/download/%s/vantage-agent.msi", u.GiteaBaseURL, tag)
|
||||
checksumURL := fmt.Sprintf("%s/mrhid6/vantage/releases/download/%s/checksums-msi.txt", u.GiteaBaseURL, tag)
|
||||
|
||||
log.Printf("updating agent to v%s from %s (cmd=%s)", u.Version, u.GiteaBaseURL, cmd.CommandId)
|
||||
|
||||
msiPath := filepath.Join(os.TempDir(), "vantage-agent-update.msi")
|
||||
if err := downloadFile(msiURL, msiPath); err != nil {
|
||||
log.Printf("update download failed (cmd=%s): %v", cmd.CommandId, err)
|
||||
return
|
||||
}
|
||||
|
||||
checksumData, err := httpGetBytes(checksumURL)
|
||||
if err != nil {
|
||||
log.Printf("update checksum fetch failed (cmd=%s): %v", cmd.CommandId, err)
|
||||
return
|
||||
}
|
||||
if err := verifyChecksum(msiPath, "vantage-agent.msi", checksumData); err != nil {
|
||||
log.Printf("update checksum mismatch (cmd=%s): %v", cmd.CommandId, err)
|
||||
os.Remove(msiPath)
|
||||
return
|
||||
}
|
||||
|
||||
logPath := filepath.Join(os.TempDir(), "vantage-agent-msi.log")
|
||||
log.Printf("launching msiexec for upgrade to v%s (cmd=%s)", u.Version, cmd.CommandId)
|
||||
// "start" detaches msiexec from this process tree so the service stop
|
||||
// during the upgrade does not terminate the installer.
|
||||
up := exec.Command("cmd", "/c", "start", "", "/wait", "msiexec", "/i", msiPath, "/qn", "/norestart", "/l*v", logPath)
|
||||
if err := up.Start(); err != nil {
|
||||
log.Printf("failed to launch msiexec (cmd=%s): %v", cmd.CommandId, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFile(url, dest string) error {
|
||||
resp, err := http.Get(url) //nolint:gosec
|
||||
if err != nil {
|
||||
|
||||
+78
-13
@@ -2,7 +2,8 @@ param(
|
||||
[string]$ServerId,
|
||||
[string]$Token,
|
||||
[string]$ServerUrl,
|
||||
[string]$InstallDir
|
||||
[string]$InstallDir,
|
||||
[switch]$Uninstall
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
@@ -17,23 +18,65 @@ function Write-Log($msg) {
|
||||
|
||||
# Fail native-exe (nssm) calls loudly: check $LASTEXITCODE after each call
|
||||
function Invoke-Native {
|
||||
param([string]$File, [string[]]$Args)
|
||||
Write-Log ("RUN: {0} {1}" -f $File, ($Args -join " "))
|
||||
$out = & $File @Args 2>&1
|
||||
param([string]$File, [string[]]$Arguments)
|
||||
Write-Log ("RUN: {0} {1}" -f $File, ($Arguments -join " "))
|
||||
$out = & $File @Arguments 2>&1
|
||||
if ($out) { Write-Log ("OUT: {0}" -f ($out -join "`n")) }
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw ("{0} exited {1}" -f $File, $LASTEXITCODE)
|
||||
}
|
||||
}
|
||||
|
||||
# Like Invoke-Native but never throws — for teardown, where a missing/stopped
|
||||
# service must not abort the uninstall.
|
||||
function Invoke-NativeSoft {
|
||||
param([string]$File, [string[]]$Arguments)
|
||||
Write-Log ("RUN(soft): {0} {1}" -f $File, ($Arguments -join " "))
|
||||
$out = & $File @Arguments 2>&1
|
||||
if ($out) { Write-Log ("OUT: {0}" -f ($out -join "`n")) }
|
||||
Write-Log ("EXIT: {0}" -f $LASTEXITCODE)
|
||||
}
|
||||
|
||||
if ($Uninstall) {
|
||||
try {
|
||||
Write-Log "=== teardown start ==="
|
||||
if (-not $InstallDir) { $InstallDir = $PSScriptRoot }
|
||||
$nssm = Join-Path $InstallDir "nssm.exe"
|
||||
if (Test-Path $nssm) {
|
||||
Invoke-NativeSoft -File $nssm -Arguments @("stop", "VantageAgent")
|
||||
Invoke-NativeSoft -File $nssm -Arguments @("remove", "VantageAgent", "confirm")
|
||||
} else {
|
||||
Write-Log "nssm.exe not found at $nssm - using sc.exe fallback"
|
||||
Invoke-NativeSoft -File "sc.exe" -Arguments @("stop", "VantageAgent")
|
||||
Invoke-NativeSoft -File "sc.exe" -Arguments @("delete", "VantageAgent")
|
||||
}
|
||||
Write-Log "=== teardown ok ==="
|
||||
exit 0
|
||||
}
|
||||
catch {
|
||||
Write-Log ("TEARDOWN ERROR: {0}" -f $_.Exception.Message)
|
||||
# Never block uninstall
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
Write-Log "=== setup start ==="
|
||||
Write-Log ("ServerId={0} ServerUrl={1} InstallDir={2}" -f $ServerId, $ServerUrl, $InstallDir)
|
||||
|
||||
$cfgDir = Join-Path $env:ProgramData "vantage"
|
||||
New-Item -ItemType Directory -Force -Path $cfgDir | Out-Null
|
||||
$cfgPath = Join-Path $cfgDir "config.yaml"
|
||||
|
||||
$cfg = @"
|
||||
# Preserve existing config on upgrade. A MajorUpgrade re-runs this script with
|
||||
# no SERVERID/TOKEN, so blindly rewriting would wipe the agent_token the agent
|
||||
# persisted after Register(). Only (re)write when a ServerId is supplied
|
||||
# (fresh install / explicit re-register).
|
||||
if ((Test-Path $cfgPath) -and (-not $ServerId)) {
|
||||
Write-Log "config.yaml exists and no ServerId supplied - preserving existing config (upgrade)"
|
||||
}
|
||||
else {
|
||||
$cfg = @"
|
||||
server_url: "$ServerUrl"
|
||||
server_id: "$ServerId"
|
||||
pre_reg_token: "$Token"
|
||||
@@ -41,12 +84,12 @@ agent_token: ""
|
||||
poll_interval: 30s
|
||||
tls: true
|
||||
"@
|
||||
$cfgPath = Join-Path $cfgDir "config.yaml"
|
||||
Set-Content -Path $cfgPath -Value $cfg -Encoding utf8
|
||||
Write-Log "wrote $cfgPath"
|
||||
Set-Content -Path $cfgPath -Value $cfg -Encoding utf8
|
||||
Write-Log "wrote $cfgPath"
|
||||
|
||||
# Lock down ACL: SYSTEM + Administrators only
|
||||
Invoke-Native -File "icacls" -Args @($cfgPath, "/inheritance:r", "/grant:r", "SYSTEM:F", "Administrators:F")
|
||||
# Lock down ACL: SYSTEM + Administrators only
|
||||
Invoke-Native -File "icacls" -Arguments @($cfgPath, "/inheritance:r", "/grant:r", "SYSTEM:F", "Administrators:F")
|
||||
}
|
||||
|
||||
if (-not $InstallDir) { $InstallDir = $PSScriptRoot }
|
||||
$nssm = Join-Path $InstallDir "nssm.exe"
|
||||
@@ -55,9 +98,31 @@ tls: true
|
||||
if (-not (Test-Path $nssm)) { throw "nssm.exe not found at $nssm" }
|
||||
if (-not (Test-Path $exe)) { throw "vantage-agent.exe not found at $exe" }
|
||||
|
||||
Invoke-Native -File $nssm -Args @("install", "VantageAgent", $exe)
|
||||
Invoke-Native -File $nssm -Args @("set", "VantageAgent", "Start", "SERVICE_AUTO_START")
|
||||
Invoke-Native -File $nssm -Args @("start", "VantageAgent")
|
||||
# Install only if the service isn't already registered (an upgrade may leave
|
||||
# it in place). "nssm install" on an existing service errors otherwise.
|
||||
$exists = Get-Service -Name "VantageAgent" -ErrorAction SilentlyContinue
|
||||
if (-not $exists) {
|
||||
Invoke-Native -File $nssm -Arguments @("install", "VantageAgent", $exe)
|
||||
} else {
|
||||
Write-Log "VantageAgent service already exists - updating binary path"
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "Application", $exe)
|
||||
}
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "Start", "SERVICE_AUTO_START")
|
||||
|
||||
# Redirect service stdout/stderr to log files (nssm discards them otherwise)
|
||||
# with online rotation at ~1MB.
|
||||
$outLog = Join-Path $logDir "agent-stdout.log"
|
||||
$errLog = Join-Path $logDir "agent-stderr.log"
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppStdout", $outLog)
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppStderr", $errLog)
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppStdoutCreationDisposition", "4")
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppStderrCreationDisposition", "4")
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppRotateFiles", "1")
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppRotateOnline", "1")
|
||||
Invoke-Native -File $nssm -Arguments @("set", "VantageAgent", "AppRotateBytes", "1048576")
|
||||
|
||||
# restart (not just start) so an upgrade picks up the new binary
|
||||
Invoke-NativeSoft -File $nssm -Arguments @("restart", "VantageAgent")
|
||||
|
||||
Write-Log "=== setup ok ==="
|
||||
exit 0
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?ifndef Version ?>
|
||||
<?define Version = "0.0.0.0" ?>
|
||||
<?endif?>
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
|
||||
<Package Name="Vantage Agent" Manufacturer="Vantage"
|
||||
Version="1.0.0.0" UpgradeCode="7d1e6d2c-2a5f-4b3e-9c3a-8a1b2c3d4e5f"
|
||||
Version="$(var.Version)" UpgradeCode="7d1e6d2c-2a5f-4b3e-9c3a-8a1b2c3d4e5f"
|
||||
Scope="perMachine">
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version is already installed." />
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
@@ -59,8 +62,20 @@
|
||||
<CustomAction Id="WriteConfig" Directory="INSTALLDIR" ExeCommand="[WriteConfig]"
|
||||
Execute="deferred" Impersonate="no" Return="check" />
|
||||
|
||||
<!-- Teardown on uninstall: stop + remove the service BEFORE RemoveFiles
|
||||
deletes nssm.exe/setup.ps1. Same CustomActionData marshaling pattern
|
||||
as WriteConfig. REMOVE="ALL" = full uninstall (not a component-level
|
||||
repair/modify). -->
|
||||
<SetProperty Id="RemoveService"
|
||||
Before="RemoveService" Sequence="execute" Condition="REMOVE="ALL""
|
||||
Value='cmd.exe /c powershell -ExecutionPolicy Bypass -File "[INSTALLDIR]setup.ps1" -Uninstall' />
|
||||
|
||||
<CustomAction Id="RemoveService" Directory="INSTALLDIR" ExeCommand="[RemoveService]"
|
||||
Execute="deferred" Impersonate="no" Return="ignore" />
|
||||
|
||||
<InstallExecuteSequence>
|
||||
<Custom Action="WriteConfig" After="InstallFiles" Condition="NOT Installed" />
|
||||
<Custom Action="RemoveService" Before="RemoveFiles" Condition="REMOVE="ALL"" />
|
||||
</InstallExecuteSequence>
|
||||
</Package>
|
||||
</Wix>
|
||||
|
||||
@@ -23,6 +23,7 @@ func RegisterRoutes(r *gin.Engine) {
|
||||
r.GET("/install", handleInstallScript)
|
||||
r.GET("/install.ps1", handleInstallScriptWindows)
|
||||
r.GET("/update", handleUpdateScript)
|
||||
r.GET("/update.ps1", handleUpdateScriptWindows)
|
||||
|
||||
// ESO read endpoint — bearer-token auth, not session auth, so Kubernetes
|
||||
// External Secrets Operator can call it. Lives under /api (so the reverse
|
||||
|
||||
@@ -54,3 +54,40 @@ func handleInstallScriptWindows(c *gin.Context) {
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(http.StatusOK, script)
|
||||
}
|
||||
|
||||
// handleUpdateScriptWindows serves a PowerShell one-liner that upgrades an
|
||||
// already-installed Windows agent. No server_id/token needed: the MSI is a
|
||||
// MajorUpgrade and setup.ps1 preserves the existing config on upgrade.
|
||||
func handleUpdateScriptWindows(c *gin.Context) {
|
||||
giteaHost := os.Getenv("GITEA_HOST")
|
||||
if giteaHost == "" {
|
||||
giteaHost = "gitea.example.com"
|
||||
}
|
||||
|
||||
script := fmt.Sprintf(
|
||||
"#Requires -RunAsAdministrator\n"+
|
||||
"$ErrorActionPreference = \"Stop\"\n"+
|
||||
"\n"+
|
||||
"$GiteaHost = \"%s\"\n"+
|
||||
"\n"+
|
||||
"$rel = Invoke-RestMethod -Uri \"https://$GiteaHost/api/v1/repos/mrhid6/vantage/releases?limit=10\"\n"+
|
||||
"$tag = ($rel | Where-Object { $_.tag_name -like 'agent/v*' } | Select-Object -First 1).tag_name\n"+
|
||||
"if (-not $tag) { throw \"Could not determine latest agent version\" }\n"+
|
||||
"$enc = $tag -replace '/','%%2F'\n"+
|
||||
"$base = \"https://$GiteaHost/mrhid6/vantage/releases/download/$enc\"\n"+
|
||||
"\n"+
|
||||
"$tmp = Join-Path $env:TEMP \"vantage-agent.msi\"\n"+
|
||||
"Invoke-WebRequest -Uri \"$base/vantage-agent.msi\" -OutFile $tmp\n"+
|
||||
"Invoke-WebRequest -Uri \"$base/checksums-msi.txt\" -OutFile \"$env:TEMP\\checksums-msi.txt\"\n"+
|
||||
"\n"+
|
||||
"$expected = (Get-Content \"$env:TEMP\\checksums-msi.txt\" | Select-String 'vantage-agent.msi').ToString().Split()[0]\n"+
|
||||
"$actual = (Get-FileHash $tmp -Algorithm SHA256).Hash.ToLower()\n"+
|
||||
"if ($expected -ne $actual) { throw \"Checksum mismatch\" }\n"+
|
||||
"\n"+
|
||||
"Start-Process msiexec.exe -Wait -ArgumentList \"/i `\"$tmp`\" /qn /norestart\"\n"+
|
||||
"Write-Host \"Vantage agent updated to $tag.\"\n",
|
||||
giteaHost)
|
||||
|
||||
c.Header("Content-Type", "text/plain; charset=utf-8")
|
||||
c.String(http.StatusOK, script)
|
||||
}
|
||||
|
||||
@@ -416,10 +416,10 @@ export default function ServerDetailPage() {
|
||||
{updateSuccess ? "Update Sent!" : "Update Agent"}
|
||||
</Button>
|
||||
<div className="relative flex-1 min-w-64 rounded-lg border border-border bg-[#0a0c14] px-4 py-2.5 font-mono text-sm">
|
||||
<span className="text-accent">$</span> <span className="text-text-primary">{api.getUpdateCommand()}</span>
|
||||
<span className="text-accent">{server.os_info?.toLowerCase().includes("windows") ? "PS>" : "$"}</span> <span className="text-text-primary">{api.getUpdateCommand(server.os_info)}</span>
|
||||
<button
|
||||
onClick={async () => {
|
||||
await navigator.clipboard.writeText(api.getUpdateCommand());
|
||||
await navigator.clipboard.writeText(api.getUpdateCommand(server.os_info));
|
||||
setCopiedUpdate(true);
|
||||
setTimeout(() => setCopiedUpdate(false), 2000);
|
||||
}}
|
||||
|
||||
+4
-1
@@ -189,7 +189,10 @@ export const api = {
|
||||
});
|
||||
},
|
||||
|
||||
getUpdateCommand(): string {
|
||||
getUpdateCommand(osInfo?: string): string {
|
||||
if (osInfo && osInfo.toLowerCase().includes("windows")) {
|
||||
return `irm "${window.location.origin}/update.ps1" | iex`;
|
||||
}
|
||||
return `curl -fsSL "${window.location.origin}/update" | bash`;
|
||||
},
|
||||
|
||||
|
||||
@@ -26,6 +26,10 @@ const nextConfig: NextConfig = {
|
||||
source: "/update",
|
||||
destination: `${apiUrl}/update`,
|
||||
},
|
||||
{
|
||||
source: "/update.ps1",
|
||||
destination: `${apiUrl}/update.ps1`,
|
||||
},
|
||||
];
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user