a414918350
The runtime image is distroless and has no git binary, so every sync failed and bootapi silently served the stale embedded templates. - fetch the branch tarball (<repo>/archive/<branch>.tar.gz) and extract it into an in-memory FS; no checkout, no writable volume - digest the extracted tree, not the archive bytes, so a recompressed identical archive is not a change - skip entries that would escape the tree - log the source commit from Gitea's immutable Link header
232 lines
8.4 KiB
Go
232 lines
8.4 KiB
Go
// 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 plain-HTTP bind address, e.g. ":8000". The boot path
|
|
// (iPXE + kickstart) is always served here so installers with no internal
|
|
// CA trust can reach it.
|
|
ListenAddr string
|
|
|
|
// TLSListenAddr, when set with TLSCertFile/TLSKeyFile, additionally serves
|
|
// HTTPS. Boot endpoints work on both; the plain-HTTP listener is mandatory,
|
|
// HTTPS is opt-in (see docs/endpoints.md).
|
|
TLSListenAddr string
|
|
TLSCertFile string
|
|
TLSKeyFile 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. Needs
|
|
// WRITE scope on the device pxe_enabled custom field for the callback.
|
|
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.
|
|
CacheTTL time.Duration
|
|
|
|
// TemplateDir, when set, is a directory of override templates layered on
|
|
// top of the embedded defaults (a ConfigMap mount). Ignored when a template
|
|
// git repo is configured.
|
|
TemplateDir string
|
|
// DefaultTemplate is the kickstart template used when no catalog/platform
|
|
// selection key matches.
|
|
DefaultTemplate string
|
|
|
|
// --- template git-sync (preferred over TemplateDir) ---
|
|
// TemplateGitURL, when set, makes bootapi fetch a templates repo's branch
|
|
// tarball over HTTP (<repo>/archive/<branch>.tar.gz) and re-fetch
|
|
// it every TemplateGitInterval, atomically swapping the loaded set on change
|
|
// and keeping the last-good set on a parse failure.
|
|
TemplateGitURL string
|
|
TemplateGitBranch string
|
|
TemplateGitInterval time.Duration
|
|
// TemplateGitToken is an optional token for a private templates repo,
|
|
// sent as a Gitea token header. Empty for a public repo.
|
|
TemplateGitToken string
|
|
|
|
// BaseURL is the http:// base PXE clients use to reach bootapi. It is baked
|
|
// into the iPXE inst.ks= and /ks URLs, so it MUST be reachable without CA
|
|
// trust (plain HTTP). e.g. "http://bootapi.k8s.syd1.au.unkin.net".
|
|
BaseURL string
|
|
// CallbackBaseURL is the base the end-of-kickstart callback uses. Defaults
|
|
// to BaseURL (plain HTTP, works before the internal CA is installed). Set to
|
|
// an https:// URL only if the kickstart installs the internal CA before the
|
|
// callback runs.
|
|
CallbackBaseURL string
|
|
|
|
// ArtifactBaseURL is the artifactapi remote base the distro catalog builds
|
|
// kernel/initrd URLs from,
|
|
// e.g. "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote".
|
|
ArtifactBaseURL string
|
|
// BootBaseURL is a legacy fallback OS-tree base used only when no catalog
|
|
// entry matches a host. Normally empty (the catalog drives boot images).
|
|
BootBaseURL string
|
|
|
|
// ProvisionToken guards POST /provisioned. Empty disables the callback
|
|
// endpoint (fail closed). Prefer ProvisionTokenFile in k8s.
|
|
ProvisionToken string
|
|
|
|
// PuppetServer / PuppetCAServer are baked into kickstart %post so the
|
|
// freshly-installed host checks in to the k8s puppetserver.
|
|
PuppetServer string
|
|
PuppetCAServer string
|
|
// PuppetCAURL is written to the puppet-initial EnvironmentFile as
|
|
// PUPPETCA_URL (consumed by that RPM's systemd bootstrap unit).
|
|
PuppetCAURL string
|
|
|
|
// Domain is the default DNS domain applied when NetBox records none.
|
|
Domain string
|
|
// Nameservers is the default resolver list applied when NetBox records none.
|
|
Nameservers []string
|
|
|
|
// RootPasswordHash is a crypt(3) hash injected into kickstarts at render
|
|
// time (Vault in k8s). Empty locks the root account.
|
|
RootPasswordHash string
|
|
// SSHAuthorizedKeys are public keys installed for root at render time.
|
|
SSHAuthorizedKeys []string
|
|
|
|
// UnknownMACFallback selects the iPXE script for an unknown MAC: "local"
|
|
// (boot local disk, safe default) or "shell" (iPXE shell for debugging).
|
|
UnknownMACFallback string
|
|
}
|
|
|
|
// Load reads configuration from the environment, applying defaults. *_FILE
|
|
// variants (Vault-mounted secrets) win over their inline counterparts.
|
|
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)
|
|
}
|
|
gitInterval, err := time.ParseDuration(getenv("BOOTAPI_TEMPLATE_GIT_INTERVAL", "3m"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid BOOTAPI_TEMPLATE_GIT_INTERVAL: %w", err)
|
|
}
|
|
|
|
token, err := readSecret("BOOTAPI_NETBOX_TOKEN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rootHash, err := readSecret("BOOTAPI_ROOT_PASSWORD_HASH")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
provToken, err := readSecret("BOOTAPI_PROVISION_TOKEN")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
baseURL := strings.TrimRight(os.Getenv("BOOTAPI_BASE_URL"), "/")
|
|
callbackBase := strings.TrimRight(os.Getenv("BOOTAPI_CALLBACK_BASE_URL"), "/")
|
|
if callbackBase == "" {
|
|
callbackBase = baseURL
|
|
}
|
|
|
|
ns := splitList(os.Getenv("BOOTAPI_NAMESERVERS"))
|
|
if len(ns) == 0 {
|
|
ns = []string{"198.18.200.7"} // k8s bind-resolvers LB
|
|
}
|
|
|
|
return &Config{
|
|
ListenAddr: getenv("BOOTAPI_LISTEN_ADDR", ":8000"),
|
|
TLSListenAddr: getenv("BOOTAPI_TLS_LISTEN_ADDR", ""),
|
|
TLSCertFile: os.Getenv("BOOTAPI_TLS_CERT_FILE"),
|
|
TLSKeyFile: os.Getenv("BOOTAPI_TLS_KEY_FILE"),
|
|
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"),
|
|
TemplateGitURL: strings.TrimRight(os.Getenv("BOOTAPI_TEMPLATE_GIT_URL"), "/"),
|
|
TemplateGitBranch: getenv("BOOTAPI_TEMPLATE_GIT_BRANCH", "main"),
|
|
TemplateGitInterval: gitInterval,
|
|
TemplateGitToken: os.Getenv("BOOTAPI_TEMPLATE_GIT_TOKEN"),
|
|
BaseURL: baseURL,
|
|
CallbackBaseURL: callbackBase,
|
|
ArtifactBaseURL: strings.TrimRight(getenv("BOOTAPI_ARTIFACT_BASE_URL", "https://artifactapi.k8s.syd1.au.unkin.net/api/v1/remote"), "/"),
|
|
BootBaseURL: strings.TrimRight(os.Getenv("BOOTAPI_BOOT_BASE_URL"), "/"),
|
|
ProvisionToken: provToken,
|
|
PuppetServer: getenv("BOOTAPI_PUPPET_SERVER", "puppet.k8s.syd1.au.unkin.net"),
|
|
PuppetCAServer: getenv("BOOTAPI_PUPPET_CA_SERVER", "puppetca.k8s.syd1.au.unkin.net"),
|
|
PuppetCAURL: getenv("BOOTAPI_PUPPET_CA_URL", "puppetca.k8s.syd1.au.unkin.net"),
|
|
Domain: getenv("BOOTAPI_DOMAIN", "main.unkin.net"),
|
|
Nameservers: ns,
|
|
RootPasswordHash: rootHash,
|
|
SSHAuthorizedKeys: splitLines(os.Getenv("BOOTAPI_SSH_AUTHORIZED_KEYS")),
|
|
UnknownMACFallback: fallback,
|
|
}, nil
|
|
}
|
|
|
|
// readSecret returns the value of env key, or the trimmed contents of the file
|
|
// named by key+"_FILE" when that is set (the file wins).
|
|
func readSecret(key string) (string, error) {
|
|
v := os.Getenv(key)
|
|
if f := os.Getenv(key + "_FILE"); f != "" {
|
|
b, err := os.ReadFile(f)
|
|
if err != nil {
|
|
return "", fmt.Errorf("read %s_FILE %q: %w", key, f, err)
|
|
}
|
|
v = strings.TrimSpace(string(b))
|
|
}
|
|
return v, 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
|
|
}
|