remove hosts/ directory — single source of truth is inventory.yaml
- Delete cmd/oikos/build_hosts.go (generator no longer needed) - Remove build-hosts subcommand from main.go - Fix oikos homelab whoami: read from inventory.yaml instead of hosts/ - Update bootstrap.sh: identity check uses inventory.yaml - Remove all 27 generated hosts/*.yaml files - Update AGENTS.md and OIKOS.md to reference inventory.yaml only
This commit is contained in:
@@ -1,187 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func runBuildHosts() error {
|
||||
repoDir := "."
|
||||
if d := os.Getenv("HOMELAB_CONTEXT_DIR"); d != "" {
|
||||
repoDir = d
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(filepath.Join(repoDir, "inventory.yaml"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read inventory: %w", err)
|
||||
}
|
||||
|
||||
var inventory map[string]any
|
||||
if err := yaml.Unmarshal(data, &inventory); err != nil {
|
||||
return fmt.Errorf("parse inventory: %w", err)
|
||||
}
|
||||
|
||||
hosts, _ := inventory["hosts"].(map[string]any)
|
||||
services, _ := inventory["services"].(map[string]any)
|
||||
mesh, _ := inventory["mesh"].(map[string]any)
|
||||
|
||||
hostsDir := filepath.Join(repoDir, "hosts")
|
||||
os.MkdirAll(hostsDir, 0755)
|
||||
|
||||
desired := make(map[string]string)
|
||||
for name, entry := range hosts {
|
||||
entryMap, _ := entry.(map[string]any)
|
||||
record := buildHostRecord(name, entryMap, services, mesh)
|
||||
content := fmt.Sprintf("# Generated by oikos build-hosts from inventory.yaml.\n# Do NOT edit by hand.\n\n%s", asYAML(record))
|
||||
desired[name+".yaml"] = content
|
||||
}
|
||||
|
||||
for name, content := range desired {
|
||||
path := filepath.Join(hostsDir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", path, err)
|
||||
}
|
||||
fmt.Printf("wrote %s\n", name)
|
||||
}
|
||||
|
||||
// Clean orphans
|
||||
entries, _ := os.ReadDir(hostsDir)
|
||||
for _, e := range entries {
|
||||
if strings.HasSuffix(e.Name(), ".yaml") && desired[e.Name()] == "" {
|
||||
os.Remove(filepath.Join(hostsDir, e.Name()))
|
||||
fmt.Printf("deleted orphan %s\n", e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildHostRecord(name string, entry map[string]any, services map[string]any, mesh map[string]any) map[string]any {
|
||||
kind, _ := entry["kind"].(string)
|
||||
|
||||
var runsServices []string
|
||||
for svc, v := range services {
|
||||
if svcMap, ok := v.(map[string]any); ok {
|
||||
if backend, ok := svcMap["backend"].(string); ok && backend == name {
|
||||
runsServices = append(runsServices, svc)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(runsServices)
|
||||
|
||||
var hosted []map[string]any
|
||||
for _, svc := range runsServices {
|
||||
if svcMap, ok := services[svc].(map[string]any); ok {
|
||||
entry := make(map[string]any)
|
||||
for k, v := range svcMap {
|
||||
entry[k] = v
|
||||
}
|
||||
entry["name"] = svc
|
||||
hosted = append(hosted, entry)
|
||||
}
|
||||
}
|
||||
|
||||
record := map[string]any{
|
||||
"name": name,
|
||||
"kind": kind,
|
||||
"os": entry["os"],
|
||||
"role": entry["role"],
|
||||
"state": stringOr(entry["state"], "active"),
|
||||
"host": entry["host"],
|
||||
"pve_id": entry["pve_id"],
|
||||
"storage": entry["storage"],
|
||||
"depends_on": entry["depends_on"],
|
||||
"lan_ip": entry["lan_ip"],
|
||||
"mesh": entry["mesh"],
|
||||
"peers": entry["peers"],
|
||||
"mounts": entry["mounts"],
|
||||
"public_host": entry["public_host"],
|
||||
"ssh": entry["ssh"],
|
||||
"runs": append(stringSlice(entry["runs"]), runsServices...),
|
||||
"services_hosted": hosted,
|
||||
"notes": entry["notes"],
|
||||
"age_pubkey": entry["age_pubkey"],
|
||||
}
|
||||
|
||||
if mesh != nil {
|
||||
record["mesh_globals"] = map[string]any{
|
||||
"primary": mesh["primary"],
|
||||
"accepted": mesh["accepted"],
|
||||
}
|
||||
}
|
||||
|
||||
if mcp, ok := services["homelab_mcp"].(map[string]any); ok {
|
||||
record["mcp_endpoint"] = mcp["endpoint"]
|
||||
}
|
||||
if si, ok := services["secrets_issuance"].(map[string]any); ok {
|
||||
record["secrets_issuance_endpoint"] = si["endpoint"]
|
||||
}
|
||||
|
||||
cleaned := make(map[string]any)
|
||||
for k, v := range record {
|
||||
switch val := v.(type) {
|
||||
case nil:
|
||||
continue
|
||||
case string:
|
||||
if val == "" {
|
||||
continue
|
||||
}
|
||||
case []string:
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
case []map[string]any:
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
case map[string]any:
|
||||
if len(val) == 0 {
|
||||
continue
|
||||
}
|
||||
case int:
|
||||
if val == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
cleaned[k] = v
|
||||
}
|
||||
|
||||
return cleaned
|
||||
}
|
||||
|
||||
func stringOr(v any, def string) string {
|
||||
if s, ok := v.(string); ok && s != "" {
|
||||
return s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
func stringSlice(v any) []string {
|
||||
switch val := v.(type) {
|
||||
case []any:
|
||||
var out []string
|
||||
for _, item := range val {
|
||||
if s, ok := item.(string); ok {
|
||||
out = append(out, s)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
return val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func asYAML(v any) string {
|
||||
var buf bytes.Buffer
|
||||
enc := yaml.NewEncoder(&buf)
|
||||
enc.SetIndent(2)
|
||||
enc.Encode(v)
|
||||
return buf.String()
|
||||
}
|
||||
@@ -47,15 +47,24 @@ func runHomelabSubcommand() {
|
||||
|
||||
case "whoami":
|
||||
hostname, _ := os.Hostname()
|
||||
hostFile := filepath.Join(ctxDir, "hosts", hostname+".yaml")
|
||||
data, err := os.ReadFile(hostFile)
|
||||
if err != nil {
|
||||
fmt.Printf("host: %s (not enrolled — no hosts/%s.yaml)\n", hostname, hostname)
|
||||
// Try short hostname and full hostname
|
||||
hostnameShort := strings.Split(hostname, ".")[0]
|
||||
inv := loadInventory(ctxDir)
|
||||
h, ok := inv.Hosts[hostnameShort]
|
||||
if !ok {
|
||||
// Try with full hostname
|
||||
h, ok = inv.Hosts[hostname]
|
||||
}
|
||||
if !ok {
|
||||
fmt.Printf("host: %s (not enrolled — no entry in inventory.yaml)\n", hostname)
|
||||
return
|
||||
}
|
||||
var h cliHost
|
||||
yaml.Unmarshal(data, &h)
|
||||
fmt.Printf("host: %s kind: %s role: %s lan_ip: %s\n", h.Name, h.Kind, h.Role, h.LanIP)
|
||||
state := h.State
|
||||
if state == "" {
|
||||
state = "active"
|
||||
}
|
||||
fmt.Printf("host: %s kind: %s role: %s lan_ip: %s state: %s\n",
|
||||
hostnameShort, h.Kind, h.Role, h.LanIP, state)
|
||||
for _, svc := range h.Services {
|
||||
fmt.Printf(" service: %v\n", svc["name"])
|
||||
}
|
||||
|
||||
@@ -60,11 +60,6 @@ func main() {
|
||||
slog.Error("export failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "build-hosts":
|
||||
if err := runBuildHosts(); err != nil {
|
||||
slog.Error("build-hosts failed", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
case "homelab":
|
||||
runHomelabSubcommand()
|
||||
case "api":
|
||||
@@ -119,7 +114,6 @@ Roles:
|
||||
migrate Run database migrations (forward-only, idempotent)
|
||||
seed Ingest seed YAML files into the database
|
||||
export Export DB state back to seed YAMLs (DR / version control)
|
||||
build-hosts Generate hosts/*.yaml from inventory.yaml
|
||||
homelab Operator CLI: list, whoami, ssh, secret
|
||||
api Run the REST + MCP API server (Phase 2)
|
||||
scheduler Run the observe + act loop (Phase 3)
|
||||
|
||||
Reference in New Issue
Block a user