generated from mathias/template-go-web
feat(atlas): Phase B — substrate from the live cluster
The substrate machines are now rendered from the live k3s nodes (arch, cpu, memory, GPU, k3s version), read in-cluster via the mounted ServiceAccount with a stdlib HTTP client (no client-go) and a 30s cache; falls back to the authored substrate whenever the cluster is unreachable. Placement per the homelab decision "in-cluster only for in-workload ops" — a running web app rendering live data fits. HostsFromNodes parser built test-first. RBAC (read-only node-reader SA) shipped in infra k3s/apps/cad-atlas/. Verified: go build/vet/lint(0)/test green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user