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:
2026-07-07 20:48:51 +02:00
parent f04e0dc0d4
commit 5009a335bb
33 changed files with 23 additions and 1003 deletions

View File

@@ -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()
}

View File

@@ -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"])
}

View File

@@ -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)