package atlas import ( "encoding/json" "fmt" "math" "sort" "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 } // Flux is the reconciliation state of a Flux Kustomization. type Flux struct { Ready bool Reason string Revision string } // FluxStatus parses a Flux Kustomization object into its reconcile state. func FluxStatus(kustJSON []byte) (Flux, error) { var k struct { Status struct { Conditions []struct { Type string `json:"type"` Status string `json:"status"` Reason string `json:"reason"` } `json:"conditions"` LastAppliedRevision string `json:"lastAppliedRevision"` } `json:"status"` } if err := json.Unmarshal(kustJSON, &k); err != nil { return Flux{}, fmt.Errorf("parse kustomization: %w", err) } f := Flux{Revision: shortRev(k.Status.LastAppliedRevision)} for _, c := range k.Status.Conditions { if c.Type == "Ready" { f.Ready = c.Status == "True" f.Reason = c.Reason } } return f, nil } // FluxNode renders the Flux reconcile state as a stage node. func FluxNode(f Flux) Node { if f.Ready { return Node{ Title: "⟳ Flux · reconciled · " + f.Revision, Pill: "var(--green)", Tags: []string{"live · k8s"}, } } return Node{ Title: "⟳ Flux · " + f.Reason, Pill: "var(--coral)", Tags: []string{"live · k8s"}, } } // shortRev turns a Flux revision "main@sha1:" into "main@". func shortRev(rev string) string { at := strings.Index(rev, "@") if at < 0 { return rev } branch, sha := rev[:at], rev[at+1:] if c := strings.LastIndex(sha, ":"); c >= 0 { sha = sha[c+1:] } if len(sha) > 7 { sha = sha[:7] } return branch + "@" + sha } // Deploy is the live state of a Kubernetes Deployment. type Deploy struct { Image string Ready int Desired int } // Tag is the image tag (substring after the last ":"). func (d Deploy) Tag() string { if i := strings.LastIndex(d.Image, ":"); i >= 0 { return d.Image[i+1:] } return d.Image } // DeployState parses a Kubernetes Deployment object into its live state. func DeployState(deployJSON []byte) (Deploy, error) { var dep struct { Spec struct { Replicas int `json:"replicas"` Template struct { Spec struct { Containers []struct { Image string `json:"image"` } `json:"containers"` } `json:"spec"` } `json:"template"` } `json:"spec"` Status struct { ReadyReplicas int `json:"readyReplicas"` } `json:"status"` } if err := json.Unmarshal(deployJSON, &dep); err != nil { return Deploy{}, fmt.Errorf("parse deployment: %w", err) } d := Deploy{Ready: dep.Status.ReadyReplicas, Desired: dep.Spec.Replicas} if len(dep.Spec.Template.Spec.Containers) > 0 { d.Image = dep.Spec.Template.Spec.Containers[0].Image } return d, nil } // DeployNode renders the live deploy state as a stage node (green when the // rollout is fully ready, amber otherwise). func DeployNode(d Deploy) Node { pill := "var(--amber)" if d.Desired > 0 && d.Ready == d.Desired { pill = "var(--green)" } img := d.Image if i := strings.LastIndex(img, "/"); i >= 0 { img = img[i+1:] // drop registry host } return Node{ Title: fmt.Sprintf("◆ deployed · %s · %d/%d ready", img, d.Ready, d.Desired), Pill: pill, Tags: []string{"live · k8s"}, } } // NamespaceSummary parses a Kubernetes `/api/v1/namespaces` list into a compact // "ns: a · b · c" line for the substrate, dropping system namespaces, sorting, // and capping the count (with "+N more" when it overflows). func NamespaceSummary(nsJSON []byte) (string, error) { var list struct { Items []struct { Metadata struct { Name string `json:"name"` } `json:"metadata"` } `json:"items"` } if err := json.Unmarshal(nsJSON, &list); err != nil { return "", fmt.Errorf("parse namespaces: %w", err) } var names []string for _, it := range list.Items { n := it.Metadata.Name if strings.HasPrefix(n, "kube-") || n == "default" || n == "flux-system" { continue } names = append(names, n) } sort.Strings(names) const maxShown = 12 more := 0 if len(names) > maxShown { more = len(names) - maxShown names = names[:maxShown] } if len(names) == 0 { return "ns: (none)", nil } s := "ns: " + strings.Join(names, " · ") if more > 0 { s += " · +" + strconv.Itoa(more) + " more" } return s, 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 }