package k8s import ( "context" "encoding/json" "fmt" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/iliaivanov/spec-kit-remote/cmd/dev-pod-api/internal/model" ) // nodeMetricsList mirrors the metrics API response for nodes. type nodeMetricsList struct { Items []nodeMetrics `json:"items"` } type nodeMetrics struct { Metadata struct { Name string `json:"name"` } `json:"metadata"` Usage map[string]string `json:"usage"` } // GetClusterStatus returns node list with capacity, allocatable, and usage. func (c *Client) GetClusterStatus(ctx context.Context) (*model.ClusterStatus, error) { nodes, err := c.Clientset.CoreV1().Nodes().List(ctx, metav1.ListOptions{}) if err != nil { return nil, fmt.Errorf("list nodes: %w", err) } // Try to fetch node metrics (may not be available) metricsMap := c.fetchNodeMetrics(ctx) var totalCPUCap, totalCPUAlloc, totalMemCap, totalMemAlloc int64 status := &model.ClusterStatus{ Nodes: make([]model.NodeStatus, 0, len(nodes.Items)), } for _, node := range nodes.Items { ns := model.NodeStatus{ Name: node.Name, Status: nodeConditionStatus(node), CPUCapacity: node.Status.Capacity.Cpu().String(), CPUAllocatable: node.Status.Allocatable.Cpu().String(), MemCapacity: node.Status.Capacity.Memory().String(), MemAllocatable: node.Status.Allocatable.Memory().String(), } totalCPUCap += node.Status.Capacity.Cpu().MilliValue() totalCPUAlloc += node.Status.Allocatable.Cpu().MilliValue() totalMemCap += node.Status.Capacity.Memory().Value() totalMemAlloc += node.Status.Allocatable.Memory().Value() if m, ok := metricsMap[node.Name]; ok { ns.CPUUsage = m.cpuUsage ns.MemUsage = m.memUsage } status.Nodes = append(status.Nodes, ns) } status.Total = model.ResourceSummary{ CPUCapacity: resource.NewMilliQuantity(totalCPUCap, resource.DecimalSI).String(), CPUAllocatable: resource.NewMilliQuantity(totalCPUAlloc, resource.DecimalSI).String(), MemCapacity: resource.NewQuantity(totalMemCap, resource.BinarySI).String(), MemAllocatable: resource.NewQuantity(totalMemAlloc, resource.BinarySI).String(), } return status, nil } type nodeUsage struct { cpuUsage string memUsage string } func (c *Client) fetchNodeMetrics(ctx context.Context) map[string]nodeUsage { result := make(map[string]nodeUsage) restClient := c.Clientset.Discovery().RESTClient() if restClient == nil { return result } data, err := restClient.Get(). AbsPath("/apis/metrics.k8s.io/v1beta1/nodes"). DoRaw(ctx) if err != nil { return result } var metrics nodeMetricsList if err := json.Unmarshal(data, &metrics); err != nil { return result } for _, m := range metrics.Items { nu := nodeUsage{} if cpu, ok := m.Usage["cpu"]; ok { nu.cpuUsage = cpu } if mem, ok := m.Usage["memory"]; ok { nu.memUsage = mem } result[m.Metadata.Name] = nu } return result } func nodeConditionStatus(node corev1.Node) string { for _, cond := range node.Status.Conditions { if cond.Type == corev1.NodeReady { if cond.Status == corev1.ConditionTrue { return "Ready" } return "NotReady" } } return "Unknown" } // cacheServices maps friendly names to PVC names in the dev-infra namespace. var cacheServices = []struct { name string pvcName string }{ {"verdaccio", "verdaccio-storage"}, {"athens", "athens-storage"}, {"cargo-proxy", "cargo-proxy-cache"}, } // GetCacheStats returns PVC status for cache services in dev-infra. func (c *Client) GetCacheStats(ctx context.Context) ([]model.CacheStat, error) { stats := make([]model.CacheStat, 0, len(cacheServices)) for _, svc := range cacheServices { pvc, err := c.Clientset.CoreV1().PersistentVolumeClaims("dev-infra").Get(ctx, svc.pvcName, metav1.GetOptions{}) if err != nil { stats = append(stats, model.CacheStat{ Name: svc.name, PVCName: svc.pvcName, Status: "NotFound", }) continue } capacity := "" if pvc.Status.Capacity != nil { if storage, ok := pvc.Status.Capacity[corev1.ResourceStorage]; ok { capacity = storage.String() } } stats = append(stats, model.CacheStat{ Name: svc.name, PVCName: svc.pvcName, Capacity: capacity, Status: string(pvc.Status.Phase), }) } return stats, nil }