package atlas import ( "encoding/json" "fmt" "math" "strconv" "strings" ) // HostsFromNodes parses a Kubernetes `/api/v1/nodes` list response into // substrate Host entries, so the atlas machines reflect the live cluster. func HostsFromNodes(nodesJSON []byte) ([]Host, error) { var list struct { Items []struct { Metadata struct { Name string `json:"name"` } `json:"metadata"` Status struct { Capacity map[string]string `json:"capacity"` NodeInfo struct { Architecture string `json:"architecture"` KubeletVersion string `json:"kubeletVersion"` } `json:"nodeInfo"` } `json:"status"` } `json:"items"` } if err := json.Unmarshal(nodesJSON, &list); err != nil { return nil, fmt.Errorf("parse nodes: %w", err) } hosts := make([]Host, 0, len(list.Items)) for _, it := range list.Items { var parts []string if a := it.Status.NodeInfo.Architecture; a != "" { parts = append(parts, a) } if c := it.Status.Capacity["cpu"]; c != "" { parts = append(parts, c+" cpu") } if gi := memGi(it.Status.Capacity["memory"]); gi != "" { parts = append(parts, gi+"Gi") } if g := it.Status.Capacity["nvidia.com/gpu"]; g != "" && g != "0" { parts = append(parts, g+"× GPU") } if v := k3sVersion(it.Status.NodeInfo.KubeletVersion); v != "" { parts = append(parts, v) } hosts = append(hosts, Host{Name: it.Metadata.Name, Spec: strings.Join(parts, " · ")}) } return hosts, nil } // MergeSubstrate overlays live node specs onto the authored substrate: an // authored host is replaced by the live node of the same name (fresh specs), // authored-only machines (non-cluster: iguana/flamingo/piguard) are kept, and // live-only nodes (new to the cluster) are appended. func MergeSubstrate(authored, live []Host) []Host { liveByName := make(map[string]Host, len(live)) for _, h := range live { liveByName[h.Name] = h } seen := make(map[string]bool, len(authored)) out := make([]Host, 0, len(authored)+len(live)) for _, a := range authored { if l, ok := liveByName[a.Name]; ok { out = append(out, l) } else { out = append(out, a) } seen[a.Name] = true } for _, l := range live { if !seen[l.Name] { out = append(out, l) } } return out } // memGi converts a Kubernetes memory quantity in Ki (e.g. "67108864Ki") to a // rounded Gi string. Returns "" if unparseable. func memGi(ki string) string { n, err := strconv.ParseFloat(strings.TrimSuffix(ki, "Ki"), 64) if err != nil { return "" } return strconv.Itoa(int(math.Round(n / 1048576))) } // k3sVersion trims a kubeletVersion's build metadata, labelling k3s builds. // "v1.31.4+k3s1" → "k3s v1.31.4"; "v1.31.4" → "v1.31.4". func k3sVersion(kubelet string) string { if kubelet == "" { return "" } if i := strings.Index(kubelet, "+"); i >= 0 { ver, suffix := kubelet[:i], kubelet[i+1:] if strings.Contains(suffix, "k3s") { return "k3s " + ver } return ver } return kubelet }