diff --git a/api/cmd/main.go b/api/cmd/main.go index ba9f8fcb..7abd21a8 100644 --- a/api/cmd/main.go +++ b/api/cmd/main.go @@ -569,28 +569,28 @@ func setupRoutes(router *gin.Engine, h *api.Handler, userHandler *handlers.UserH scaling.GET("/autoscaling/history", loadBalancingHandler.GetScalingHistory) } -// // // Compliance & Governance - Admin only -// compliance := protected.Group("/compliance") -// compliance.Use(adminMiddleware) -// { -// // Frameworks -// compliance.GET("/frameworks", h.ListComplianceFrameworks) -// compliance.POST("/frameworks", h.CreateComplianceFramework) -// -// // Policies -// compliance.GET("/policies", h.ListCompliancePolicies) -// compliance.POST("/policies", h.CreateCompliancePolicy) -// -// // Violations -// compliance.GET("/violations", h.ListViolations) -// compliance.POST("/violations", h.RecordViolation) -// compliance.POST("/violations/:violationId/resolve", h.ResolveViolation) - -// } - -// -// NOTE: Compliance & Governance is now handled by the streamspace-compliance plugin -// Install it via: Admin → Plugins → streamspace-compliance + // Compliance & Governance - Admin only + // NOTE: These are STUB endpoints that return empty data when the compliance plugin + // is not installed. Install streamspace-compliance plugin for full functionality. + compliance := protected.Group("/compliance") + compliance.Use(adminMiddleware) + { + // Dashboard + compliance.GET("/dashboard", h.GetComplianceDashboard) + + // Frameworks + compliance.GET("/frameworks", h.ListComplianceFrameworks) + compliance.POST("/frameworks", h.CreateComplianceFramework) + + // Policies + compliance.GET("/policies", h.ListCompliancePolicies) + compliance.POST("/policies", h.CreateCompliancePolicy) + + // Violations + compliance.GET("/violations", h.ListViolations) + compliance.POST("/violations", h.RecordViolation) + compliance.POST("/violations/:violationId/resolve", h.ResolveViolation) + } // Templates (read: all users, write: operators/admins) templates := protected.Group("/templates") { diff --git a/api/internal/api/stubs.go b/api/internal/api/stubs.go index d40a0b33..33464b70 100644 --- a/api/internal/api/stubs.go +++ b/api/internal/api/stubs.go @@ -620,7 +620,7 @@ func (h *Handler) GetMetrics(c *gin.Context) { usedPods := 0 totalPods := 0 - for _, node := range nodes { + for _, node := range nodes.Items { // Check if node is ready for _, condition := range node.Status.Conditions { if condition.Type == corev1.NodeReady && condition.Status == corev1.ConditionTrue { @@ -644,7 +644,7 @@ func (h *Handler) GetMetrics(c *gin.Context) { // Get all pods to calculate resource usage pods, err := h.k8sClient.GetPods(ctx, h.namespace) if err == nil { - usedPods = len(pods) + usedPods = len(pods.Items) } // Get session counts from database @@ -715,9 +715,9 @@ func (h *Handler) GetMetrics(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "cluster": gin.H{ "nodes": gin.H{ - "total": len(nodes), + "total": len(nodes.Items), "ready": readyNodes, - "notReady": len(nodes) - readyNodes, + "notReady": len(nodes.Items) - readyNodes, }, "sessions": gin.H{ "total": sessionCounts.Total, @@ -932,3 +932,87 @@ func (h *Handler) WebhookRepositorySync(c *gin.Context) { "repositoryID": repoID, }) } + +// ==================================================================================== +// COMPLIANCE STUBS +// ==================================================================================== +// NOTE: These are stub endpoints that return empty data when the compliance plugin +// is not installed. When streamspace-compliance plugin is installed, it will +// override these endpoints with actual functionality. +// ==================================================================================== + +// ListComplianceFrameworks returns available compliance frameworks +// Stub returns empty list - install streamspace-compliance plugin for real data +func (h *Handler) ListComplianceFrameworks(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "frameworks": []gin.H{}, + }) +} + +// CreateComplianceFramework creates a new compliance framework +// Stub returns not implemented - install streamspace-compliance plugin +func (h *Handler) CreateComplianceFramework(c *gin.Context) { + c.JSON(http.StatusNotImplemented, gin.H{ + "error": "Compliance features require the streamspace-compliance plugin", + "message": "Please install the streamspace-compliance plugin from Admin → Plugins", + }) +} + +// ListCompliancePolicies returns all compliance policies +// Stub returns empty list - install streamspace-compliance plugin for real data +func (h *Handler) ListCompliancePolicies(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "policies": []gin.H{}, + }) +} + +// CreateCompliancePolicy creates a new compliance policy +// Stub returns not implemented - install streamspace-compliance plugin +func (h *Handler) CreateCompliancePolicy(c *gin.Context) { + c.JSON(http.StatusNotImplemented, gin.H{ + "error": "Compliance features require the streamspace-compliance plugin", + "message": "Please install the streamspace-compliance plugin from Admin → Plugins", + }) +} + +// ListViolations returns all compliance violations +// Stub returns empty list - install streamspace-compliance plugin for real data +func (h *Handler) ListViolations(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "violations": []gin.H{}, + }) +} + +// RecordViolation records a new compliance violation +// Stub returns not implemented - install streamspace-compliance plugin +func (h *Handler) RecordViolation(c *gin.Context) { + c.JSON(http.StatusNotImplemented, gin.H{ + "error": "Compliance features require the streamspace-compliance plugin", + "message": "Please install the streamspace-compliance plugin from Admin → Plugins", + }) +} + +// ResolveViolation marks a violation as resolved +// Stub returns not implemented - install streamspace-compliance plugin +func (h *Handler) ResolveViolation(c *gin.Context) { + c.JSON(http.StatusNotImplemented, gin.H{ + "error": "Compliance features require the streamspace-compliance plugin", + "message": "Please install the streamspace-compliance plugin from Admin → Plugins", + }) +} + +// GetComplianceDashboard returns compliance dashboard metrics +// Stub returns zero metrics - install streamspace-compliance plugin for real data +func (h *Handler) GetComplianceDashboard(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "total_policies": 0, + "active_policies": 0, + "total_open_violations": 0, + "violations_by_severity": gin.H{ + "critical": 0, + "high": 0, + "medium": 0, + "low": 0, + }, + }) +} diff --git a/api/internal/handlers/nodes.go b/api/internal/handlers/nodes.go index c23aa087..bdeab240 100644 --- a/api/internal/handlers/nodes.go +++ b/api/internal/handlers/nodes.go @@ -70,8 +70,7 @@ import ( "github.com/streamspace/streamspace/api/internal/db" "github.com/streamspace/streamspace/api/internal/k8s" corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/api/resource" ) // NodeHandler handles node management operations @@ -123,13 +122,20 @@ type NodeSystemInfo struct { } // ClusterStats represents aggregate cluster statistics +// ClusterStatsResources represents resource totals in a JSON-friendly format +type ClusterStatsResources struct { + CPU string `json:"cpu"` + Memory string `json:"memory"` + Pods int `json:"pods"` +} + type ClusterStats struct { - TotalNodes int `json:"totalNodes"` - ReadyNodes int `json:"readyNodes"` - NotReadyNodes int `json:"notReadyNodes"` - TotalCapacity corev1.ResourceList `json:"totalCapacity"` - TotalAllocatable corev1.ResourceList `json:"totalAllocatable"` - TotalUsage *ClusterUsage `json:"totalUsage,omitempty"` + TotalNodes int `json:"totalNodes"` + ReadyNodes int `json:"readyNodes"` + NotReadyNodes int `json:"notReadyNodes"` + TotalCapacity *ClusterStatsResources `json:"totalCapacity"` + TotalAllocatable *ClusterStatsResources `json:"totalAllocatable"` + TotalUsage *ClusterUsage `json:"totalUsage,omitempty"` } // ClusterUsage represents aggregate cluster usage @@ -486,30 +492,25 @@ func (h *NodeHandler) nodeToNodeInfo(node *corev1.Node) NodeInfo { // Helper function to calculate cluster statistics func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterStats { - stats := ClusterStats{ - TotalNodes: len(nodeList.Items), - ReadyNodes: 0, - NotReadyNodes: 0, - TotalCapacity: corev1.ResourceList{ - corev1.ResourceCPU: *newQuantity(0), - corev1.ResourceMemory: *newQuantity(0), - corev1.ResourcePods: *newQuantity(0), - }, - TotalAllocatable: corev1.ResourceList{ - corev1.ResourceCPU: *newQuantity(0), - corev1.ResourceMemory: *newQuantity(0), - corev1.ResourcePods: *newQuantity(0), - }, - } + // Initialize temporary resource totals + totalCapacityCPU := newQuantity(0) + totalCapacityMemory := newQuantity(0) + totalCapacityPods := newQuantity(0) + totalAllocatableCPU := newQuantity(0) + totalAllocatableMemory := newQuantity(0) + totalAllocatablePods := newQuantity(0) + + readyNodes := 0 + notReadyNodes := 0 for _, node := range nodeList.Items { // Count ready vs not ready nodes for _, condition := range node.Status.Conditions { if condition.Type == corev1.NodeReady { if condition.Status == corev1.ConditionTrue { - stats.ReadyNodes++ + readyNodes++ } else { - stats.NotReadyNodes++ + notReadyNodes++ } break } @@ -517,31 +518,75 @@ func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterSt // Aggregate capacity if cpu, ok := node.Status.Capacity[corev1.ResourceCPU]; ok { - stats.TotalCapacity[corev1.ResourceCPU].Add(cpu) + totalCapacityCPU.Add(cpu) } if mem, ok := node.Status.Capacity[corev1.ResourceMemory]; ok { - stats.TotalCapacity[corev1.ResourceMemory].Add(mem) + totalCapacityMemory.Add(mem) } if pods, ok := node.Status.Capacity[corev1.ResourcePods]; ok { - stats.TotalCapacity[corev1.ResourcePods].Add(pods) + totalCapacityPods.Add(pods) } // Aggregate allocatable if cpu, ok := node.Status.Allocatable[corev1.ResourceCPU]; ok { - stats.TotalAllocatable[corev1.ResourceCPU].Add(cpu) + totalAllocatableCPU.Add(cpu) } if mem, ok := node.Status.Allocatable[corev1.ResourceMemory]; ok { - stats.TotalAllocatable[corev1.ResourceMemory].Add(mem) + totalAllocatableMemory.Add(mem) } if pods, ok := node.Status.Allocatable[corev1.ResourcePods]; ok { - stats.TotalAllocatable[corev1.ResourcePods].Add(pods) + totalAllocatablePods.Add(pods) } } + // Build the response with properly formatted resources + stats := ClusterStats{ + TotalNodes: len(nodeList.Items), + ReadyNodes: readyNodes, + NotReadyNodes: notReadyNodes, + TotalCapacity: &ClusterStatsResources{ + CPU: formatCPU(totalCapacityCPU.MilliValue()), + Memory: formatMemory(totalCapacityMemory.Value()), + Pods: int(totalCapacityPods.Value()), + }, + TotalAllocatable: &ClusterStatsResources{ + CPU: formatCPU(totalAllocatableCPU.MilliValue()), + Memory: formatMemory(totalAllocatableMemory.Value()), + Pods: int(totalAllocatablePods.Value()), + }, + } + return stats } +// formatCPU converts milliCPU to a readable string (e.g., "4" for 4 cores) +func formatCPU(milliCPU int64) string { + cores := float64(milliCPU) / 1000.0 + return fmt.Sprintf("%.1f", cores) +} + +// formatMemory converts bytes to a readable string (e.g., "8Gi", "16Gi") +func formatMemory(bytes int64) string { + const ( + KB = 1024 + MB = 1024 * KB + GB = 1024 * MB + TB = 1024 * GB + ) + + if bytes >= TB { + return fmt.Sprintf("%.1fTi", float64(bytes)/float64(TB)) + } else if bytes >= GB { + return fmt.Sprintf("%.1fGi", float64(bytes)/float64(GB)) + } else if bytes >= MB { + return fmt.Sprintf("%.1fMi", float64(bytes)/float64(MB)) + } else if bytes >= KB { + return fmt.Sprintf("%.1fKi", float64(bytes)/float64(KB)) + } + return fmt.Sprintf("%d", bytes) +} + // Helper function to create a new Quantity -func newQuantity(value int64) *corev1.Quantity { - return &corev1.Quantity{} +func newQuantity(value int64) resource.Quantity { + return *resource.NewQuantity(value, resource.DecimalSI) } diff --git a/api/internal/k8s/client.go b/api/internal/k8s/client.go index 6c70ac93..07c7c446 100644 --- a/api/internal/k8s/client.go +++ b/api/internal/k8s/client.go @@ -95,6 +95,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" diff --git a/ui/src/pages/admin/Groups.tsx b/ui/src/pages/admin/Groups.tsx index 79c5cae3..feb81b95 100644 --- a/ui/src/pages/admin/Groups.tsx +++ b/ui/src/pages/admin/Groups.tsx @@ -41,6 +41,7 @@ import { useGroupEvents } from '../../hooks/useEnterpriseWebSocket'; import { useNotificationQueue } from '../../components/NotificationQueue'; import EnhancedWebSocketStatus from '../../components/EnhancedWebSocketStatus'; import WebSocketErrorBoundary from '../../components/WebSocketErrorBoundary'; +import Layout from '../../components/Layout'; export default function Groups() { const navigate = useNavigate(); @@ -161,7 +162,8 @@ export default function Groups() { return ( - + + @@ -340,7 +342,8 @@ export default function Groups() { - + + ); } diff --git a/ui/src/pages/admin/Users.tsx b/ui/src/pages/admin/Users.tsx index 956565d9..2a0ebd19 100644 --- a/ui/src/pages/admin/Users.tsx +++ b/ui/src/pages/admin/Users.tsx @@ -41,6 +41,7 @@ import { useUserEvents } from '../../hooks/useEnterpriseWebSocket'; import { useNotificationQueue } from '../../components/NotificationQueue'; import EnhancedWebSocketStatus from '../../components/EnhancedWebSocketStatus'; import WebSocketErrorBoundary from '../../components/WebSocketErrorBoundary'; +import Layout from '../../components/Layout'; /** * Users - User account management for administrators @@ -238,7 +239,8 @@ export default function Users() { return ( - + + @@ -448,7 +450,8 @@ export default function Users() { - + + ); }