From 9b6936a30a379d991947328ff8baf0d11e4b608d Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 22:29:14 +0000 Subject: [PATCH 1/5] fix(api): add missing types import in k8s client Add missing 'k8s.io/apimachinery/pkg/types' import required for types.StrategicMergePatchType in PatchNode function. Fixes compilation error: undefined: types --- api/internal/k8s/client.go | 1 + 1 file changed, 1 insertion(+) 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" From 3961d5bae431979fac074c01e3cb083cd63c51da Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 22:34:36 +0000 Subject: [PATCH 2/5] fix(api): resolve type errors in nodes and stubs handlers Fixed multiple compilation errors: nodes.go: - Added missing 'resource' import from k8s.io/apimachinery/pkg/api/resource - Removed unused 'metav1' and 'types' imports - Fixed newQuantity() to return resource.Quantity instead of corev1.Quantity - Fixed resource.Quantity.Add() calls by getting value, modifying, then reassigning to map stubs.go: - Fixed iteration over NodeList to use nodes.Items instead of nodes directly - Fixed len() calls on PodList to use pods.Items instead of pods - Fixed len() calls on NodeList to use nodes.Items instead of nodes Resolves compilation errors with undefined types and invalid pointer method calls. --- api/internal/api/stubs.go | 8 +++---- api/internal/handlers/nodes.go | 43 +++++++++++++++++++++------------- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/api/internal/api/stubs.go b/api/internal/api/stubs.go index d40a0b33..455751c7 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, diff --git a/api/internal/handlers/nodes.go b/api/internal/handlers/nodes.go index c23aa087..c7cc4757 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 @@ -491,14 +490,14 @@ func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterSt ReadyNodes: 0, NotReadyNodes: 0, TotalCapacity: corev1.ResourceList{ - corev1.ResourceCPU: *newQuantity(0), - corev1.ResourceMemory: *newQuantity(0), - corev1.ResourcePods: *newQuantity(0), + 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), + corev1.ResourceCPU: newQuantity(0), + corev1.ResourceMemory: newQuantity(0), + corev1.ResourcePods: newQuantity(0), }, } @@ -517,24 +516,36 @@ 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) + totalCPU := stats.TotalCapacity[corev1.ResourceCPU] + totalCPU.Add(cpu) + stats.TotalCapacity[corev1.ResourceCPU] = totalCPU } if mem, ok := node.Status.Capacity[corev1.ResourceMemory]; ok { - stats.TotalCapacity[corev1.ResourceMemory].Add(mem) + totalMem := stats.TotalCapacity[corev1.ResourceMemory] + totalMem.Add(mem) + stats.TotalCapacity[corev1.ResourceMemory] = totalMem } if pods, ok := node.Status.Capacity[corev1.ResourcePods]; ok { - stats.TotalCapacity[corev1.ResourcePods].Add(pods) + totalPods := stats.TotalCapacity[corev1.ResourcePods] + totalPods.Add(pods) + stats.TotalCapacity[corev1.ResourcePods] = totalPods } // Aggregate allocatable if cpu, ok := node.Status.Allocatable[corev1.ResourceCPU]; ok { - stats.TotalAllocatable[corev1.ResourceCPU].Add(cpu) + allocCPU := stats.TotalAllocatable[corev1.ResourceCPU] + allocCPU.Add(cpu) + stats.TotalAllocatable[corev1.ResourceCPU] = allocCPU } if mem, ok := node.Status.Allocatable[corev1.ResourceMemory]; ok { - stats.TotalAllocatable[corev1.ResourceMemory].Add(mem) + allocMem := stats.TotalAllocatable[corev1.ResourceMemory] + allocMem.Add(mem) + stats.TotalAllocatable[corev1.ResourceMemory] = allocMem } if pods, ok := node.Status.Allocatable[corev1.ResourcePods]; ok { - stats.TotalAllocatable[corev1.ResourcePods].Add(pods) + allocPods := stats.TotalAllocatable[corev1.ResourcePods] + allocPods.Add(pods) + stats.TotalAllocatable[corev1.ResourcePods] = allocPods } } @@ -542,6 +553,6 @@ func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterSt } // 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) } From 5df79b0c1a455edebf9bea91b4db4d7afd5dd07e Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 22:40:40 +0000 Subject: [PATCH 3/5] fix(ui): add missing Layout wrapper to Users and Groups admin pages Both /admin/users and /admin/groups pages were missing the Layout component wrapper that provides the admin sidebar navigation. This caused the sidebar to disappear when navigating to these pages. Changes: - Added Layout import to Users.tsx and Groups.tsx - Wrapped Container content in Layout component - Maintains existing WebSocketErrorBoundary wrapper structure Fixes sidebar navigation visibility on admin Users and Groups pages. --- ui/src/pages/admin/Groups.tsx | 7 +++++-- ui/src/pages/admin/Users.tsx | 7 +++++-- 2 files changed, 10 insertions(+), 4 deletions(-) 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() { - + + ); } From ae0b5a8defcaa65b16b55d7d03dedb6174d570d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 22:42:50 +0000 Subject: [PATCH 4/5] fix(api): add compliance stub endpoints to prevent 404 errors Added stub implementations for compliance endpoints that return empty data when the streamspace-compliance plugin is not installed. This prevents 404 errors on the Compliance admin page while clearly indicating that the full functionality requires the plugin. Changes: - Added compliance stub handlers in api/internal/api/stubs.go: - ListComplianceFrameworks (returns empty frameworks array) - CreateComplianceFramework (returns 501 with plugin installation message) - ListCompliancePolicies (returns empty policies array) - CreateCompliancePolicy (returns 501 with plugin installation message) - ListViolations (returns empty violations array) - RecordViolation (returns 501 with plugin installation message) - ResolveViolation (returns 501 with plugin installation message) - GetComplianceDashboard (returns zero metrics) - Uncommented compliance routes in api/cmd/main.go: - Added /compliance/dashboard endpoint - Enabled all compliance CRUD endpoints under /compliance/* routes - Added documentation noting these are stubs until plugin is installed Benefits: - No more 404 errors in browser console on Compliance page - Clear indication when compliance features require plugin installation - Graceful degradation when plugin is not available - Plugin can override stubs when installed Resolves compliance page 404 errors for frameworks, policies, violations, and dashboard. --- api/cmd/main.go | 44 ++++++++++---------- api/internal/api/stubs.go | 84 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+), 22 deletions(-) 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 455751c7..33464b70 100644 --- a/api/internal/api/stubs.go +++ b/api/internal/api/stubs.go @@ -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, + }, + }) +} From fe25b1ff178a43c1a86047a08704a1314596a926 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 17 Nov 2025 22:46:32 +0000 Subject: [PATCH 5/5] fix(api): fix ClusterStats JSON serialization for Nodes page Fixed TypeError on Nodes admin page where totalCapacity was undefined. The issue was that corev1.ResourceList was being directly serialized to JSON which didn't match the expected frontend interface. Changes: - Created ClusterStatsResources struct with proper JSON-friendly format: - cpu: string (e.g., "4.0" for 4 cores) - memory: string (e.g., "8.0Gi") - pods: int (e.g., 110) - Updated ClusterStats struct to use ClusterStatsResources pointers instead of corev1.ResourceList for totalCapacity and totalAllocatable - Rewrote calculateClusterStats to: - Aggregate resources using temporary Quantity variables - Format resources into human-readable strings - Return properly structured ClusterStatsResources - Added helper functions: - formatCPU: converts milliCPU to cores string (e.g., "4.0") - formatMemory: converts bytes to human-readable format (Ki/Mi/Gi/Ti) Frontend now receives proper JSON structure: { "totalNodes": 3, "readyNodes": 3, "notReadyNodes": 0, "totalCapacity": { "cpu": "12.0", "memory": "24.0Gi", "pods": 330 }, "totalAllocatable": {...} } Resolves Nodes page crash: 'can't access property "cpu", j.totalCapacity is undefined' --- api/internal/handlers/nodes.go | 116 +++++++++++++++++++++------------ 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/api/internal/handlers/nodes.go b/api/internal/handlers/nodes.go index c7cc4757..bdeab240 100644 --- a/api/internal/handlers/nodes.go +++ b/api/internal/handlers/nodes.go @@ -122,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 @@ -485,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 } @@ -516,42 +518,74 @@ func (h *NodeHandler) calculateClusterStats(nodeList *corev1.NodeList) ClusterSt // Aggregate capacity if cpu, ok := node.Status.Capacity[corev1.ResourceCPU]; ok { - totalCPU := stats.TotalCapacity[corev1.ResourceCPU] - totalCPU.Add(cpu) - stats.TotalCapacity[corev1.ResourceCPU] = totalCPU + totalCapacityCPU.Add(cpu) } if mem, ok := node.Status.Capacity[corev1.ResourceMemory]; ok { - totalMem := stats.TotalCapacity[corev1.ResourceMemory] - totalMem.Add(mem) - stats.TotalCapacity[corev1.ResourceMemory] = totalMem + totalCapacityMemory.Add(mem) } if pods, ok := node.Status.Capacity[corev1.ResourcePods]; ok { - totalPods := stats.TotalCapacity[corev1.ResourcePods] - totalPods.Add(pods) - stats.TotalCapacity[corev1.ResourcePods] = totalPods + totalCapacityPods.Add(pods) } // Aggregate allocatable if cpu, ok := node.Status.Allocatable[corev1.ResourceCPU]; ok { - allocCPU := stats.TotalAllocatable[corev1.ResourceCPU] - allocCPU.Add(cpu) - stats.TotalAllocatable[corev1.ResourceCPU] = allocCPU + totalAllocatableCPU.Add(cpu) } if mem, ok := node.Status.Allocatable[corev1.ResourceMemory]; ok { - allocMem := stats.TotalAllocatable[corev1.ResourceMemory] - allocMem.Add(mem) - stats.TotalAllocatable[corev1.ResourceMemory] = allocMem + totalAllocatableMemory.Add(mem) } if pods, ok := node.Status.Allocatable[corev1.ResourcePods]; ok { - allocPods := stats.TotalAllocatable[corev1.ResourcePods] - allocPods.Add(pods) - stats.TotalAllocatable[corev1.ResourcePods] = allocPods + 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) resource.Quantity { return *resource.NewQuantity(value, resource.DecimalSI)