generated from mathias/template-go-web
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>
80 lines
2.1 KiB
Go
80 lines
2.1 KiB
Go
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
|
||
}
|