feat: mock gateway
This commit is contained in:
449
internal/mock/data.go
Normal file
449
internal/mock/data.go
Normal file
@@ -0,0 +1,449 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"spore-gateway/pkg/client"
|
||||
"spore-gateway/pkg/registry"
|
||||
)
|
||||
|
||||
// GenerateMockClusterMembers generates mock cluster member data
|
||||
func GenerateMockClusterMembers(nodes map[string]*NodeInfo) []client.ClusterMember {
|
||||
members := make([]client.ClusterMember, 0, len(nodes))
|
||||
|
||||
for _, node := range nodes {
|
||||
member := client.ClusterMember{
|
||||
IP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
Status: string(node.Status),
|
||||
Latency: node.Latency,
|
||||
LastSeen: node.LastSeen.Unix(),
|
||||
Labels: node.Labels,
|
||||
Resources: map[string]interface{}{
|
||||
"freeHeap": 32768 + rand.Intn(32768),
|
||||
"cpuFreqMHz": 80 + rand.Intn(160),
|
||||
"flashChipSize": 4194304,
|
||||
},
|
||||
}
|
||||
members = append(members, member)
|
||||
}
|
||||
|
||||
return members
|
||||
}
|
||||
|
||||
// GenerateMockTaskStatus generates mock task status data
|
||||
func GenerateMockTaskStatus() *client.TaskStatusResponse {
|
||||
tasks := []client.TaskInfo{
|
||||
{
|
||||
Name: "HeartbeatTask",
|
||||
Interval: 5000,
|
||||
Enabled: true,
|
||||
Running: true,
|
||||
AutoStart: true,
|
||||
},
|
||||
{
|
||||
Name: "SensorReadTask",
|
||||
Interval: 10000,
|
||||
Enabled: true,
|
||||
Running: true,
|
||||
AutoStart: true,
|
||||
},
|
||||
{
|
||||
Name: "StatusUpdateTask",
|
||||
Interval: 30000,
|
||||
Enabled: true,
|
||||
Running: false,
|
||||
AutoStart: false,
|
||||
},
|
||||
{
|
||||
Name: "CleanupTask",
|
||||
Interval: 60000,
|
||||
Enabled: false,
|
||||
Running: false,
|
||||
AutoStart: false,
|
||||
},
|
||||
}
|
||||
|
||||
activeTasks := 0
|
||||
for _, task := range tasks {
|
||||
if task.Running {
|
||||
activeTasks++
|
||||
}
|
||||
}
|
||||
|
||||
return &client.TaskStatusResponse{
|
||||
Summary: client.TaskSummary{
|
||||
TotalTasks: len(tasks),
|
||||
ActiveTasks: activeTasks,
|
||||
},
|
||||
Tasks: tasks,
|
||||
System: client.SystemInfo{
|
||||
FreeHeap: 32768 + int64(rand.Intn(32768)),
|
||||
Uptime: int64(time.Now().Unix() - 3600*24), // 24 hours uptime
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMockSystemStatus generates mock system status data
|
||||
func GenerateMockSystemStatus(labels map[string]string) *client.SystemStatusResponse {
|
||||
return &client.SystemStatusResponse{
|
||||
FreeHeap: 32768 + int64(rand.Intn(32768)),
|
||||
ChipID: int64(rand.Int31()),
|
||||
SDKVersion: "3.1.0",
|
||||
CPUFreqMHz: 80 + rand.Intn(160),
|
||||
FlashChipSize: 4194304,
|
||||
Labels: labels,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMockCapabilities generates mock API endpoint capabilities
|
||||
func GenerateMockCapabilities() *client.CapabilitiesResponse {
|
||||
return &client.CapabilitiesResponse{
|
||||
Endpoints: []client.EndpointInfo{
|
||||
{
|
||||
URI: "/api/node/status",
|
||||
Method: "GET",
|
||||
Parameters: []client.ParameterInfo{
|
||||
{
|
||||
Name: "detailed",
|
||||
Type: "boolean",
|
||||
Required: false,
|
||||
Description: "Include detailed system information",
|
||||
Location: "query",
|
||||
Default: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
URI: "/api/cluster/members",
|
||||
Method: "GET",
|
||||
Parameters: []client.ParameterInfo{},
|
||||
},
|
||||
{
|
||||
URI: "/api/tasks/status",
|
||||
Method: "GET",
|
||||
Parameters: []client.ParameterInfo{},
|
||||
},
|
||||
{
|
||||
URI: "/api/node/update",
|
||||
Method: "POST",
|
||||
Parameters: []client.ParameterInfo{
|
||||
{
|
||||
Name: "firmware",
|
||||
Type: "file",
|
||||
Required: true,
|
||||
Description: "Firmware binary file",
|
||||
Location: "body",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
URI: "/api/node/config",
|
||||
Method: "POST",
|
||||
Parameters: []client.ParameterInfo{
|
||||
{
|
||||
Name: "labels",
|
||||
Type: "json",
|
||||
Required: true,
|
||||
Description: "Node labels in JSON format",
|
||||
Location: "body",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
URI: "/api/sensors/read",
|
||||
Method: "GET",
|
||||
Parameters: []client.ParameterInfo{
|
||||
{
|
||||
Name: "sensor",
|
||||
Type: "string",
|
||||
Required: false,
|
||||
Description: "Specific sensor to read",
|
||||
Location: "query",
|
||||
Values: []string{"temperature", "humidity", "pressure"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMockFirmwareList generates mock firmware registry data
|
||||
func GenerateMockFirmwareList() []registry.GroupedFirmware {
|
||||
return []registry.GroupedFirmware{
|
||||
{
|
||||
Name: "spore-firmware",
|
||||
Firmware: []registry.FirmwareRecord{
|
||||
{
|
||||
Name: "spore-firmware",
|
||||
Version: "1.0.0",
|
||||
Size: 524288,
|
||||
Labels: map[string]string{
|
||||
"stable": "true",
|
||||
"env": "production",
|
||||
},
|
||||
Path: "/firmware/spore-firmware/1.0.0",
|
||||
},
|
||||
{
|
||||
Name: "spore-firmware",
|
||||
Version: "1.1.0",
|
||||
Size: 548864,
|
||||
Labels: map[string]string{
|
||||
"stable": "true",
|
||||
"env": "production",
|
||||
},
|
||||
Path: "/firmware/spore-firmware/1.1.0",
|
||||
},
|
||||
{
|
||||
Name: "spore-firmware",
|
||||
Version: "1.2.0",
|
||||
Size: 573440,
|
||||
Labels: map[string]string{
|
||||
"stable": "false",
|
||||
"env": "beta",
|
||||
},
|
||||
Path: "/firmware/spore-firmware/1.2.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "sensor-firmware",
|
||||
Firmware: []registry.FirmwareRecord{
|
||||
{
|
||||
Name: "sensor-firmware",
|
||||
Version: "2.0.0",
|
||||
Size: 262144,
|
||||
Labels: map[string]string{
|
||||
"stable": "true",
|
||||
"type": "sensor",
|
||||
},
|
||||
Path: "/firmware/sensor-firmware/2.0.0",
|
||||
},
|
||||
{
|
||||
Name: "sensor-firmware",
|
||||
Version: "2.1.0",
|
||||
Size: 286720,
|
||||
Labels: map[string]string{
|
||||
"stable": "true",
|
||||
"type": "sensor",
|
||||
},
|
||||
Path: "/firmware/sensor-firmware/2.1.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMockFirmwareBinary generates mock firmware binary data
|
||||
func GenerateMockFirmwareBinary(size int) []byte {
|
||||
// Generate some pseudo-random but deterministic binary data
|
||||
data := make([]byte, size)
|
||||
for i := range data {
|
||||
data[i] = byte(i % 256)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// NodeInfo is an alias to avoid import cycle
|
||||
type NodeInfo struct {
|
||||
IP string
|
||||
Hostname string
|
||||
Status string
|
||||
Latency int64
|
||||
LastSeen time.Time
|
||||
Labels map[string]string
|
||||
}
|
||||
|
||||
// ResourceMetrics represents resource usage metrics for a node
|
||||
type ResourceMetrics struct {
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NodeIP string `json:"node_ip"`
|
||||
Hostname string `json:"hostname"`
|
||||
CPU CPUMetrics `json:"cpu"`
|
||||
Memory MemoryMetrics `json:"memory"`
|
||||
Network NetworkMetrics `json:"network"`
|
||||
Flash FlashMetrics `json:"flash"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// CPUMetrics represents CPU usage metrics
|
||||
type CPUMetrics struct {
|
||||
Frequency int `json:"frequency_mhz"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
Temperature float64 `json:"temperature_c,omitempty"`
|
||||
}
|
||||
|
||||
// MemoryMetrics represents memory usage metrics
|
||||
type MemoryMetrics struct {
|
||||
Total int64 `json:"total_bytes"`
|
||||
Free int64 `json:"free_bytes"`
|
||||
Used int64 `json:"used_bytes"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
// NetworkMetrics represents network usage metrics
|
||||
type NetworkMetrics struct {
|
||||
BytesSent int64 `json:"bytes_sent"`
|
||||
BytesReceived int64 `json:"bytes_received"`
|
||||
PacketsSent int64 `json:"packets_sent"`
|
||||
PacketsRecv int64 `json:"packets_received"`
|
||||
RSSI int `json:"rssi_dbm,omitempty"`
|
||||
SignalQuality float64 `json:"signal_quality_percent,omitempty"`
|
||||
}
|
||||
|
||||
// FlashMetrics represents flash storage metrics
|
||||
type FlashMetrics struct {
|
||||
Total int64 `json:"total_bytes"`
|
||||
Used int64 `json:"used_bytes"`
|
||||
Free int64 `json:"free_bytes"`
|
||||
UsagePercent float64 `json:"usage_percent"`
|
||||
}
|
||||
|
||||
// MonitoringResourcesResponse represents the monitoring resources endpoint response
|
||||
type MonitoringResourcesResponse struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Nodes []ResourceMetrics `json:"nodes"`
|
||||
Summary ResourceSummary `json:"summary"`
|
||||
}
|
||||
|
||||
// ResourceSummary provides aggregate statistics across all nodes
|
||||
type ResourceSummary struct {
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
AvgCPUUsage float64 `json:"avg_cpu_usage_percent"`
|
||||
AvgMemoryUsage float64 `json:"avg_memory_usage_percent"`
|
||||
AvgFlashUsage float64 `json:"avg_flash_usage_percent"`
|
||||
TotalBytesSent int64 `json:"total_bytes_sent"`
|
||||
TotalBytesRecv int64 `json:"total_bytes_received"`
|
||||
}
|
||||
|
||||
// GenerateMockMonitoringResources generates meaningful mock monitoring data for all nodes
|
||||
func GenerateMockMonitoringResources(nodes map[string]*NodeInfo) *MonitoringResourcesResponse {
|
||||
now := time.Now()
|
||||
metrics := make([]ResourceMetrics, 0, len(nodes))
|
||||
|
||||
var totalCPU, totalMemoryPercent, totalFlash float64
|
||||
var totalBytesSent, totalBytesRecv int64
|
||||
|
||||
for _, node := range nodes {
|
||||
// Generate realistic resource usage based on node characteristics
|
||||
cpuFreq := 80 + rand.Intn(160)
|
||||
cpuUsage := 15.0 + rand.Float64()*45.0 // 15-60% usage
|
||||
|
||||
// Memory metrics
|
||||
nodeMemoryTotal := int64(65536 + rand.Intn(65536)) // 64-128KB
|
||||
freeMemory := int64(32768 + rand.Intn(32768)) // 32-64KB free
|
||||
usedMemory := nodeMemoryTotal - freeMemory
|
||||
memoryUsagePercent := float64(usedMemory) / float64(nodeMemoryTotal) * 100
|
||||
|
||||
// Flash metrics
|
||||
flashTotal := int64(4194304) // 4MB
|
||||
flashUsed := int64(1048576 + rand.Intn(2097152)) // 1-3MB used
|
||||
flashFree := flashTotal - flashUsed
|
||||
flashUsagePercent := float64(flashUsed) / float64(flashTotal) * 100
|
||||
|
||||
// Network metrics (simulating accumulated traffic)
|
||||
bytesSent := int64(1000000 + rand.Intn(5000000)) // 1-6MB
|
||||
bytesRecv := int64(2000000 + rand.Intn(8000000)) // 2-10MB
|
||||
packetsSent := int64(10000 + rand.Intn(50000))
|
||||
packetsRecv := int64(15000 + rand.Intn(75000))
|
||||
|
||||
// WiFi signal metrics
|
||||
rssi := -30 - rand.Intn(60) // -30 to -90 dBm
|
||||
signalQuality := float64(100+rssi+90) / 60.0 * 100 // Convert RSSI to quality percentage
|
||||
if signalQuality < 0 {
|
||||
signalQuality = 0
|
||||
} else if signalQuality > 100 {
|
||||
signalQuality = 100
|
||||
}
|
||||
|
||||
metric := ResourceMetrics{
|
||||
Timestamp: now.Unix(),
|
||||
NodeIP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
CPU: CPUMetrics{
|
||||
Frequency: cpuFreq,
|
||||
UsagePercent: cpuUsage,
|
||||
Temperature: 45.0 + rand.Float64()*20.0, // 45-65°C
|
||||
},
|
||||
Memory: MemoryMetrics{
|
||||
Total: nodeMemoryTotal,
|
||||
Free: freeMemory,
|
||||
Used: usedMemory,
|
||||
UsagePercent: memoryUsagePercent,
|
||||
},
|
||||
Network: NetworkMetrics{
|
||||
BytesSent: bytesSent,
|
||||
BytesReceived: bytesRecv,
|
||||
PacketsSent: packetsSent,
|
||||
PacketsRecv: packetsRecv,
|
||||
RSSI: rssi,
|
||||
SignalQuality: signalQuality,
|
||||
},
|
||||
Flash: FlashMetrics{
|
||||
Total: flashTotal,
|
||||
Used: flashUsed,
|
||||
Free: flashFree,
|
||||
UsagePercent: flashUsagePercent,
|
||||
},
|
||||
Labels: node.Labels,
|
||||
}
|
||||
|
||||
metrics = append(metrics, metric)
|
||||
|
||||
// Accumulate for summary
|
||||
totalCPU += cpuUsage
|
||||
totalMemoryPercent += memoryUsagePercent
|
||||
totalFlash += flashUsagePercent
|
||||
totalBytesSent += bytesSent
|
||||
totalBytesRecv += bytesRecv
|
||||
}
|
||||
|
||||
// Calculate averages
|
||||
nodeCount := len(nodes)
|
||||
var avgCPU, avgMemory, avgFlash float64
|
||||
if nodeCount > 0 {
|
||||
avgCPU = totalCPU / float64(nodeCount)
|
||||
avgMemory = totalMemoryPercent / float64(nodeCount)
|
||||
avgFlash = totalFlash / float64(nodeCount)
|
||||
}
|
||||
|
||||
return &MonitoringResourcesResponse{
|
||||
Timestamp: now.Format(time.RFC3339),
|
||||
Nodes: metrics,
|
||||
Summary: ResourceSummary{
|
||||
TotalNodes: nodeCount,
|
||||
AvgCPUUsage: avgCPU,
|
||||
AvgMemoryUsage: avgMemory,
|
||||
AvgFlashUsage: avgFlash,
|
||||
TotalBytesSent: totalBytesSent,
|
||||
TotalBytesRecv: totalBytesRecv,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateMockProxyResponse generates a mock response for proxy calls
|
||||
func GenerateMockProxyResponse(method, uri string) map[string]interface{} {
|
||||
switch uri {
|
||||
case "/api/sensors/read":
|
||||
return map[string]interface{}{
|
||||
"temperature": 22.5 + rand.Float64()*5,
|
||||
"humidity": 45.0 + rand.Float64()*20,
|
||||
"pressure": 1013.0 + rand.Float64()*10,
|
||||
"timestamp": time.Now().Unix(),
|
||||
}
|
||||
case "/api/led/control":
|
||||
return map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": "LED state updated",
|
||||
"state": "on",
|
||||
}
|
||||
default:
|
||||
return map[string]interface{}{
|
||||
"status": "success",
|
||||
"message": fmt.Sprintf("Mock response for %s %s", method, uri),
|
||||
"data": map[string]interface{}{"mock": true},
|
||||
}
|
||||
}
|
||||
}
|
||||
274
internal/mock/discovery.go
Normal file
274
internal/mock/discovery.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"spore-gateway/internal/discovery"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// MockNodeDiscovery simulates node discovery with mock nodes
|
||||
type MockNodeDiscovery struct {
|
||||
mockNodes map[string]*discovery.NodeInfo
|
||||
primaryNode string
|
||||
mutex sync.RWMutex
|
||||
callbacks []discovery.NodeUpdateCallback
|
||||
heartbeatRate time.Duration
|
||||
shutdownChan chan struct{}
|
||||
shutdownOnce sync.Once
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewMockNodeDiscovery creates a new mock node discovery instance
|
||||
func NewMockNodeDiscovery(numNodes int, heartbeatRate time.Duration) *MockNodeDiscovery {
|
||||
mnd := &MockNodeDiscovery{
|
||||
mockNodes: make(map[string]*discovery.NodeInfo),
|
||||
heartbeatRate: heartbeatRate,
|
||||
shutdownChan: make(chan struct{}),
|
||||
logger: log.New(),
|
||||
}
|
||||
|
||||
// Generate mock nodes with realistic firmware versions
|
||||
// These versions should match the firmware available in the registry
|
||||
now := time.Now()
|
||||
for i := 0; i < numNodes; i++ {
|
||||
ip := fmt.Sprintf("192.168.1.%d", 100+i)
|
||||
hostname := fmt.Sprintf("spore-node-%d", i+1)
|
||||
|
||||
// Distribute nodes across different firmware versions
|
||||
// Most nodes on stable versions, some on beta
|
||||
var version string
|
||||
switch i % 5 {
|
||||
case 0, 1:
|
||||
version = "1.0.0" // 40% on oldest stable
|
||||
case 2, 3:
|
||||
version = "1.1.0" // 40% on newer stable
|
||||
case 4:
|
||||
version = "1.2.0" // 20% on beta
|
||||
}
|
||||
|
||||
// Determine stability based on version
|
||||
stable := "true"
|
||||
env := "production"
|
||||
if version == "1.2.0" {
|
||||
stable = "false"
|
||||
env = "beta"
|
||||
}
|
||||
|
||||
nodeInfo := &discovery.NodeInfo{
|
||||
IP: ip,
|
||||
Port: 80,
|
||||
Hostname: hostname,
|
||||
Status: discovery.NodeStatusActive,
|
||||
DiscoveredAt: now.Add(-time.Duration(i*5) * time.Minute),
|
||||
LastSeen: now,
|
||||
Uptime: fmt.Sprintf("%dh%dm", 10+i, rand.Intn(60)),
|
||||
Labels: map[string]string{
|
||||
"version": version,
|
||||
"stable": stable,
|
||||
"env": env,
|
||||
"zone": fmt.Sprintf("zone-%d", (i%3)+1),
|
||||
"type": "spore-node",
|
||||
},
|
||||
Latency: int64(10 + rand.Intn(50)),
|
||||
Resources: map[string]interface{}{
|
||||
"freeHeap": 32768 + rand.Intn(32768),
|
||||
"cpuFreqMHz": 80 + rand.Intn(160),
|
||||
"flashChipSize": 4194304,
|
||||
},
|
||||
}
|
||||
|
||||
mnd.mockNodes[ip] = nodeInfo
|
||||
}
|
||||
|
||||
// Set first node as primary
|
||||
if numNodes > 0 {
|
||||
mnd.primaryNode = fmt.Sprintf("192.168.1.%d", 100)
|
||||
}
|
||||
|
||||
mnd.logger.WithField("nodes", numNodes).Info("Mock discovery initialized with nodes")
|
||||
return mnd
|
||||
}
|
||||
|
||||
// Start starts the mock discovery (simulates periodic heartbeats)
|
||||
func (mnd *MockNodeDiscovery) Start() error {
|
||||
mnd.logger.Info("Starting mock node discovery")
|
||||
|
||||
// Simulate periodic node updates
|
||||
go mnd.simulateHeartbeats()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the mock discovery
|
||||
func (mnd *MockNodeDiscovery) Shutdown(ctx context.Context) error {
|
||||
mnd.shutdownOnce.Do(func() {
|
||||
mnd.logger.Info("Shutting down mock node discovery")
|
||||
close(mnd.shutdownChan)
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// simulateHeartbeats simulates periodic node heartbeats and updates
|
||||
func (mnd *MockNodeDiscovery) simulateHeartbeats() {
|
||||
ticker := time.NewTicker(mnd.heartbeatRate)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mnd.shutdownChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
mnd.updateMockNodes()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// updateMockNodes simulates node updates (version changes, status changes, etc.)
|
||||
func (mnd *MockNodeDiscovery) updateMockNodes() {
|
||||
mnd.mutex.Lock()
|
||||
defer mnd.mutex.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
|
||||
for ip, node := range mnd.mockNodes {
|
||||
// Update last seen time
|
||||
node.LastSeen = now
|
||||
|
||||
// Randomly update some metrics
|
||||
if rand.Float32() < 0.3 { // 30% chance
|
||||
node.Latency = int64(10 + rand.Intn(50))
|
||||
// Update resources map
|
||||
node.Resources["freeHeap"] = 32768 + rand.Intn(32768)
|
||||
}
|
||||
|
||||
// Occasionally notify about updates
|
||||
if rand.Float32() < 0.1 { // 10% chance
|
||||
mnd.notifyCallbacks(ip, "heartbeat")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetNodes returns a copy of all mock nodes
|
||||
func (mnd *MockNodeDiscovery) GetNodes() map[string]*discovery.NodeInfo {
|
||||
mnd.mutex.RLock()
|
||||
defer mnd.mutex.RUnlock()
|
||||
|
||||
nodes := make(map[string]*discovery.NodeInfo)
|
||||
for ip, node := range mnd.mockNodes {
|
||||
// Create a copy
|
||||
nodeCopy := *node
|
||||
nodes[ip] = &nodeCopy
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
|
||||
// GetPrimaryNode returns the current primary node IP
|
||||
func (mnd *MockNodeDiscovery) GetPrimaryNode() string {
|
||||
mnd.mutex.RLock()
|
||||
defer mnd.mutex.RUnlock()
|
||||
return mnd.primaryNode
|
||||
}
|
||||
|
||||
// SetPrimaryNode manually sets the primary node
|
||||
func (mnd *MockNodeDiscovery) SetPrimaryNode(ip string) error {
|
||||
mnd.mutex.Lock()
|
||||
defer mnd.mutex.Unlock()
|
||||
|
||||
if _, exists := mnd.mockNodes[ip]; !exists {
|
||||
return fmt.Errorf("node %s not found", ip)
|
||||
}
|
||||
|
||||
oldPrimary := mnd.primaryNode
|
||||
mnd.primaryNode = ip
|
||||
mnd.logger.WithFields(log.Fields{
|
||||
"old_primary": oldPrimary,
|
||||
"new_primary": ip,
|
||||
}).Info("Primary node changed")
|
||||
|
||||
mnd.notifyCallbacks(ip, "primary_changed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// SelectRandomPrimaryNode selects a random active node as primary
|
||||
func (mnd *MockNodeDiscovery) SelectRandomPrimaryNode() string {
|
||||
mnd.mutex.Lock()
|
||||
defer mnd.mutex.Unlock()
|
||||
|
||||
if len(mnd.mockNodes) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Get active nodes
|
||||
var activeNodes []string
|
||||
for ip, node := range mnd.mockNodes {
|
||||
if node.Status == discovery.NodeStatusActive {
|
||||
activeNodes = append(activeNodes, ip)
|
||||
}
|
||||
}
|
||||
|
||||
if len(activeNodes) == 0 {
|
||||
return mnd.primaryNode
|
||||
}
|
||||
|
||||
// Select random node
|
||||
randomIndex := rand.Intn(len(activeNodes))
|
||||
randomNode := activeNodes[randomIndex]
|
||||
|
||||
oldPrimary := mnd.primaryNode
|
||||
mnd.primaryNode = randomNode
|
||||
mnd.logger.WithFields(log.Fields{
|
||||
"old_primary": oldPrimary,
|
||||
"new_primary": randomNode,
|
||||
}).Info("Randomly selected new primary node")
|
||||
|
||||
mnd.notifyCallbacks(randomNode, "primary_changed")
|
||||
return randomNode
|
||||
}
|
||||
|
||||
// AddCallback registers a callback for node updates
|
||||
func (mnd *MockNodeDiscovery) AddCallback(callback discovery.NodeUpdateCallback) {
|
||||
mnd.mutex.Lock()
|
||||
defer mnd.mutex.Unlock()
|
||||
mnd.callbacks = append(mnd.callbacks, callback)
|
||||
}
|
||||
|
||||
// GetClusterStatus returns current cluster status
|
||||
func (mnd *MockNodeDiscovery) GetClusterStatus() discovery.ClusterStatus {
|
||||
mnd.mutex.RLock()
|
||||
defer mnd.mutex.RUnlock()
|
||||
|
||||
return discovery.ClusterStatus{
|
||||
PrimaryNode: mnd.primaryNode,
|
||||
TotalNodes: len(mnd.mockNodes),
|
||||
UDPPort: "4210",
|
||||
ServerRunning: true,
|
||||
}
|
||||
}
|
||||
|
||||
// notifyCallbacks notifies all registered callbacks about node changes
|
||||
func (mnd *MockNodeDiscovery) notifyCallbacks(nodeIP, action string) {
|
||||
for _, callback := range mnd.callbacks {
|
||||
go callback(nodeIP, action)
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateNodeVersion simulates updating a node's version (for testing rollouts)
|
||||
func (mnd *MockNodeDiscovery) UpdateNodeVersion(ip, version string) {
|
||||
mnd.mutex.Lock()
|
||||
defer mnd.mutex.Unlock()
|
||||
|
||||
if node, exists := mnd.mockNodes[ip]; exists {
|
||||
node.Labels["version"] = version
|
||||
mnd.logger.WithFields(log.Fields{
|
||||
"ip": ip,
|
||||
"version": version,
|
||||
}).Info("Updated node version")
|
||||
mnd.notifyCallbacks(ip, "version_updated")
|
||||
}
|
||||
}
|
||||
752
internal/mock/server.go
Normal file
752
internal/mock/server.go
Normal file
@@ -0,0 +1,752 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"spore-gateway/internal/discovery"
|
||||
"spore-gateway/pkg/client"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// MockHTTPServer represents the mock HTTP server
|
||||
type MockHTTPServer struct {
|
||||
port string
|
||||
router *mux.Router
|
||||
discovery *MockNodeDiscovery
|
||||
wsServer *MockWebSocketServer
|
||||
server *http.Server
|
||||
enableWS bool
|
||||
firmwareStore map[string][]byte // Simple in-memory firmware storage
|
||||
}
|
||||
|
||||
// NewMockHTTPServer creates a new mock HTTP server instance
|
||||
func NewMockHTTPServer(port string, discovery *MockNodeDiscovery, enableWS bool) *MockHTTPServer {
|
||||
ms := &MockHTTPServer{
|
||||
port: port,
|
||||
router: mux.NewRouter(),
|
||||
discovery: discovery,
|
||||
enableWS: enableWS,
|
||||
firmwareStore: make(map[string][]byte),
|
||||
}
|
||||
|
||||
// Initialize WebSocket server if enabled
|
||||
if enableWS {
|
||||
ms.wsServer = NewMockWebSocketServer(discovery)
|
||||
}
|
||||
|
||||
ms.setupRoutes()
|
||||
ms.setupMiddleware()
|
||||
|
||||
ms.server = &http.Server{
|
||||
Addr: ":" + port,
|
||||
Handler: ms.router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
return ms
|
||||
}
|
||||
|
||||
// setupMiddleware configures middleware for the server
|
||||
func (ms *MockHTTPServer) setupMiddleware() {
|
||||
ms.router.Use(ms.corsMiddleware)
|
||||
ms.router.Use(ms.jsonMiddleware)
|
||||
ms.router.Use(ms.loggingMiddleware)
|
||||
}
|
||||
|
||||
// corsMiddleware handles CORS headers
|
||||
func (ms *MockHTTPServer) corsMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Content-Type, Content-Length")
|
||||
|
||||
if r.Method == "OPTIONS" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// jsonMiddleware sets JSON content type
|
||||
func (ms *MockHTTPServer) jsonMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// loggingMiddleware logs HTTP requests
|
||||
func (ms *MockHTTPServer) loggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
next.ServeHTTP(w, r)
|
||||
log.WithFields(log.Fields{
|
||||
"method": r.Method,
|
||||
"path": r.URL.Path,
|
||||
"remote_addr": r.RemoteAddr,
|
||||
"duration": time.Since(start),
|
||||
}).Debug("HTTP request")
|
||||
})
|
||||
}
|
||||
|
||||
// setupRoutes configures all the API routes
|
||||
func (ms *MockHTTPServer) setupRoutes() {
|
||||
// API routes
|
||||
api := ms.router.PathPrefix("/api").Subrouter()
|
||||
api.Use(ms.corsMiddleware)
|
||||
|
||||
// Discovery endpoints
|
||||
api.HandleFunc("/discovery/nodes", ms.getDiscoveryNodes).Methods("GET")
|
||||
api.HandleFunc("/discovery/refresh", ms.refreshDiscovery).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/discovery/random-primary", ms.selectRandomPrimary).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/discovery/primary/{ip}", ms.setPrimaryNode).Methods("POST", "OPTIONS")
|
||||
|
||||
// Cluster endpoints
|
||||
api.HandleFunc("/cluster/members", ms.getClusterMembers).Methods("GET")
|
||||
api.HandleFunc("/cluster/refresh", ms.refreshCluster).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/cluster/node/versions", ms.getClusterNodeVersions).Methods("GET")
|
||||
api.HandleFunc("/rollout", ms.startRollout).Methods("POST", "OPTIONS")
|
||||
|
||||
// Task endpoints
|
||||
api.HandleFunc("/tasks/status", ms.getTaskStatus).Methods("GET")
|
||||
|
||||
// Node endpoints
|
||||
api.HandleFunc("/node/status", ms.getNodeStatus).Methods("GET")
|
||||
api.HandleFunc("/node/status/{ip}", ms.getNodeStatusByIP).Methods("GET")
|
||||
api.HandleFunc("/node/endpoints", ms.getNodeEndpoints).Methods("GET")
|
||||
api.HandleFunc("/node/update", ms.updateNodeFirmware).Methods("POST", "OPTIONS")
|
||||
|
||||
// Proxy endpoints
|
||||
api.HandleFunc("/proxy-call", ms.proxyCall).Methods("POST", "OPTIONS")
|
||||
|
||||
// Registry proxy endpoints
|
||||
api.HandleFunc("/registry/health", ms.getRegistryHealth).Methods("GET")
|
||||
api.HandleFunc("/registry/firmware", ms.listRegistryFirmware).Methods("GET")
|
||||
api.HandleFunc("/registry/firmware", ms.uploadRegistryFirmware).Methods("POST", "OPTIONS")
|
||||
api.HandleFunc("/registry/firmware/{name}/{version}", ms.downloadRegistryFirmware).Methods("GET")
|
||||
api.HandleFunc("/registry/firmware/{name}/{version}", ms.updateRegistryFirmware).Methods("PUT", "OPTIONS")
|
||||
api.HandleFunc("/registry/firmware/{name}/{version}", ms.deleteRegistryFirmware).Methods("DELETE", "OPTIONS")
|
||||
|
||||
// Monitoring endpoints
|
||||
api.HandleFunc("/monitoring/resources", ms.getMonitoringResources).Methods("GET")
|
||||
|
||||
// Test endpoints
|
||||
api.HandleFunc("/test/websocket", ms.testWebSocket).Methods("POST", "OPTIONS")
|
||||
|
||||
// Health check
|
||||
api.HandleFunc("/health", ms.healthCheck).Methods("GET")
|
||||
|
||||
// WebSocket endpoint
|
||||
if ms.enableWS {
|
||||
ms.router.HandleFunc("/ws", ms.corsMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := ms.wsServer.HandleWebSocket(w, r); err != nil {
|
||||
log.WithError(err).Error("WebSocket connection failed")
|
||||
http.Error(w, "WebSocket upgrade failed", http.StatusBadRequest)
|
||||
}
|
||||
})).ServeHTTP)
|
||||
}
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (ms *MockHTTPServer) Start() error {
|
||||
log.WithField("port", ms.port).Info("Starting mock HTTP server")
|
||||
return ms.server.ListenAndServe()
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the HTTP server
|
||||
func (ms *MockHTTPServer) Shutdown(ctx context.Context) error {
|
||||
log.Info("Shutting down mock HTTP server")
|
||||
|
||||
// Shutdown WebSocket server if enabled
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
if err := ms.wsServer.Shutdown(ctx); err != nil {
|
||||
log.WithError(err).Error("WebSocket server shutdown error")
|
||||
}
|
||||
}
|
||||
|
||||
return ms.server.Shutdown(ctx)
|
||||
}
|
||||
|
||||
// API endpoint handlers
|
||||
|
||||
// GET /api/discovery/nodes
|
||||
func (ms *MockHTTPServer) getDiscoveryNodes(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
primaryNode := ms.discovery.GetPrimaryNode()
|
||||
clusterStatus := ms.discovery.GetClusterStatus()
|
||||
|
||||
type NodeResponse struct {
|
||||
*discovery.NodeInfo
|
||||
IsPrimary bool `json:"isPrimary"`
|
||||
}
|
||||
|
||||
response := struct {
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
Nodes []NodeResponse `json:"nodes"`
|
||||
ClientInitialized bool `json:"clientInitialized"`
|
||||
ClientBaseURL string `json:"clientBaseUrl"`
|
||||
ClusterStatus discovery.ClusterStatus `json:"clusterStatus"`
|
||||
}{
|
||||
PrimaryNode: primaryNode,
|
||||
TotalNodes: len(nodes),
|
||||
Nodes: make([]NodeResponse, 0, len(nodes)),
|
||||
ClientInitialized: primaryNode != "",
|
||||
ClientBaseURL: fmt.Sprintf("http://%s", primaryNode),
|
||||
ClusterStatus: clusterStatus,
|
||||
}
|
||||
|
||||
for _, node := range nodes {
|
||||
nodeResponse := NodeResponse{
|
||||
NodeInfo: node,
|
||||
IsPrimary: node.IP == primaryNode,
|
||||
}
|
||||
response.Nodes = append(response.Nodes, nodeResponse)
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/discovery/refresh
|
||||
func (ms *MockHTTPServer) refreshDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
ClientInitialized bool `json:"clientInitialized"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: "Mock cluster refresh completed",
|
||||
PrimaryNode: ms.discovery.GetPrimaryNode(),
|
||||
TotalNodes: len(ms.discovery.GetNodes()),
|
||||
ClientInitialized: ms.discovery.GetPrimaryNode() != "",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/discovery/random-primary
|
||||
func (ms *MockHTTPServer) selectRandomPrimary(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
if len(nodes) == 0 {
|
||||
http.Error(w, `{"error": "No nodes available"}`, http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
newPrimary := ms.discovery.SelectRandomPrimaryNode()
|
||||
if newPrimary == "" {
|
||||
http.Error(w, `{"error": "Selection failed"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
ClientInitialized bool `json:"clientInitialized"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Randomly selected new primary node: %s", newPrimary),
|
||||
PrimaryNode: newPrimary,
|
||||
TotalNodes: len(nodes),
|
||||
ClientInitialized: true,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/discovery/primary/{ip}
|
||||
func (ms *MockHTTPServer) setPrimaryNode(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
requestedIP := vars["ip"]
|
||||
|
||||
if err := ms.discovery.SetPrimaryNode(requestedIP); err != nil {
|
||||
http.Error(w, fmt.Sprintf(`{"error": "Node not found", "message": "Node with IP %s not found"}`, requestedIP), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
ClientInitialized bool `json:"clientInitialized"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Primary node set to %s", requestedIP),
|
||||
PrimaryNode: requestedIP,
|
||||
ClientInitialized: true,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/cluster/members
|
||||
func (ms *MockHTTPServer) getClusterMembers(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
|
||||
// Convert to mock format
|
||||
mockNodes := make(map[string]*NodeInfo)
|
||||
for ip, node := range nodes {
|
||||
mockNodes[ip] = &NodeInfo{
|
||||
IP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
Status: string(node.Status),
|
||||
Latency: node.Latency,
|
||||
LastSeen: node.LastSeen,
|
||||
Labels: node.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
members := GenerateMockClusterMembers(mockNodes)
|
||||
|
||||
response := &client.ClusterStatusResponse{
|
||||
Members: members,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/cluster/refresh
|
||||
func (ms *MockHTTPServer) refreshCluster(w http.ResponseWriter, r *http.Request) {
|
||||
var requestBody struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil && err.Error() != "EOF" {
|
||||
requestBody.Reason = "manual_refresh"
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason"`
|
||||
WSclients int `json:"wsClients"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: "Mock cluster refresh triggered",
|
||||
Reason: requestBody.Reason,
|
||||
}
|
||||
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
response.WSclients = ms.wsServer.GetClientCount()
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/cluster/node/versions
|
||||
func (ms *MockHTTPServer) getClusterNodeVersions(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
|
||||
type NodeVersionInfo struct {
|
||||
IP string `json:"ip"`
|
||||
Version string `json:"version"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
var nodeVersions []NodeVersionInfo
|
||||
for _, node := range nodes {
|
||||
version := "unknown"
|
||||
if v, exists := node.Labels["version"]; exists {
|
||||
version = v
|
||||
}
|
||||
|
||||
nodeVersions = append(nodeVersions, NodeVersionInfo{
|
||||
IP: node.IP,
|
||||
Version: version,
|
||||
Labels: node.Labels,
|
||||
})
|
||||
}
|
||||
|
||||
response := struct {
|
||||
Nodes []NodeVersionInfo `json:"nodes"`
|
||||
}{
|
||||
Nodes: nodeVersions,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// RolloutNode represents a node in a rollout request
|
||||
type RolloutNode struct {
|
||||
IP string `json:"ip"`
|
||||
Version string `json:"version"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
// POST /api/rollout
|
||||
func (ms *MockHTTPServer) startRollout(w http.ResponseWriter, r *http.Request) {
|
||||
var request struct {
|
||||
Firmware struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Labels map[string]string `json:"labels"`
|
||||
} `json:"firmware"`
|
||||
Nodes []RolloutNode `json:"nodes"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
rolloutID := fmt.Sprintf("rollout_%d", time.Now().Unix())
|
||||
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
RolloutID string `json:"rolloutId"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
FirmwareURL string `json:"firmwareUrl"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: fmt.Sprintf("Mock rollout started for %d nodes", len(request.Nodes)),
|
||||
RolloutID: rolloutID,
|
||||
TotalNodes: len(request.Nodes),
|
||||
FirmwareURL: fmt.Sprintf("http://localhost:3002/firmware/%s/%s", request.Firmware.Name, request.Firmware.Version),
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
// Simulate rollout progress in background
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
go ms.simulateRollout(rolloutID, request.Nodes, request.Firmware.Version)
|
||||
}
|
||||
}
|
||||
|
||||
// simulateRollout simulates a rollout process with progress updates
|
||||
func (ms *MockHTTPServer) simulateRollout(rolloutID string, nodes []RolloutNode, newVersion string) {
|
||||
for i, node := range nodes {
|
||||
// Simulate updating labels
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if ms.wsServer != nil {
|
||||
ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "updating_labels", i+1, len(nodes))
|
||||
}
|
||||
|
||||
// Simulate uploading
|
||||
time.Sleep(1 * time.Second)
|
||||
if ms.wsServer != nil {
|
||||
ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "uploading", i+1, len(nodes))
|
||||
}
|
||||
|
||||
// Update node version in discovery
|
||||
ms.discovery.UpdateNodeVersion(node.IP, strings.TrimPrefix(newVersion, "v"))
|
||||
|
||||
// Simulate completion
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if ms.wsServer != nil {
|
||||
ms.wsServer.BroadcastRolloutProgress(rolloutID, node.IP, "completed", i+1, len(nodes))
|
||||
}
|
||||
}
|
||||
|
||||
log.WithField("rollout_id", rolloutID).Info("Mock rollout completed")
|
||||
}
|
||||
|
||||
// GET /api/tasks/status
|
||||
func (ms *MockHTTPServer) getTaskStatus(w http.ResponseWriter, r *http.Request) {
|
||||
response := GenerateMockTaskStatus()
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/node/status
|
||||
func (ms *MockHTTPServer) getNodeStatus(w http.ResponseWriter, r *http.Request) {
|
||||
primaryNode := ms.discovery.GetPrimaryNode()
|
||||
nodes := ms.discovery.GetNodes()
|
||||
|
||||
var labels map[string]string
|
||||
if primaryNode != "" && nodes[primaryNode] != nil {
|
||||
labels = nodes[primaryNode].Labels
|
||||
} else {
|
||||
labels = make(map[string]string)
|
||||
}
|
||||
|
||||
response := GenerateMockSystemStatus(labels)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/node/status/{ip}
|
||||
func (ms *MockHTTPServer) getNodeStatusByIP(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
nodeIP := vars["ip"]
|
||||
|
||||
nodes := ms.discovery.GetNodes()
|
||||
node, exists := nodes[nodeIP]
|
||||
if !exists {
|
||||
http.Error(w, fmt.Sprintf(`{"error": "Node not found", "message": "Node with IP %s not found"}`, nodeIP), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
response := GenerateMockSystemStatus(node.Labels)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/node/endpoints
|
||||
func (ms *MockHTTPServer) getNodeEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
response := GenerateMockCapabilities()
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/node/update
|
||||
func (ms *MockHTTPServer) updateNodeFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
nodeIP := r.URL.Query().Get("ip")
|
||||
if nodeIP == "" {
|
||||
nodeIP = r.Header.Get("X-Node-IP")
|
||||
}
|
||||
|
||||
if nodeIP == "" {
|
||||
http.Error(w, `{"error": "Node IP required"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse multipart form
|
||||
err := r.ParseMultipartForm(50 << 20) // 50MB limit
|
||||
if err != nil {
|
||||
http.Error(w, `{"error": "Failed to parse form"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
file, fileHeader, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
http.Error(w, `{"error": "No file received"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
filename := fileHeader.Filename
|
||||
if filename == "" {
|
||||
filename = "firmware.bin"
|
||||
}
|
||||
|
||||
// Read file data
|
||||
fileData, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
http.Error(w, `{"error": "Failed to read file"}`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
log.WithFields(log.Fields{
|
||||
"node_ip": nodeIP,
|
||||
"file_size": len(fileData),
|
||||
"filename": filename,
|
||||
}).Info("Mock firmware upload received")
|
||||
|
||||
// Store firmware in memory
|
||||
ms.firmwareStore[nodeIP] = fileData
|
||||
|
||||
// Broadcast status if WebSocket enabled
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "uploading", filename, len(fileData))
|
||||
}
|
||||
|
||||
// Send immediate response
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
NodeIP string `json:"nodeIp"`
|
||||
FileSize int `json:"fileSize"`
|
||||
Filename string `json:"filename"`
|
||||
Status string `json:"status"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: "Mock firmware upload received",
|
||||
NodeIP: nodeIP,
|
||||
FileSize: len(fileData),
|
||||
Filename: filename,
|
||||
Status: "processing",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
|
||||
// Simulate firmware update in background
|
||||
go func() {
|
||||
time.Sleep(2 * time.Second)
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
// Randomly succeed or fail (90% success rate)
|
||||
if rand.Float32() < 0.9 {
|
||||
ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "completed", filename, len(fileData))
|
||||
} else {
|
||||
ms.wsServer.BroadcastFirmwareUploadStatus(nodeIP, "failed", filename, len(fileData))
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// POST /api/proxy-call
|
||||
func (ms *MockHTTPServer) proxyCall(w http.ResponseWriter, r *http.Request) {
|
||||
var requestBody struct {
|
||||
IP string `json:"ip"`
|
||||
Method string `json:"method"`
|
||||
URI string `json:"uri"`
|
||||
Params []map[string]interface{} `json:"params"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
http.Error(w, `{"error": "Invalid JSON"}`, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Generate mock response based on URI
|
||||
mockResponse := GenerateMockProxyResponse(requestBody.Method, requestBody.URI)
|
||||
|
||||
// Wrap in data field for consistency
|
||||
response := map[string]interface{}{
|
||||
"data": mockResponse,
|
||||
"status": http.StatusOK,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/registry/health
|
||||
func (ms *MockHTTPServer) getRegistryHealth(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"service": "mock-registry",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/registry/firmware
|
||||
func (ms *MockHTTPServer) listRegistryFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
firmwareList := GenerateMockFirmwareList()
|
||||
json.NewEncoder(w).Encode(firmwareList)
|
||||
}
|
||||
|
||||
// POST /api/registry/firmware
|
||||
func (ms *MockHTTPServer) uploadRegistryFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Mock firmware uploaded successfully",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/registry/firmware/{name}/{version}
|
||||
func (ms *MockHTTPServer) downloadRegistryFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
name := vars["name"]
|
||||
version := vars["version"]
|
||||
|
||||
// Generate mock firmware binary
|
||||
firmwareData := GenerateMockFirmwareBinary(524288) // 512KB
|
||||
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s-%s.bin\"", name, version))
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(firmwareData)))
|
||||
|
||||
w.Write(firmwareData)
|
||||
}
|
||||
|
||||
// PUT /api/registry/firmware/{name}/{version}
|
||||
func (ms *MockHTTPServer) updateRegistryFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Mock firmware metadata updated",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// DELETE /api/registry/firmware/{name}/{version}
|
||||
func (ms *MockHTTPServer) deleteRegistryFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
response := map[string]interface{}{
|
||||
"success": true,
|
||||
"message": "Mock firmware deleted",
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// POST /api/test/websocket
|
||||
func (ms *MockHTTPServer) testWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
response := struct {
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
WSclients int `json:"websocketClients"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
}{
|
||||
Success: true,
|
||||
Message: "Mock WebSocket test broadcast sent",
|
||||
TotalNodes: len(ms.discovery.GetNodes()),
|
||||
}
|
||||
|
||||
if ms.enableWS && ms.wsServer != nil {
|
||||
response.WSclients = ms.wsServer.GetClientCount()
|
||||
// Trigger a test broadcast
|
||||
ms.wsServer.BroadcastClusterUpdate()
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/monitoring/resources
|
||||
func (ms *MockHTTPServer) getMonitoringResources(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
|
||||
// Convert to mock format
|
||||
mockNodes := make(map[string]*NodeInfo)
|
||||
for ip, node := range nodes {
|
||||
mockNodes[ip] = &NodeInfo{
|
||||
IP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
Status: string(node.Status),
|
||||
Latency: node.Latency,
|
||||
LastSeen: node.LastSeen,
|
||||
Labels: node.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
response := GenerateMockMonitoringResources(mockNodes)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// GET /api/health
|
||||
func (ms *MockHTTPServer) healthCheck(w http.ResponseWriter, r *http.Request) {
|
||||
nodes := ms.discovery.GetNodes()
|
||||
primaryNode := ms.discovery.GetPrimaryNode()
|
||||
|
||||
health := struct {
|
||||
Status string `json:"status"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Services map[string]bool `json:"services"`
|
||||
Cluster map[string]interface{} `json:"cluster"`
|
||||
Mock bool `json:"mock"`
|
||||
}{
|
||||
Status: "healthy",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
Services: map[string]bool{
|
||||
"http": true,
|
||||
"websocket": ms.enableWS,
|
||||
"discovery": true,
|
||||
"mockClient": primaryNode != "",
|
||||
},
|
||||
Cluster: map[string]interface{}{
|
||||
"totalNodes": len(nodes),
|
||||
"primaryNode": primaryNode,
|
||||
},
|
||||
Mock: true,
|
||||
}
|
||||
|
||||
json.NewEncoder(w).Encode(health)
|
||||
}
|
||||
475
internal/mock/websocket.go
Normal file
475
internal/mock/websocket.go
Normal file
@@ -0,0 +1,475 @@
|
||||
package mock
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true // Allow connections from any origin
|
||||
},
|
||||
}
|
||||
|
||||
// MockWebSocketServer manages WebSocket connections and mock broadcasts
|
||||
type MockWebSocketServer struct {
|
||||
discovery *MockNodeDiscovery
|
||||
clients map[*websocket.Conn]bool
|
||||
mutex sync.RWMutex
|
||||
writeMutex sync.Mutex
|
||||
shutdownChan chan struct{}
|
||||
shutdownOnce sync.Once
|
||||
logger *log.Logger
|
||||
}
|
||||
|
||||
// NewMockWebSocketServer creates a new mock WebSocket server
|
||||
func NewMockWebSocketServer(discovery *MockNodeDiscovery) *MockWebSocketServer {
|
||||
mws := &MockWebSocketServer{
|
||||
discovery: discovery,
|
||||
clients: make(map[*websocket.Conn]bool),
|
||||
shutdownChan: make(chan struct{}),
|
||||
logger: log.New(),
|
||||
}
|
||||
|
||||
// Register callback for node updates
|
||||
discovery.AddCallback(mws.handleNodeUpdate)
|
||||
|
||||
// Start periodic broadcasts
|
||||
go mws.startPeriodicBroadcasts()
|
||||
|
||||
return mws
|
||||
}
|
||||
|
||||
// HandleWebSocket handles WebSocket upgrade and connection
|
||||
func (mws *MockWebSocketServer) HandleWebSocket(w http.ResponseWriter, r *http.Request) error {
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to upgrade WebSocket connection")
|
||||
return err
|
||||
}
|
||||
|
||||
mws.mutex.Lock()
|
||||
mws.clients[conn] = true
|
||||
mws.mutex.Unlock()
|
||||
|
||||
mws.logger.Debug("Mock WebSocket client connected")
|
||||
|
||||
// Send current cluster state to newly connected client
|
||||
go mws.sendCurrentClusterState(conn)
|
||||
|
||||
// Handle client messages and disconnection
|
||||
go mws.handleClient(conn)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleClient handles messages from a WebSocket client
|
||||
func (mws *MockWebSocketServer) handleClient(conn *websocket.Conn) {
|
||||
defer func() {
|
||||
mws.mutex.Lock()
|
||||
delete(mws.clients, conn)
|
||||
mws.mutex.Unlock()
|
||||
conn.Close()
|
||||
mws.logger.Debug("Mock WebSocket client disconnected")
|
||||
}()
|
||||
|
||||
// Set read deadline and pong handler
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
conn.SetPongHandler(func(string) error {
|
||||
conn.SetReadDeadline(time.Now().Add(60 * time.Second))
|
||||
return nil
|
||||
})
|
||||
|
||||
// Start ping routine
|
||||
go func() {
|
||||
ticker := time.NewTicker(54 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mws.shutdownChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Read messages
|
||||
for {
|
||||
_, _, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
mws.logger.WithError(err).Error("WebSocket error")
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// sendCurrentClusterState sends the current cluster state to a newly connected client
|
||||
func (mws *MockWebSocketServer) sendCurrentClusterState(conn *websocket.Conn) {
|
||||
nodes := mws.discovery.GetNodes()
|
||||
if len(nodes) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to mock format
|
||||
mockNodes := make(map[string]*NodeInfo)
|
||||
for ip, node := range nodes {
|
||||
mockNodes[ip] = &NodeInfo{
|
||||
IP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
Status: string(node.Status),
|
||||
Latency: node.Latency,
|
||||
LastSeen: node.LastSeen,
|
||||
Labels: node.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
members := GenerateMockClusterMembers(mockNodes)
|
||||
|
||||
message := struct {
|
||||
Type string `json:"type"`
|
||||
Members interface{} `json:"members"`
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Type: "cluster_update",
|
||||
Members: members,
|
||||
PrimaryNode: mws.discovery.GetPrimaryNode(),
|
||||
TotalNodes: len(nodes),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to marshal cluster data")
|
||||
return
|
||||
}
|
||||
|
||||
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
||||
if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to send initial cluster state")
|
||||
}
|
||||
}
|
||||
|
||||
// handleNodeUpdate is called when node information changes
|
||||
func (mws *MockWebSocketServer) handleNodeUpdate(nodeIP, action string) {
|
||||
mws.logger.WithFields(log.Fields{
|
||||
"node_ip": nodeIP,
|
||||
"action": action,
|
||||
}).Debug("Mock node update received, broadcasting to WebSocket clients")
|
||||
|
||||
// Broadcast cluster update
|
||||
mws.BroadcastClusterUpdate()
|
||||
|
||||
// Also broadcast node discovery event
|
||||
mws.broadcastNodeDiscovery(nodeIP, action)
|
||||
}
|
||||
|
||||
// startPeriodicBroadcasts sends periodic updates to keep clients informed
|
||||
func (mws *MockWebSocketServer) startPeriodicBroadcasts() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-mws.shutdownChan:
|
||||
return
|
||||
case <-ticker.C:
|
||||
mws.BroadcastClusterUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastClusterUpdate sends cluster updates to all connected clients
|
||||
func (mws *MockWebSocketServer) BroadcastClusterUpdate() {
|
||||
mws.mutex.RLock()
|
||||
clients := make([]*websocket.Conn, 0, len(mws.clients))
|
||||
for client := range mws.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
mws.mutex.RUnlock()
|
||||
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nodes := mws.discovery.GetNodes()
|
||||
|
||||
// Convert to mock format
|
||||
mockNodes := make(map[string]*NodeInfo)
|
||||
for ip, node := range nodes {
|
||||
mockNodes[ip] = &NodeInfo{
|
||||
IP: node.IP,
|
||||
Hostname: node.Hostname,
|
||||
Status: string(node.Status),
|
||||
Latency: node.Latency,
|
||||
LastSeen: node.LastSeen,
|
||||
Labels: node.Labels,
|
||||
}
|
||||
}
|
||||
|
||||
members := GenerateMockClusterMembers(mockNodes)
|
||||
|
||||
message := struct {
|
||||
Type string `json:"type"`
|
||||
Members interface{} `json:"members"`
|
||||
PrimaryNode string `json:"primaryNode"`
|
||||
TotalNodes int `json:"totalNodes"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Type: "cluster_update",
|
||||
Members: members,
|
||||
PrimaryNode: mws.discovery.GetPrimaryNode(),
|
||||
TotalNodes: len(nodes),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to marshal cluster update")
|
||||
return
|
||||
}
|
||||
|
||||
mws.logger.WithField("clients", len(clients)).Debug("Broadcasting mock cluster update")
|
||||
|
||||
// Send to all clients with write synchronization
|
||||
mws.writeMutex.Lock()
|
||||
defer mws.writeMutex.Unlock()
|
||||
|
||||
for _, client := range clients {
|
||||
client.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to send cluster update to client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// broadcastNodeDiscovery sends node discovery events to all clients
|
||||
func (mws *MockWebSocketServer) broadcastNodeDiscovery(nodeIP, action string) {
|
||||
mws.mutex.RLock()
|
||||
clients := make([]*websocket.Conn, 0, len(mws.clients))
|
||||
for client := range mws.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
mws.mutex.RUnlock()
|
||||
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := struct {
|
||||
Type string `json:"type"`
|
||||
Action string `json:"action"`
|
||||
NodeIP string `json:"nodeIp"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Type: "node_discovery",
|
||||
Action: action,
|
||||
NodeIP: nodeIP,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to marshal node discovery event")
|
||||
return
|
||||
}
|
||||
|
||||
// Send to all clients with write synchronization
|
||||
mws.writeMutex.Lock()
|
||||
defer mws.writeMutex.Unlock()
|
||||
|
||||
for _, client := range clients {
|
||||
client.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to send node discovery event to client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastFirmwareUploadStatus sends firmware upload status updates to all clients
|
||||
func (mws *MockWebSocketServer) BroadcastFirmwareUploadStatus(nodeIP, status, filename string, fileSize int) {
|
||||
mws.mutex.RLock()
|
||||
clients := make([]*websocket.Conn, 0, len(mws.clients))
|
||||
for client := range mws.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
mws.mutex.RUnlock()
|
||||
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := struct {
|
||||
Type string `json:"type"`
|
||||
NodeIP string `json:"nodeIp"`
|
||||
Status string `json:"status"`
|
||||
Filename string `json:"filename"`
|
||||
FileSize int `json:"fileSize"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Type: "firmware_upload_status",
|
||||
NodeIP: nodeIP,
|
||||
Status: status,
|
||||
Filename: filename,
|
||||
FileSize: fileSize,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to marshal firmware upload status")
|
||||
return
|
||||
}
|
||||
|
||||
mws.logger.WithFields(log.Fields{
|
||||
"node_ip": nodeIP,
|
||||
"status": status,
|
||||
"clients": len(clients),
|
||||
}).Debug("Broadcasting mock firmware upload status")
|
||||
|
||||
// Send to all clients with write synchronization
|
||||
mws.writeMutex.Lock()
|
||||
defer mws.writeMutex.Unlock()
|
||||
|
||||
for _, client := range clients {
|
||||
client.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to send firmware upload status to client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BroadcastRolloutProgress sends rollout progress updates to all clients
|
||||
func (mws *MockWebSocketServer) BroadcastRolloutProgress(rolloutID, nodeIP, status string, current, total int) {
|
||||
mws.mutex.RLock()
|
||||
clients := make([]*websocket.Conn, 0, len(mws.clients))
|
||||
for client := range mws.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
mws.mutex.RUnlock()
|
||||
|
||||
if len(clients) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
message := struct {
|
||||
Type string `json:"type"`
|
||||
RolloutID string `json:"rolloutId"`
|
||||
NodeIP string `json:"nodeIp"`
|
||||
Status string `json:"status"`
|
||||
Current int `json:"current"`
|
||||
Total int `json:"total"`
|
||||
Progress int `json:"progress"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}{
|
||||
Type: "rollout_progress",
|
||||
RolloutID: rolloutID,
|
||||
NodeIP: nodeIP,
|
||||
Status: status,
|
||||
Current: current,
|
||||
Total: total,
|
||||
Progress: calculateProgress(current, total, status),
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to marshal rollout progress")
|
||||
return
|
||||
}
|
||||
|
||||
mws.logger.WithFields(log.Fields{
|
||||
"rollout_id": rolloutID,
|
||||
"node_ip": nodeIP,
|
||||
"status": status,
|
||||
"progress": fmt.Sprintf("%d/%d", current, total),
|
||||
}).Debug("Broadcasting mock rollout progress")
|
||||
|
||||
// Send to all clients with write synchronization
|
||||
mws.writeMutex.Lock()
|
||||
defer mws.writeMutex.Unlock()
|
||||
|
||||
for _, client := range clients {
|
||||
client.SetWriteDeadline(time.Now().Add(5 * time.Second))
|
||||
if err := client.WriteMessage(websocket.TextMessage, data); err != nil {
|
||||
mws.logger.WithError(err).Error("Failed to send rollout progress to client")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// calculateProgress calculates the correct progress percentage based on current status
|
||||
func calculateProgress(current, total int, status string) int {
|
||||
if total == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Base progress is based on completed nodes
|
||||
completedNodes := current - 1
|
||||
if status == "completed" {
|
||||
completedNodes = current
|
||||
}
|
||||
|
||||
// Calculate base progress (completed nodes / total nodes)
|
||||
baseProgress := float64(completedNodes) / float64(total) * 100
|
||||
|
||||
// If currently updating labels or uploading, add partial progress for the current node
|
||||
if status == "updating_labels" {
|
||||
nodeProgress := 100.0 / float64(total) * 0.25
|
||||
baseProgress += nodeProgress
|
||||
} else if status == "uploading" {
|
||||
nodeProgress := 100.0 / float64(total) * 0.5
|
||||
baseProgress += nodeProgress
|
||||
}
|
||||
|
||||
// Ensure we don't exceed 100%
|
||||
if baseProgress > 100 {
|
||||
baseProgress = 100
|
||||
}
|
||||
|
||||
return int(baseProgress)
|
||||
}
|
||||
|
||||
// GetClientCount returns the number of connected WebSocket clients
|
||||
func (mws *MockWebSocketServer) GetClientCount() int {
|
||||
mws.mutex.RLock()
|
||||
defer mws.mutex.RUnlock()
|
||||
return len(mws.clients)
|
||||
}
|
||||
|
||||
// Shutdown gracefully shuts down the WebSocket server
|
||||
func (mws *MockWebSocketServer) Shutdown(ctx context.Context) error {
|
||||
mws.shutdownOnce.Do(func() {
|
||||
mws.logger.Info("Shutting down mock WebSocket server")
|
||||
close(mws.shutdownChan)
|
||||
|
||||
mws.mutex.Lock()
|
||||
clients := make([]*websocket.Conn, 0, len(mws.clients))
|
||||
for client := range mws.clients {
|
||||
clients = append(clients, client)
|
||||
}
|
||||
mws.mutex.Unlock()
|
||||
|
||||
// Close all client connections
|
||||
for _, client := range clients {
|
||||
client.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseGoingAway, "Server shutting down"))
|
||||
client.Close()
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user