sorcery-go/pkg/config/config.go

235 lines
9.1 KiB
Go
Executable File

// Package config holds the runtime configuration for a Sorcery-Go process.
//
// One Config is constructed at startup (from CLI flags + env vars + a small
// /etc/sorcery-go/config.yaml if present) and threaded through every pkg/*
// call site. This replaces the global Bash variables that the original
// Sorcery used.
//
// Paths default to /var/lib/sorcery-go so a Sorcery-Go binary can be
// dropped into an existing Source Mage chroot WITHOUT overwriting the
// legacy Bash state under /var/lib/sorcery. Both tools can coexist while
// the migration is in progress.
package config
import (
"fmt"
"log"
"net/http"
"os"
"path/filepath"
"runtime"
"time"
)
// Config is the runtime configuration.
type Config struct {
// RootDir is the engine's state root. Defaults to /var/lib/sorcery-go
// so we never clobber the legacy /var/lib/sorcery tree.
RootDir string
// GrimoirePath is the directory containing the spell tree. We default
// to the real Source Mage location so an existing chroot "just works".
GrimoirePath string
// StateDB is the bbolt file path.
StateDB string
// TombRoot is the content-addressable blob store root.
TombRoot string
// BuildRoot is where sandbox overlays are mounted.
BuildRoot string
// SpoolDir is where downloaded source tarballs land.
SpoolDir string
// LogDir is where per-spell build logs are persisted.
LogDir string
// Concurrency is the worker-pool size for parallel casts.
Concurrency int
// HostArch is the architecture of the running host.
HostArch string
// ActivePosture is the active legal profile name
// (strict_copyleft | corporate_lite | lawless).
ActivePosture string
// Firewall is which network gatekeeper is active
// (opensnitch | portmaster | none).
Firewall string
// PGPKeyring is the GnuPG keyring used to verify signed DETAILS files.
// Empty disables PGP attestation (the Warding will warn but not block).
PGPKeyring string
// Runtime is the container runtime to use for Sanctum management.
// (lxc | podman | firecracker | baremetal | auto)
// "auto" probes for available runtimes in order.
Runtime string
// EBPFEnforce controls whether the eBPF Tomb Guard operates in
// enforcing mode (true) or permissive/log-only mode (false).
EBPFEnforce bool
// FirecrackerBin is the path to the firecracker VMM binary.
// Used only when Runtime == "firecracker".
FirecrackerBin string
// FirecrackerKernel is the path to the kernel image for Firecracker VMs.
FirecrackerKernel string
// NetworkBridge is the host bridge interface for container networking.
// Defaults to "br0".
NetworkBridge string
// FesterURL is the URL of the Fester cluster controller.
// When set, distributed build scheduling and node telemetry are
// delegated to Fester. Empty means standalone mode.
// Example: "http://192.168.1.100:8787"
FesterURL string
// Toolchain selects which compiler toolchain to use.
// "gcc" (default) uses the host system GCC.
// "btc" uses a BTC.sh sovereign-forged toolchain.
Toolchain string
// BTCPath is the path to the BTC.sh script.
BTCPath string
// BTCRoot is the BTC persistent archive root directory.
BTCRoot string
// BTCSYSLabel is the pre-detected SYS_LABEL for the BTC toolchain.
// When empty, sorcery-go will auto-detect it from the golden image
// filename at startup.
BTCSYSLabel string
}
// Default returns a Config wired for an existing Source Mage chroot.
// Every path is overridable via env var (SORCERY_GO_<NAME>) so the same
// binary can run in CI, in a chroot, or in any supported container runtime.
func Default() *Config {
cfg := &Config{
RootDir: envOr("SORCERY_GO_ROOT", "/var/lib/sorcery-go"),
GrimoirePath: envOr("SORCERY_GO_GRIMOIRE", "/var/lib/sorcery/codex/grimoire"),
Concurrency: runtime.NumCPU(),
HostArch: hostArch(),
ActivePosture: envOr("SORCERY_GO_POSTURE", "strict_copyleft"),
Firewall: envOr("SORCERY_GO_FIREWALL", "none"),
PGPKeyring: envOr("SORCERY_GO_PGP_KEYRING", "/etc/sorcery-go/keyring.gpg"),
Runtime: envOr("SORCERY_GO_RUNTIME", "auto"),
EBPFEnforce: envBool("SORCERY_GO_EBPF_ENFORCE", false),
FirecrackerBin: envOr("SORCERY_GO_FIRECRACKER_BIN", "firecracker"),
FirecrackerKernel: envOr("SORCERY_GO_FIRECRACKER_KERNEL", ""),
NetworkBridge: envOr("SORCERY_GO_NETWORK_BRIDGE", "br0"),
FesterURL: envOr("SORCERY_GO_FESTER_URL", ""),
Toolchain: envOr("SORCERY_GO_TOOLCHAIN", "gcc"),
BTCPath: envOr("SORCERY_GO_BTC_PATH", "/opt/BTC/BTC.sh"),
BTCRoot: envOr("SORCERY_GO_BTC_ROOT", "/opt/BTC"),
BTCSYSLabel: envOr("SORCERY_GO_BTC_SYS_LABEL", ""),
}
cfg.StateDB = filepath.Join(cfg.RootDir, "state", "state.db")
cfg.TombRoot = filepath.Join(cfg.RootDir, "tomb")
cfg.BuildRoot = filepath.Join(cfg.RootDir, "build")
cfg.SpoolDir = envOr("SORCERY_GO_SPOOL", "/var/spool/sorcery-go")
cfg.LogDir = filepath.Join(cfg.RootDir, "log")
return cfg
}
// EnsureDirs creates every state directory with the right mode. Idempotent.
// Must be called as root (or with write access to RootDir).
func (c *Config) EnsureDirs() error {
dirs := []struct {
path string
mode os.FileMode
}{
{c.RootDir, 0755},
{filepath.Dir(c.StateDB), 0700},
{c.TombRoot, 0700},
{filepath.Join(c.TombRoot, "epitaphs"), 0700},
{filepath.Join(c.TombRoot, "blobs"), 0700},
{c.BuildRoot, 0755},
{c.SpoolDir, 0755},
{c.LogDir, 0755},
}
for _, d := range dirs {
if err := os.MkdirAll(d.path, d.mode); err != nil {
return fmt.Errorf("config: mkdir %s: %w", d.path, err)
}
}
return nil
}
// IsRoot reports whether the process is running as UID 0. Required for
// OverlayFS mounts and atomic commits to the live root.
func IsRoot() bool { return os.Geteuid() == 0 }
// ValidateFesterURL checks whether the configured FesterURL is reachable.
// It logs a warning if the URL is set but the host does not respond.
// This is a non-blocking best-effort check — failure does not prevent startup.
func (c *Config) ValidateFesterURL() {
if c.FesterURL == "" {
return
}
client := &http.Client{Timeout: 3 * time.Second}
resp, err := client.Get(c.FesterURL + "/api/health")
if err != nil {
log.Printf("config: warning: FesterURL %s is not reachable: %v", c.FesterURL, err)
log.Printf("config: distributed build features will be unavailable until Fester is accessible")
return
}
resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Printf("config: warning: FesterURL %s returned status %d", c.FesterURL, resp.StatusCode)
} else {
log.Printf("config: FesterURL %s is reachable", c.FesterURL)
}
}
// ValidatePaths checks that critical paths exist and logs warnings
// for any that are missing. This gives operators clear error messages
// instead of cryptic "file not found" errors later.
func (c *Config) ValidatePaths() {
if _, err := os.Stat(c.GrimoirePath); os.IsNotExist(err) {
log.Printf("config: warning: GrimoirePath %s does not exist", c.GrimoirePath)
log.Printf("config: no spell definitions will be available. Set SORCERY_GO_GRIMOIRE or install a grimoire.")
}
if c.Toolchain == "btc" {
if _, err := os.Stat(c.BTCRoot); os.IsNotExist(err) {
log.Printf("config: warning: BTCRoot %s does not exist", c.BTCRoot)
log.Printf("config: BTC toolchain will not be available. Run BTC.sh first or set SORCERY_GO_BTC_ROOT.")
}
}
}
func envOr(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envBool(key string, def bool) bool {
v := os.Getenv(key)
if v == "" {
return def
}
return v == "1" || v == "true" || v == "yes"
}
// archMap maps runtime.GOARCH to Source Mage canonical arch names.
var archMap = map[string]string{
"arm64": "aarch64",
"amd64": "x86_64",
"386": "i686",
}
func hostArch() string {
if arch, ok := archMap[runtime.GOARCH]; ok {
return arch
}
return runtime.GOARCH
}