Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions api/cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
{
Expand Down
92 changes: 88 additions & 4 deletions api/internal/api/stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
})
}
111 changes: 78 additions & 33 deletions api/internal/handlers/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -486,62 +492,101 @@ 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
}
}

// 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)
}
1 change: 1 addition & 0 deletions api/internal/k8s/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions ui/src/pages/admin/Groups.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -161,7 +162,8 @@ export default function Groups() {

return (
<WebSocketErrorBoundary>
<Container maxWidth="xl" sx={{ py: 4 }}>
<Layout>
<Container maxWidth="xl" sx={{ py: 4 }}>
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<GroupsIcon sx={{ fontSize: 40 }} />
Expand Down Expand Up @@ -340,7 +342,8 @@ export default function Groups() {
</Button>
</DialogActions>
</Dialog>
</Container>
</Container>
</Layout>
</WebSocketErrorBoundary>
);
}
7 changes: 5 additions & 2 deletions ui/src/pages/admin/Users.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -238,7 +239,8 @@ export default function Users() {

return (
<WebSocketErrorBoundary>
<Container maxWidth="xl" sx={{ py: 4 }}>
<Layout>
<Container maxWidth="xl" sx={{ py: 4 }}>
<Box sx={{ mb: 4, display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<PersonIcon sx={{ fontSize: 40 }} />
Expand Down Expand Up @@ -448,7 +450,8 @@ export default function Users() {
</Button>
</DialogActions>
</Dialog>
</Container>
</Container>
</Layout>
</WebSocketErrorBoundary>
);
}
Loading