9.9 KiB
RedFlag v0.1.26 → v0.1.27 Migration Plan
Single Sprint, Non-Breaking, Complete Independence
Status: IMPLEMENTATION PLAN Target: v0.1.27 Timeline: Single sprint, no staged releases, no extended deprecation
Executive Summary
Transition from monolithic scan_updates command to fully independent subsystem commands. Delete legacy handleScanUpdatesV2 entirely and implement individual handlers for all subsystems.
Architecture Change
Before (v0.1.26 - Current)
User triggers scan
↓
Server sends: scan_updates (single command ID)
↓
Agent: handleScanUpdatesV2 → orch.ScanAll()
↓
Runs ALL scanners in parallel
↓
Single command lifecycle (all succeed or all fail together)
↓
Single history entry (if kept)
After (v0.1.27 - Target)
User triggers storage scan
↓
Server sends: scan_storage (unique command ID)
↓
Agent: handleScanStorage → orch.ScanSingle("storage")
↓
Runs ONLY storage scanner
↓
Independent command lifecycle
↓
Independent history entry
(Same pattern for: apt, dnf, winget, windows, updates, docker, system)
Phase 1: Immediate Changes (Ready for Testing)
1.1 Mark Legacy as DEPRECATED
File: aggregator-agent/cmd/agent/subsystem_handlers.go
// handleScanUpdatesV2_DEPRECATED [DEPRECATED v0.1.27] - Legacy monolithic scan handler
// DO NOT USE - Will be removed in v0.1.28
func handleScanUpdatesV2_DEPRECATED(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, orch *orchestrator.Orchestrator, commandID string) error {
log.Println("⚠️ DEPRECATED: Use individual scan commands (scan_storage, scan_system, scan_docker, scan_apt, scan_dnf, scan_winget)")
// Keep existing implementation for backward compatibility during testing period
// ... existing code ...
return fmt.Errorf("DEPRECATED: This command will be removed in v0.1.28. Use individual subsystem scan commands instead.")
}
1.2 Add Missing Individual Handlers
File: aggregator-agent/cmd/agent/subsystem_handlers.go
Need to create:
handleScanAPT- APT package manager scannerhandleScanDNF- DNF package manager scannerhandleScanWindows- Windows Update scannerhandleScanWinget- Winget package manager scanner
Template for each new handler:
func handleScan<Subsystem>(apiClient *client.Client, cfg *config.Config, ackTracker *acknowledgment.Tracker, orch *orchestrator.Orchestrator, commandID string) error {
log.Println("Scanning <subsystem>...")
ctx := context.Background()
startTime := time.Now()
// Execute scanner
result, err := orch.ScanSingle(ctx, "<subsystem>")
if err != nil {
return fmt.Errorf("failed to scan <subsystem>: %w", err)
}
// Format results
results := []orchestrator.ScanResult{result}
stdout, stderr, exitCode := orchestrator.FormatScanSummary(results)
duration := time.Since(startTime)
stdout += fmt.Sprintf("\n<Subsystem> scan completed in %.2f seconds\n", duration.Seconds())
// Report to dedicated endpoint
<Subsystem>Scanner := orchestrator.New<Subsystem>Scanner(cfg.AgentVersion)
var metrics []orchestrator.<Subsystem>Metric
if <Subsystem>Scanner.IsAvailable() {
var err error
metrics, err = <Subsystem>Scanner.Scan<Subsystem>()
if err != nil {
return fmt.Errorf("failed to scan <subsystem> metrics: %w", err)
}
if len(metrics) > 0 {
metricItems := make([]client.<Subsystem>ReportItem, 0, len(metrics))
for _, metric := range metrics {
item := convert<Subsystem>Metric(metric)
metricItems = append(metricItems, item)
}
report := client.<Subsystem>Report{
AgentID: cfg.AgentID,
CommandID: commandID,
Timestamp: time.Now(),
Metrics: metricItems,
}
if err := apiClient.Report<Subsystem>Metrics(cfg.AgentID, report); err != nil {
return fmt.Errorf("failed to report <subsystem> metrics: %w", err)
}
log.Printf("[INFO] [<subsystem>] Successfully reported %d <subsystem> metrics to server\n", len(metrics))
}
}
// Create history entry for unified view
logReport := client.LogReport{
CommandID: commandID,
Action: "scan_<subsystem>",
Result: map[bool]string{true: "success", false: "failure"}[exitCode == 0],
Stdout: stdout,
Stderr: stderr,
ExitCode: exitCode,
DurationSeconds: int(duration.Seconds()),
Metadata: map[string]string{
"subsystem_label": "<Subsystem Name>",
"subsystem": "<subsystem>",
"metrics_count": fmt.Sprintf("%d", len(metrics)),
},
}
if err := reportLogWithAck(apiClient, cfg, ackTracker, logReport); err != nil {
log.Printf("[ERROR] [agent] [<subsystem>] report_log_failed: %v", err)
log.Printf("[HISTORY] [agent] [<subsystem>] report_log_failed error=\"%v\" timestamp=%s", err, time.Now().Format(time.RFC3339))
} else {
log.Printf("[INFO] [agent] [<subsystem>] history_log_created command_id=%s", commandID)
log.Printf("[HISTORY] [agent] [scan_<subsystem>] log_created agent_id=%s command_id=%s result=%s timestamp=%s", cfg.AgentID, commandID, map[bool]string{true: "success", false: "failure"}[exitCode == 0], time.Now().Format(time.RFC3339))
}
return nil
}
1.3 Update Command Routing
File: aggregator-agent/cmd/agent/main.go
Add cases to the main command router:
case "scan_apt":
return handleScanAPT(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID)
case "scan_dnf":
return handleScanDNF(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID)
case "scan_windows":
return handleScanWindows(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID)
case "scan_winget":
return handleScanWinget(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID)
case "scan_updates":
return handleScanUpdatesV2_DEPRECATED(apiClient, cfg, ackTracker, scanOrchestrator, cmd.ID)
1.4 Server-Side Command Generation (Required)
Update server to send individual commands instead of scan_updates:
- Modify
/api/v1/agents/:id/subsystems/:subsystem/triggerto generate appropriatescan_<subsystem>commands - Update frontend to trigger individual scans
Phase 2: Server-Side Changes
2.1 Update Subsystem Handlers
File: aggregator-server/internal/api/handlers/subsystems.go
Modify TriggerSubsystem to generate individual commands:
func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) {
// ... existing validation ...
// Generate subsystem-specific command
commandType := "scan_" + subsystem // e.g., "scan_storage", "scan_system"
command := &models.AgentCommand{
AgentID: agentID,
CommandType: commandType,
Status: "pending",
Source: "manual",
Params: map[string]interface{}{"subsystem": subsystem},
}
// ... rest of function ...
}
2.2 Update Frontend
Files:
aggregator-web/src/components/AgentUpdates.tsx- Individual trigger buttonsaggregator-web/src/lib/api.ts- API methods for individual triggers
Phase 3: Cleanup (v0.1.27 Release)
3.1 Delete Legacy Handler
File: aggregator-agent/cmd/agent/subsystem_handlers.go
// REMOVE THIS ENTIRELY in v0.1.27
// func handleScanUpdatesV2_DEPRECATED(...) { ... }
3.2 Remove Deprecated Command Routing
File: aggregator-agent/cmd/agent/main.go
// Remove this case:
// case "scan_updates":
// return handleScanUpdatesV2_DEPRECATED(...)
3.3 Drop Deprecated Table
Migration: 024_drop_legacy_metrics.up.sql
-- Drop legacy metrics table (if it exists from v0.1.20 experiments)
DROP TABLE IF EXISTS legacy_metrics;
-- Clean up any legacy metrics from update_logs
DELETE FROM update_logs WHERE action = 'scan_all' AND created_at < '2025-01-01';
Files Modified
Agent (Backend)
aggregator-agent/cmd/agent/subsystem_handlers.go- Add handlers, deprecate legacyaggregator-agent/cmd/agent/main.go- Update routingaggregator-agent/internal/orchestrator/- Create scanner wrappers for apt/dnf/windows/winget
Server (Backend)
aggregator-server/internal/api/handlers/subsystems.go- Update command generationaggregator-server/internal/api/handlers/commands.go- Update command validationaggregator-server/internal/database/migrations/023_enable_individual_scans.up.sql- Migration
Web (Frontend)
aggregator-web/src/components/AgentSubsystems.tsx- Individual trigger UIaggregator-web/src/lib/api.ts- Individual scan API methods
Backwards Compatibility
DURING v0.1.27 TESTING:
- Deprecated handler remains but logs warnings
- Server can still accept
scan_updatescommands (for testing) - All individual handlers work correctly
AT v0.1.27 RELEASE:
- Deprecated handler removed entirely
- Server rejects
scan_updatescommands with clear error message - Breaking change - requires coordinated upgrade (acceptable for major version)
Testing Checklist
Before v0.1.27 release, verify:
- Migration 023 applied: Individual subsystem handlers exist
- Agent handles individual commands:
scan_storage,scan_system,scan_dockerall work - Agent creates history entries: Each scan creates proper log in unified history
- Server sends individual commands: Frontend triggers generate correct command types
- Retry logic isolated: APT failure doesn't affect Docker retry attempts
- UI shows individual controls: Users can trigger each subsystem independently
- Deprecated handler logs warnings: Clear messaging that feature is deprecated
Breaking Changes (v0.1.27 Release)
- Agent binaries built with v0.1.26 will NOT work with v0.1.27 servers
- Requires coordinated upgrade of all components
- v0.1.27 is a MAJOR version bump (despite numbering)
Approval Required
Decision: Proceed with single-sprint implementation as outlined above?
Alternative: Staged migration with longer deprecation period?
Note: Current code commits are reversible during testing phase. Once v0.1.27 is released and tested, changes become permanent.