Files
spore-gateway/internal/mock/server.go
Patrick Balsiger 3c3fb886a3 feat: mock gateway
2025-10-24 14:24:14 +02:00

753 lines
22 KiB
Go

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)
}