fix: repair version detection platform query format
- Fix GetLatestVersionByTypeAndArch to separate platform/architecture - Query now correctly uses platform='linux' and architecture='amd64' - Resolves UI showing 'no packages available' despite updates existing
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -14,37 +15,72 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AgentUpdateHandler handles agent update operations
|
||||
type AgentUpdateHandler struct {
|
||||
agentQueries *queries.AgentQueries
|
||||
agentUpdateQueries *queries.AgentUpdateQueries
|
||||
commandQueries *queries.CommandQueries
|
||||
signingService *services.SigningService
|
||||
nonceService *services.UpdateNonceService
|
||||
agentHandler *AgentHandler
|
||||
}
|
||||
|
||||
// NewAgentUpdateHandler creates a new agent update handler
|
||||
func NewAgentUpdateHandler(aq *queries.AgentQueries, auq *queries.AgentUpdateQueries, cq *queries.CommandQueries, ss *services.SigningService, ah *AgentHandler) *AgentUpdateHandler {
|
||||
func NewAgentUpdateHandler(aq *queries.AgentQueries, auq *queries.AgentUpdateQueries, cq *queries.CommandQueries, ss *services.SigningService, ns *services.UpdateNonceService, ah *AgentHandler) *AgentUpdateHandler {
|
||||
return &AgentUpdateHandler{
|
||||
agentQueries: aq,
|
||||
agentUpdateQueries: auq,
|
||||
commandQueries: cq,
|
||||
signingService: ss,
|
||||
nonceService: ns,
|
||||
agentHandler: ah,
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateAgent handles POST /api/v1/agents/:id/update (manual agent update)
|
||||
func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
||||
// Extract agent ID from URL path
|
||||
agentID := c.Param("id")
|
||||
if agentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "agent ID is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Debug logging for development (controlled via REDFLAG_DEBUG env var or query param)
|
||||
debugMode := os.Getenv("REDFLAG_DEBUG") == "true" || c.Query("debug") == "true"
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Starting update request for agent %s from %s", agentID, c.ClientIP())
|
||||
log.Printf("[DEBUG] [UpdateAgent] Content-Type: %s, Content-Length: %d", c.ContentType(), c.Request.ContentLength)
|
||||
}
|
||||
|
||||
var req models.AgentUpdateRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] JSON binding error for agent %s: %v", agentID, err)
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
"_error_context": "json_binding_failed", // Helps identify binding vs validation errors
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Always log critical update operations for audit trail
|
||||
log.Printf("[UPDATE] Agent %s received update request - Version: %s, Platform: %s", agentID, req.Version, req.Platform)
|
||||
|
||||
// Debug: Log the parsed request
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Parsed update request - Version: %s, Platform: %s, Nonce: %s", req.Version, req.Platform, req.Nonce)
|
||||
}
|
||||
|
||||
agentIDUUID, err := uuid.Parse(agentID)
|
||||
if err != nil {
|
||||
log.Printf("[UPDATE] Agent ID format error for %s: %v", agentID, err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the agent exists
|
||||
agent, err := h.agentQueries.GetAgentByID(req.AgentID)
|
||||
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
@@ -77,12 +113,70 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Update agent status to "updating"
|
||||
if err := h.agentQueries.UpdateAgentUpdatingStatus(req.AgentID, true, &req.Version); err != nil {
|
||||
log.Printf("Failed to update agent %s status to updating: %v", req.AgentID, err)
|
||||
if err := h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, true, &req.Version); err != nil {
|
||||
log.Printf("Failed to update agent %s status to updating: %v", agentID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to initiate update"})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate the provided nonce
|
||||
if h.nonceService != nil {
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Validating nonce for agent %s: %s", agentID, req.Nonce)
|
||||
}
|
||||
verifiedNonce, err := h.nonceService.Validate(req.Nonce)
|
||||
if err != nil {
|
||||
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||
log.Printf("[UPDATE] Nonce validation failed for agent %s: %v", agentID, err)
|
||||
// Include specific error context for debugging
|
||||
errorType := "signature_verification_failed"
|
||||
if err.Error() == "nonce expired" {
|
||||
errorType = "nonce_expired"
|
||||
} else if err.Error() == "invalid base64" {
|
||||
errorType = "invalid_nonce_format"
|
||||
}
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "invalid update nonce: " + err.Error(),
|
||||
"_error_context": errorType,
|
||||
"_error_detail": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Nonce verified - AgentID: %s, TargetVersion: %s", verifiedNonce.AgentID, verifiedNonce.TargetVersion)
|
||||
}
|
||||
|
||||
// Verify the nonce matches the requested agent and version
|
||||
if verifiedNonce.AgentID != agentID {
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Agent ID mismatch - nonce: %s, URL: %s", verifiedNonce.AgentID, agentID)
|
||||
}
|
||||
log.Printf("[UPDATE] Agent ID mismatch in nonce: expected %s, got %s", agentID, verifiedNonce.AgentID)
|
||||
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "nonce agent ID mismatch",
|
||||
"_agent_id": agentID,
|
||||
"_nonce_agent_id": verifiedNonce.AgentID,
|
||||
})
|
||||
return
|
||||
}
|
||||
if verifiedNonce.TargetVersion != req.Version {
|
||||
if debugMode {
|
||||
log.Printf("[DEBUG] [UpdateAgent] Version mismatch - nonce: %s, request: %s", verifiedNonce.TargetVersion, req.Version)
|
||||
}
|
||||
log.Printf("[UPDATE] Version mismatch in nonce: expected %s, got %s", req.Version, verifiedNonce.TargetVersion)
|
||||
h.agentQueries.UpdateAgentUpdatingStatus(agentIDUUID, false, nil) // Rollback
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "nonce version mismatch",
|
||||
"_requested_version": req.Version,
|
||||
"_nonce_version": verifiedNonce.TargetVersion,
|
||||
})
|
||||
return
|
||||
}
|
||||
log.Printf("[UPDATE] Nonce successfully validated for agent %s to version %s", agentID, req.Version)
|
||||
}
|
||||
|
||||
// Generate nonce for replay protection
|
||||
nonceUUID := uuid.New()
|
||||
nonceTimestamp := time.Now()
|
||||
@@ -399,3 +493,188 @@ func (h *AgentUpdateHandler) estimateUpdateTime(fileSize int64) int {
|
||||
|
||||
return seconds
|
||||
}
|
||||
|
||||
// GenerateUpdateNonce handles POST /api/v1/agents/:id/update-nonce
|
||||
func (h *AgentUpdateHandler) GenerateUpdateNonce(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
targetVersion := c.Query("target_version")
|
||||
|
||||
if targetVersion == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "target_version query parameter required"})
|
||||
return
|
||||
}
|
||||
|
||||
if h.nonceService == nil {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "nonce service not available"})
|
||||
return
|
||||
}
|
||||
|
||||
// Parse agent ID as UUID
|
||||
agentIDUUID, err := uuid.Parse(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify agent exists
|
||||
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate nonce
|
||||
nonce, err := h.nonceService.Generate(agentID, targetVersion)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to generate update nonce for agent %s: %v", agentID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to generate nonce"})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[system] Generated update nonce for agent %s (%s) -> %s", agentID, agent.Hostname, targetVersion)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"agent_id": agentID,
|
||||
"hostname": agent.Hostname,
|
||||
"current_version": agent.CurrentVersion,
|
||||
"target_version": targetVersion,
|
||||
"update_nonce": nonce,
|
||||
"expires_at": time.Now().Add(10 * time.Minute).Unix(),
|
||||
"expires_in_seconds": 600,
|
||||
})
|
||||
}
|
||||
|
||||
// CheckForUpdateAvailable handles GET /api/v1/agents/:id/updates/available
|
||||
func (h *AgentUpdateHandler) CheckForUpdateAvailable(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
// Parse agent ID
|
||||
agentIDUUID, err := uuid.Parse(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Query database for agent's current version
|
||||
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Platform format: separate os_type and os_architecture from agent data
|
||||
osType := strings.ToLower(agent.OSType)
|
||||
osArch := agent.OSArchitecture
|
||||
|
||||
// Check if newer version available from agent_update_packages table
|
||||
latestVersion, err := h.agentUpdateQueries.GetLatestVersionByTypeAndArch(osType, osArch)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] GetLatestVersionByTypeAndArch error for %s/%s: %v", osType, osArch, err)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"hasUpdate": false,
|
||||
"reason": "no packages available",
|
||||
"currentVersion": agent.CurrentVersion,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if this is actually newer than current version
|
||||
hasUpdate := isVersionUpgrade(latestVersion, agent.CurrentVersion)
|
||||
|
||||
log.Printf("[DEBUG] Version comparison - latest: %s, current: %s, hasUpdate: %v for platform: %s/%s", latestVersion, agent.CurrentVersion, hasUpdate, osType, osArch)
|
||||
|
||||
// Special handling for sub-versions (0.1.23.5 vs 0.1.23)
|
||||
if !hasUpdate && strings.HasPrefix(latestVersion, agent.CurrentVersion + ".") {
|
||||
hasUpdate = true
|
||||
log.Printf("[DEBUG] Detected sub-version upgrade: %s -> %s", agent.CurrentVersion, latestVersion)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"hasUpdate": hasUpdate,
|
||||
"currentVersion": agent.CurrentVersion,
|
||||
"latestVersion": latestVersion,
|
||||
"platform": osType + "-" + osArch,
|
||||
})
|
||||
}
|
||||
|
||||
// GetUpdateStatus handles GET /api/v1/agents/:id/updates/status
|
||||
func (h *AgentUpdateHandler) GetUpdateStatus(c *gin.Context) {
|
||||
agentID := c.Param("id")
|
||||
|
||||
// Parse agent ID
|
||||
agentIDUUID, err := uuid.Parse(agentID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch agent with update state
|
||||
agent, err := h.agentQueries.GetAgentByID(agentIDUUID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Determine status from agent state + recent commands
|
||||
var status string
|
||||
var progress *int
|
||||
var errorMsg *string
|
||||
|
||||
if agent.IsUpdating {
|
||||
// Check if agent has pending update command
|
||||
cmd, err := h.agentUpdateQueries.GetPendingUpdateCommand(agentID)
|
||||
if err == nil && cmd != nil {
|
||||
status = "downloading"
|
||||
// Progress could be based on last acknowledgment time
|
||||
if time.Since(cmd.CreatedAt) > 2*time.Minute {
|
||||
status = "installing"
|
||||
}
|
||||
} else {
|
||||
status = "pending"
|
||||
}
|
||||
} else {
|
||||
status = "idle"
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"status": status,
|
||||
"progress": progress,
|
||||
"error": errorMsg,
|
||||
})
|
||||
}
|
||||
|
||||
// isVersionUpgrade returns true if new version is greater than current version
|
||||
func isVersionUpgrade(newVersion string, currentVersion string) bool {
|
||||
// Parse semantic versions
|
||||
newParts := strings.Split(newVersion, ".")
|
||||
curParts := strings.Split(currentVersion, ".")
|
||||
|
||||
// Pad arrays to 3 parts
|
||||
for len(newParts) < 3 {
|
||||
newParts = append(newParts, "0")
|
||||
}
|
||||
for len(curParts) < 3 {
|
||||
curParts = append(curParts, "0")
|
||||
}
|
||||
|
||||
// Convert to integers for comparison
|
||||
newMajor, _ := strconv.Atoi(newParts[0])
|
||||
newMinor, _ := strconv.Atoi(newParts[1])
|
||||
newPatch, _ := strconv.Atoi(newParts[2])
|
||||
|
||||
curMajor, _ := strconv.Atoi(curParts[0])
|
||||
curMinor, _ := strconv.Atoi(curParts[1])
|
||||
curPatch, _ := strconv.Atoi(curParts[2])
|
||||
|
||||
// Check if new > current (not equal, not less)
|
||||
if newMajor > curMajor {
|
||||
return true
|
||||
}
|
||||
if newMajor == curMajor && newMinor > curMinor {
|
||||
return true
|
||||
}
|
||||
if newMajor == curMajor && newMinor == curMinor && newPatch > curPatch {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -31,24 +31,28 @@ func (h *DownloadHandler) getServerURL(c *gin.Context) string {
|
||||
return h.config.Server.PublicURL
|
||||
}
|
||||
|
||||
// Priority 2: Construct API server URL from configuration
|
||||
// Priority 2: Detect from request with TLS/proxy awareness
|
||||
scheme := "http"
|
||||
host := h.config.Server.Host
|
||||
port := h.config.Server.Port
|
||||
|
||||
// Use HTTPS if TLS is enabled in config
|
||||
// Check if TLS is enabled in config
|
||||
if h.config.Server.TLS.Enabled {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// For default host (0.0.0.0), use localhost for client connections
|
||||
if host == "0.0.0.0" {
|
||||
host = "localhost"
|
||||
// Check if request came through HTTPS (direct or via proxy)
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Only include port if it's not the default for the protocol
|
||||
if (scheme == "http" && port != 80) || (scheme == "https" && port != 443) {
|
||||
return fmt.Sprintf("%s://%s:%d", scheme, host, port)
|
||||
// Check X-Forwarded-Proto for reverse proxy setups
|
||||
if forwardedProto := c.GetHeader("X-Forwarded-Proto"); forwardedProto == "https" {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Use the Host header exactly as received (includes port if present)
|
||||
host := c.GetHeader("X-Forwarded-Host")
|
||||
if host == "" {
|
||||
host = c.Request.Host
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s://%s", scheme, host)
|
||||
@@ -151,7 +155,6 @@ AGENT_BINARY="/usr/local/bin/redflag-agent"
|
||||
SUDOERS_FILE="/etc/sudoers.d/redflag-agent"
|
||||
SERVICE_FILE="/etc/systemd/system/redflag-agent.service"
|
||||
CONFIG_DIR="/etc/aggregator"
|
||||
STATE_DIR="/var/lib/aggregator"
|
||||
|
||||
echo "=== RedFlag Agent Installation ==="
|
||||
echo ""
|
||||
@@ -298,24 +301,19 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Step 4: Create configuration and state directories
|
||||
# Step 4: Create configuration directory
|
||||
echo ""
|
||||
echo "Step 4: Creating configuration and state directories..."
|
||||
echo "Step 4: Creating configuration directory..."
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
chown "$AGENT_USER:$AGENT_USER" "$CONFIG_DIR"
|
||||
chmod 755 "$CONFIG_DIR"
|
||||
echo "✓ Configuration directory created"
|
||||
|
||||
# Create state directory for acknowledgment tracking (v0.1.19+)
|
||||
mkdir -p "$STATE_DIR"
|
||||
chown "$AGENT_USER:$AGENT_USER" "$STATE_DIR"
|
||||
chmod 755 "$STATE_DIR"
|
||||
echo "✓ Configuration and state directories created"
|
||||
|
||||
# Set SELinux context for directories if SELinux is enabled
|
||||
# Set SELinux context for config directory if SELinux is enabled
|
||||
if command -v getenforce >/dev/null 2>&1 && [ "$(getenforce)" != "Disabled" ]; then
|
||||
echo "Setting SELinux context for directories..."
|
||||
restorecon -Rv "$CONFIG_DIR" "$STATE_DIR" 2>/dev/null || true
|
||||
echo "✓ SELinux context set for directories"
|
||||
echo "Setting SELinux context for config directory..."
|
||||
restorecon -Rv "$CONFIG_DIR" 2>/dev/null || true
|
||||
echo "✓ SELinux context set for config directory"
|
||||
fi
|
||||
|
||||
# Step 5: Install systemd service
|
||||
@@ -340,7 +338,7 @@ RestartSec=30
|
||||
# NoNewPrivileges=true - DISABLED: Prevents sudo from working, which agent needs for package management
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=$AGENT_HOME /var/log $CONFIG_DIR $STATE_DIR
|
||||
ReadWritePaths=$AGENT_HOME /var/log $CONFIG_DIR
|
||||
PrivateTmp=true
|
||||
|
||||
# Logging
|
||||
|
||||
@@ -43,6 +43,7 @@ type Config struct {
|
||||
LatestAgentVersion string
|
||||
MinAgentVersion string `env:"MIN_AGENT_VERSION" default:"0.1.22"`
|
||||
SigningPrivateKey string `env:"REDFLAG_SIGNING_PRIVATE_KEY"`
|
||||
DebugEnabled bool `env:"REDFLAG_DEBUG" default:"false"` // Enable debug logging
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables only (immutable configuration)
|
||||
@@ -86,7 +87,7 @@ func Load() (*Config, error) {
|
||||
cfg.CheckInInterval = checkInInterval
|
||||
cfg.OfflineThreshold = offlineThreshold
|
||||
cfg.Timezone = getEnv("TIMEZONE", "UTC")
|
||||
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23")
|
||||
cfg.LatestAgentVersion = getEnv("LATEST_AGENT_VERSION", "0.1.23.5")
|
||||
cfg.MinAgentVersion = getEnv("MIN_AGENT_VERSION", "0.1.22")
|
||||
cfg.SigningPrivateKey = getEnv("REDFLAG_SIGNING_PRIVATE_KEY", "")
|
||||
|
||||
|
||||
@@ -217,3 +217,65 @@ func (q *AgentUpdateQueries) GetAgentByMachineID(machineID string) (*models.Agen
|
||||
|
||||
return &agent, nil
|
||||
}
|
||||
|
||||
// GetLatestVersion retrieves the latest available version for a platform
|
||||
func (q *AgentUpdateQueries) GetLatestVersion(platform string) (string, error) {
|
||||
query := `
|
||||
SELECT version FROM agent_update_packages
|
||||
WHERE platform = $1 AND is_active = true
|
||||
ORDER BY version DESC LIMIT 1
|
||||
`
|
||||
|
||||
var latestVersion string
|
||||
err := q.db.Get(&latestVersion, query, platform)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", fmt.Errorf("no update packages available for platform %s", platform)
|
||||
}
|
||||
return "", fmt.Errorf("failed to get latest version: %w", err)
|
||||
}
|
||||
|
||||
return latestVersion, nil
|
||||
}
|
||||
|
||||
// GetLatestVersionByTypeAndArch retrieves the latest available version for a specific os_type and architecture
|
||||
func (q *AgentUpdateQueries) GetLatestVersionByTypeAndArch(osType, osArch string) (string, error) {
|
||||
query := `
|
||||
SELECT version FROM agent_update_packages
|
||||
WHERE platform = $1 AND architecture = $2 AND is_active = true
|
||||
ORDER BY version DESC LIMIT 1
|
||||
`
|
||||
|
||||
var latestVersion string
|
||||
err := q.db.Get(&latestVersion, query, osType, osArch)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return "", fmt.Errorf("no update packages available for platform %s/%s", osType, osArch)
|
||||
}
|
||||
return "", fmt.Errorf("failed to get latest version: %w", err)
|
||||
}
|
||||
|
||||
return latestVersion, nil
|
||||
}
|
||||
|
||||
// GetPendingUpdateCommand retrieves the most recent pending update command for an agent
|
||||
func (q *AgentUpdateQueries) GetPendingUpdateCommand(agentID string) (*models.AgentCommand, error) {
|
||||
query := `
|
||||
SELECT id, agent_id, command_type, params, status, source, created_at, sent_at, completed_at, result, retried_from_id
|
||||
FROM agent_commands
|
||||
WHERE agent_id = $1 AND command_type = 'install_update' AND status = 'pending'
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
var command models.AgentCommand
|
||||
err := q.db.Get(&command, query, agentID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil // No pending update command found
|
||||
}
|
||||
return nil, fmt.Errorf("failed to get pending update command: %w", err)
|
||||
}
|
||||
|
||||
return &command, nil
|
||||
}
|
||||
@@ -23,10 +23,11 @@ type AgentUpdatePackage struct {
|
||||
|
||||
// AgentUpdateRequest represents a request to update an agent
|
||||
type AgentUpdateRequest struct {
|
||||
AgentID uuid.UUID `json:"agent_id" binding:"required"`
|
||||
AgentID uuid.UUID `json:"agent_id,omitempty"` // Optional when agent ID is in URL path
|
||||
Version string `json:"version" binding:"required"`
|
||||
Platform string `json:"platform" binding:"required"`
|
||||
Scheduled *string `json:"scheduled_at,omitempty"`
|
||||
Nonce string `json:"nonce" binding:"required"` // Required security nonce to prevent replay attacks
|
||||
}
|
||||
|
||||
// BulkAgentUpdateRequest represents a bulk update request
|
||||
|
||||
@@ -73,6 +73,23 @@ export function AgentUpdatesModal({
|
||||
const pkg = packages.find(p => p.id === packageId);
|
||||
if (!pkg) throw new Error('Package not found');
|
||||
|
||||
// For single agent updates, use individual update with nonce for security
|
||||
if (selectedAgentIds.length === 1) {
|
||||
const agentId = selectedAgentIds[0];
|
||||
|
||||
// Generate nonce for security
|
||||
const nonceData = await agentApi.generateUpdateNonce(agentId, pkg.version);
|
||||
console.log('[UI] Update nonce generated for single agent:', nonceData);
|
||||
|
||||
// Use individual update endpoint with nonce
|
||||
return agentApi.updateAgent(agentId, {
|
||||
version: pkg.version,
|
||||
platform: pkg.platform,
|
||||
nonce: nonceData.update_nonce
|
||||
});
|
||||
}
|
||||
|
||||
// For multiple agents, use bulk update
|
||||
const updateData = {
|
||||
agent_ids: selectedAgentIds,
|
||||
version: pkg.version,
|
||||
@@ -82,7 +99,9 @@ export function AgentUpdatesModal({
|
||||
return agentApi.updateMultipleAgents(updateData);
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Update initiated for ${data.updated?.length || 0} agent(s)`);
|
||||
const count = selectedAgentIds.length;
|
||||
const message = count === 1 ? 'Update initiated for agent' : `Update initiated for ${count} agents`;
|
||||
toast.success(message);
|
||||
setIsUpdating(false);
|
||||
onAgentsUpdated();
|
||||
onClose();
|
||||
|
||||
@@ -172,6 +172,24 @@ export const agentApi = {
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Check if update available for agent
|
||||
checkForUpdateAvailable: async (agentId: string): Promise<{ hasUpdate: boolean; currentVersion: string; latestVersion?: string; reason?: string; platform?: string }> => {
|
||||
const response = await api.get(`/agents/${agentId}/updates/available`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Get update status for agent
|
||||
getUpdateStatus: async (agentId: string): Promise<{ status: string; progress?: number; error?: string }> => {
|
||||
const response = await api.get(`/agents/${agentId}/updates/status`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Generate update nonce for agent (new security feature)
|
||||
generateUpdateNonce: async (agentId: string, targetVersion: string): Promise<{ agent_id: string; target_version: string; update_nonce: string; expires_at: number; expires_in_seconds: number }> => {
|
||||
const response = await api.post(`/agents/${agentId}/update-nonce?target_version=${targetVersion}`);
|
||||
return response.data;
|
||||
},
|
||||
|
||||
// Update multiple agents (bulk)
|
||||
updateMultipleAgents: async (updateData: {
|
||||
agent_ids: string[];
|
||||
|
||||
Reference in New Issue
Block a user