feat: Phase 9 — Offline Detection, API Key Auth, Agent Self-Update
Offline Detection (9.1):
- AgentOfflineDetectorService: BackgroundService, prüft alle 60s
ob Agents seit >5 min kein Heartbeat hatten → Status=Offline
- IServiceScopeFactory für korrektes Scoped-DI im Singleton
- SignalR-Push AgentStatusChanged bei jeder Offline-Markierung
API Key Auth (9.2):
- ApiKeyMiddleware: prüft X-Api-Key Header gegen Security:ApiKey Config
- Deaktiviert wenn ApiKey leer (Dev-Modus), Swagger/hubs bypassed
- Frontend: getApiKey() aus localStorage, automatisch in allen Requests
- Settings-Modal in Sidebar: API-Key eingeben + maskiert anzeigen
Agent Self-Update (9.3):
- internal/updater/updater.go: CheckForUpdate() + Update()
Download, SHA256-Verify, Windows Batch-Neustart / Linux Shell-Neustart
- AgentReleasesController: GET /api/v1/agent/releases/latest,
GET /api/v1/agent/releases/download/{platform}
- AgentReleaseOptions: LatestVersion, ReleasePath, Checksum in appsettings
- executeCommand() erhält cfg *Config statt agentID string
(für ServerAddress-Ableitung im UpdateAgent-Case)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
155
Agent/internal/updater/updater.go
Normal file
155
Agent/internal/updater/updater.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package updater
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ReleaseInfo enthält Informationen über die aktuell verfügbare Agent-Version.
|
||||
type ReleaseInfo struct {
|
||||
Version string `json:"version"`
|
||||
DownloadUrl string `json:"downloadUrl"`
|
||||
Checksum string `json:"checksum"` // SHA256 hex
|
||||
}
|
||||
|
||||
// CheckForUpdate fragt den Backend-Server nach der aktuellen Version.
|
||||
// serverAddress: z.B. "localhost:5000" (REST Port, nicht gRPC)
|
||||
func CheckForUpdate(ctx context.Context, serverAddress, currentVersion string) (*ReleaseInfo, error) {
|
||||
url := fmt.Sprintf("http://%s/api/v1/agent/releases/latest?os=%s&arch=%s",
|
||||
serverAddress, runtime.GOOS, runtime.GOARCH)
|
||||
|
||||
httpCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(httpCtx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetching release info: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status %d from release endpoint", resp.StatusCode)
|
||||
}
|
||||
|
||||
var info ReleaseInfo
|
||||
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
|
||||
return nil, fmt.Errorf("decoding release info: %w", err)
|
||||
}
|
||||
|
||||
if info.Version == currentVersion {
|
||||
return nil, nil // bereits aktuell
|
||||
}
|
||||
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
// Update lädt die neue Version herunter und startet einen Austausch-Prozess.
|
||||
// Nach erfolgreichem Download wird das neue Binary neben das aktuelle gelegt
|
||||
// und ein Neustartprozess gestartet.
|
||||
func Update(ctx context.Context, info *ReleaseInfo) error {
|
||||
binaryPath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("determining executable path: %w", err)
|
||||
}
|
||||
|
||||
// Download der neuen Version
|
||||
dlCtx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(dlCtx, http.MethodGet, info.DownloadUrl, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating download request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("downloading new binary: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("unexpected status %d during download", resp.StatusCode)
|
||||
}
|
||||
|
||||
fileBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("reading download body: %w", err)
|
||||
}
|
||||
|
||||
// SHA256-Prüfung (überspringen wenn Checksum leer = Dev-Modus)
|
||||
if info.Checksum != "" {
|
||||
sum := sha256.Sum256(fileBytes)
|
||||
got := hex.EncodeToString(sum[:])
|
||||
if got != info.Checksum {
|
||||
return fmt.Errorf("checksum mismatch: expected %s, got %s", info.Checksum, got)
|
||||
}
|
||||
}
|
||||
|
||||
newBinaryPath := binaryPath + ".new"
|
||||
if err := os.WriteFile(newBinaryPath, fileBytes, 0755); err != nil {
|
||||
return fmt.Errorf("writing new binary: %w", err)
|
||||
}
|
||||
|
||||
// Platform-spezifischer Neustart
|
||||
if runtime.GOOS == "windows" {
|
||||
return restartWindows(binaryPath, newBinaryPath)
|
||||
}
|
||||
return restartUnix(binaryPath, newBinaryPath)
|
||||
}
|
||||
|
||||
func restartWindows(binaryPath, newBinaryPath string) error {
|
||||
batchContent := fmt.Sprintf(`@echo off
|
||||
timeout /t 2 /nobreak > nul
|
||||
move /y "%s" "%s"
|
||||
start "" "%s"
|
||||
del "%%~f0"
|
||||
`, newBinaryPath, binaryPath, binaryPath)
|
||||
|
||||
batchPath := filepath.Join(os.TempDir(), "nexus-agent-update.bat")
|
||||
if err := os.WriteFile(batchPath, []byte(batchContent), 0644); err != nil {
|
||||
return fmt.Errorf("writing update batch script: %w", err)
|
||||
}
|
||||
|
||||
if err := exec.Command("cmd", "/c", "start", "", batchPath).Start(); err != nil {
|
||||
return fmt.Errorf("starting update batch script: %w", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func restartUnix(binaryPath, newBinaryPath string) error {
|
||||
scriptContent := fmt.Sprintf(`#!/bin/sh
|
||||
sleep 2
|
||||
mv -f "%s" "%s"
|
||||
"%s" &
|
||||
rm -f "$0"
|
||||
`, newBinaryPath, binaryPath, binaryPath)
|
||||
|
||||
scriptPath := filepath.Join(os.TempDir(), "nexus-agent-update.sh")
|
||||
if err := os.WriteFile(scriptPath, []byte(scriptContent), 0755); err != nil {
|
||||
return fmt.Errorf("writing update shell script: %w", err)
|
||||
}
|
||||
|
||||
if err := exec.Command("sh", scriptPath).Start(); err != nil {
|
||||
return fmt.Errorf("starting update shell script: %w", err)
|
||||
}
|
||||
|
||||
os.Exit(0)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user