Files
Redflag/docs/historical/MIGRATION_PLAN_v0.1.26_to_v0.1.27.md

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 scanner
  • handleScanDNF - DNF package manager scanner
  • handleScanWindows - Windows Update scanner
  • handleScanWinget - 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/trigger to generate appropriate scan_<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 buttons
  • aggregator-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 legacy
  • aggregator-agent/cmd/agent/main.go - Update routing
  • aggregator-agent/internal/orchestrator/ - Create scanner wrappers for apt/dnf/windows/winget

Server (Backend)

  • aggregator-server/internal/api/handlers/subsystems.go - Update command generation
  • aggregator-server/internal/api/handlers/commands.go - Update command validation
  • aggregator-server/internal/database/migrations/023_enable_individual_scans.up.sql - Migration

Web (Frontend)

  • aggregator-web/src/components/AgentSubsystems.tsx - Individual trigger UI
  • aggregator-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_updates commands (for testing)
  • All individual handlers work correctly

AT v0.1.27 RELEASE:

  • Deprecated handler removed entirely
  • Server rejects scan_updates commands 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_docker all 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.