// Package config loads bootapi server configuration from the environment, // following the same env-first convention as encapi. package config import ( "fmt" "os" "strings" "time" ) // Config is the fully-resolved server configuration. type Config struct { // ListenAddr is the HTTP bind address, e.g. ":8000". ListenAddr string // NetBoxURL is the base URL of the NetBox API, // e.g. "https://netbox.k8s.syd1.au.unkin.net". NetBoxURL string // NetBoxToken is the NetBox API token. Prefer NetBoxTokenFile in k8s. NetBoxToken string // NetBoxTimeout bounds each NetBox HTTP request. NetBoxTimeout time.Duration // NetBoxInsecure disables TLS verification against NetBox (dev only). NetBoxInsecure bool // CacheTTL is how long a resolved host is cached in memory. Short by // design: NetBox is the source of truth and a machine's provisioning data // can change between boots. CacheTTL time.Duration // TemplateDir, when set, is a directory of override templates layered on // top of the embedded defaults (a Kubernetes ConfigMap mount in prod). TemplateDir string // DefaultTemplate is the kickstart template used when NetBox provides no // platform/role/override selection key. DefaultTemplate string // BaseURL is bootapi's own externally-reachable base URL, baked into the // iPXE script's inst.ks= and repo URLs so a booting host calls back here. // e.g. "http://bootapi.k8s.syd1.au.unkin.net". BaseURL string // BootBaseURL is the base URL of the OS install trees (kernel/initrd + // inst.repo), e.g. "http://mirror.k8s.syd1.au.unkin.net/almalinux". BootBaseURL string // PuppetServer / PuppetCAServer are baked into kickstart %post so the // freshly-installed host checks in to the right place. PuppetServer string PuppetCAServer string // Domain is the default DNS domain applied when NetBox does not record one // for a device. Domain string // Nameservers is the default resolver list applied when NetBox records // none for a device. Nameservers []string // RootPasswordHash is a crypt(3) hash injected into kickstarts at render // time (sourced from Vault in k8s). Empty locks the root account. RootPasswordHash string // SSHAuthorizedKeys are public keys installed for root at render time. SSHAuthorizedKeys []string // UnknownMACFallback selects what the iPXE endpoint returns for a MAC that // NetBox does not know: "local" (chain to local disk, the safe default) or // "shell" (drop to an iPXE shell for debugging). See docs/endpoints.md. UnknownMACFallback string } // Load reads configuration from the environment, applying defaults, and reads a // token file when BOOTAPI_NETBOX_TOKEN_FILE is set (Vault-mounted secret). func Load() (*Config, error) { cacheTTL, err := time.ParseDuration(getenv("BOOTAPI_CACHE_TTL", "30s")) if err != nil { return nil, fmt.Errorf("invalid BOOTAPI_CACHE_TTL: %w", err) } nbTimeout, err := time.ParseDuration(getenv("BOOTAPI_NETBOX_TIMEOUT", "5s")) if err != nil { return nil, fmt.Errorf("invalid BOOTAPI_NETBOX_TIMEOUT: %w", err) } token := os.Getenv("BOOTAPI_NETBOX_TOKEN") if tf := os.Getenv("BOOTAPI_NETBOX_TOKEN_FILE"); tf != "" { b, err := os.ReadFile(tf) if err != nil { return nil, fmt.Errorf("read BOOTAPI_NETBOX_TOKEN_FILE %q: %w", tf, err) } token = strings.TrimSpace(string(b)) } fallback := getenv("BOOTAPI_UNKNOWN_MAC_FALLBACK", "local") if fallback != "local" && fallback != "shell" { return nil, fmt.Errorf("invalid BOOTAPI_UNKNOWN_MAC_FALLBACK %q: want \"local\" or \"shell\"", fallback) } rootHash := os.Getenv("BOOTAPI_ROOT_PASSWORD_HASH") if rf := os.Getenv("BOOTAPI_ROOT_PASSWORD_HASH_FILE"); rf != "" { b, err := os.ReadFile(rf) if err != nil { return nil, fmt.Errorf("read BOOTAPI_ROOT_PASSWORD_HASH_FILE %q: %w", rf, err) } rootHash = strings.TrimSpace(string(b)) } return &Config{ ListenAddr: getenv("BOOTAPI_LISTEN_ADDR", ":8000"), NetBoxURL: strings.TrimRight(os.Getenv("BOOTAPI_NETBOX_URL"), "/"), NetBoxToken: token, NetBoxTimeout: nbTimeout, NetBoxInsecure: getenv("BOOTAPI_NETBOX_INSECURE", "false") == "true", CacheTTL: cacheTTL, TemplateDir: os.Getenv("BOOTAPI_TEMPLATE_DIR"), DefaultTemplate: getenv("BOOTAPI_DEFAULT_TEMPLATE", "almalinux9"), BaseURL: strings.TrimRight(os.Getenv("BOOTAPI_BASE_URL"), "/"), BootBaseURL: strings.TrimRight(os.Getenv("BOOTAPI_BOOT_BASE_URL"), "/"), PuppetServer: getenv("BOOTAPI_PUPPET_SERVER", "puppet.query.consul"), PuppetCAServer: getenv("BOOTAPI_PUPPET_CA_SERVER", "puppetca.query.consul"), Domain: getenv("BOOTAPI_DOMAIN", "main.unkin.net"), Nameservers: splitList(os.Getenv("BOOTAPI_NAMESERVERS")), RootPasswordHash: rootHash, SSHAuthorizedKeys: splitLines(os.Getenv("BOOTAPI_SSH_AUTHORIZED_KEYS")), UnknownMACFallback: fallback, }, nil } func getenv(key, def string) string { if v := os.Getenv(key); v != "" { return v } return def } // splitList splits a comma-separated env value into a trimmed, non-empty slice. func splitList(v string) []string { if v == "" { return nil } var out []string for _, p := range strings.Split(v, ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out } // splitLines splits a newline-separated env value (e.g. multiple SSH keys) into // a trimmed, non-empty slice. func splitLines(v string) []string { if v == "" { return nil } var out []string for _, p := range strings.Split(v, "\n") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out }