/* Manifest Resolution — Resolves variable substitutions, inheritance, and defaults for Volt v2 manifests. Resolution pipeline: 1. Load base manifest (if `extends` is set) 2. Merge current manifest on top of base (current wins) 3. Substitute ${VAR} references from environment and built-in vars 4. Apply mode-specific defaults 5. Fill missing optional fields with sensible defaults Copyright (c) Armored Gates LLC. All rights reserved. */ package manifest import ( "fmt" "os" "path/filepath" "regexp" "strings" ) // ── Built-in Variables ─────────────────────────────────────────────────────── // builtinVars returns the set of variables that are always available for // substitution, regardless of the environment. func builtinVars() map[string]string { hostname, _ := os.Hostname() return map[string]string{ "HOSTNAME": hostname, "VOLT_BASE": "/var/lib/volt", "VOLT_CAS_DIR": "/var/lib/volt/cas", "VOLT_RUN_DIR": "/var/run/volt", } } // varRegex matches ${VAR_NAME} patterns. Supports alphanumeric, underscores, // and dots. var varRegex = regexp.MustCompile(`\$\{([A-Za-z_][A-Za-z0-9_.]*)\}`) // ── Resolve ────────────────────────────────────────────────────────────────── // Resolve performs the full resolution pipeline on a manifest: // 1. Extends (inheritance) // 2. Variable substitution // 3. Default values // // The manifest is modified in place and also returned for convenience. // envOverrides provides additional variables that take precedence over both // built-in vars and the OS environment. func Resolve(m *Manifest, envOverrides map[string]string) (*Manifest, error) { // Step 1: Handle extends (inheritance). if m.Extends != "" { base, err := resolveExtends(m) if err != nil { return nil, fmt.Errorf("resolve extends: %w", err) } mergeManifest(base, m) *m = *base } // Step 2: Variable substitution. substituteVars(m, envOverrides) // Step 3: Apply defaults. applyDefaults(m) return m, nil } // ── Extends / Inheritance ──────────────────────────────────────────────────── // resolveExtends loads the base manifest referenced by m.Extends. The path // is resolved relative to the current manifest's SourcePath directory, or as // an absolute path. func resolveExtends(m *Manifest) (*Manifest, error) { ref := m.Extends // Resolve relative to the current manifest file. basePath := ref if !filepath.IsAbs(ref) && m.SourcePath != "" { basePath = filepath.Join(filepath.Dir(m.SourcePath), ref) } // Check if it's a CAS reference. if strings.HasPrefix(ref, "cas://") { return nil, fmt.Errorf("CAS-based extends not yet implemented: %s", ref) } base, err := LoadFile(basePath) if err != nil { return nil, fmt.Errorf("load base manifest %s: %w", basePath, err) } // Recursively resolve the base manifest (supports chained extends). if base.Extends != "" { if _, err := resolveExtends(base); err != nil { return nil, fmt.Errorf("resolve parent %s: %w", basePath, err) } } return base, nil } // mergeManifest overlays child values onto base. Non-zero child values // overwrite base values. Slices are replaced (not appended) when non-nil. func mergeManifest(base, child *Manifest) { // Workload — child always wins for non-empty fields. if child.Workload.Name != "" { base.Workload.Name = child.Workload.Name } if child.Workload.Mode != "" { base.Workload.Mode = child.Workload.Mode } if child.Workload.Image != "" { base.Workload.Image = child.Workload.Image } if child.Workload.Description != "" { base.Workload.Description = child.Workload.Description } // Kernel. if child.Kernel.Version != "" { base.Kernel.Version = child.Kernel.Version } if child.Kernel.Path != "" { base.Kernel.Path = child.Kernel.Path } if child.Kernel.Modules != nil { base.Kernel.Modules = child.Kernel.Modules } if child.Kernel.Cmdline != "" { base.Kernel.Cmdline = child.Kernel.Cmdline } // Security. if child.Security.LandlockProfile != "" { base.Security.LandlockProfile = child.Security.LandlockProfile } if child.Security.SeccompProfile != "" { base.Security.SeccompProfile = child.Security.SeccompProfile } if child.Security.Capabilities != nil { base.Security.Capabilities = child.Security.Capabilities } if child.Security.ReadOnlyRootfs { base.Security.ReadOnlyRootfs = child.Security.ReadOnlyRootfs } // Resources. if child.Resources.MemoryLimit != "" { base.Resources.MemoryLimit = child.Resources.MemoryLimit } if child.Resources.MemorySoft != "" { base.Resources.MemorySoft = child.Resources.MemorySoft } if child.Resources.CPUWeight != 0 { base.Resources.CPUWeight = child.Resources.CPUWeight } if child.Resources.CPUSet != "" { base.Resources.CPUSet = child.Resources.CPUSet } if child.Resources.IOWeight != 0 { base.Resources.IOWeight = child.Resources.IOWeight } if child.Resources.PidsMax != 0 { base.Resources.PidsMax = child.Resources.PidsMax } // Network. if child.Network.Mode != "" { base.Network.Mode = child.Network.Mode } if child.Network.Address != "" { base.Network.Address = child.Network.Address } if child.Network.DNS != nil { base.Network.DNS = child.Network.DNS } if child.Network.Ports != nil { base.Network.Ports = child.Network.Ports } // Storage. if child.Storage.Rootfs != "" { base.Storage.Rootfs = child.Storage.Rootfs } if child.Storage.Volumes != nil { base.Storage.Volumes = child.Storage.Volumes } if child.Storage.WritableLayer != "" { base.Storage.WritableLayer = child.Storage.WritableLayer } // Clear extends — the chain has been resolved. base.Extends = "" } // ── Variable Substitution ──────────────────────────────────────────────────── // substituteVars replaces ${VAR} patterns throughout all string fields of the // manifest. Resolution order: envOverrides > OS environment > built-in vars. func substituteVars(m *Manifest, envOverrides map[string]string) { vars := builtinVars() // Layer OS environment on top. for _, kv := range os.Environ() { parts := strings.SplitN(kv, "=", 2) if len(parts) == 2 { vars[parts[0]] = parts[1] } } // Layer explicit overrides on top (highest priority). for k, v := range envOverrides { vars[k] = v } resolve := func(s string) string { return varRegex.ReplaceAllStringFunc(s, func(match string) string { // Extract variable name from ${NAME}. varName := match[2 : len(match)-1] if val, ok := vars[varName]; ok { return val } // Leave unresolved variables in place. return match }) } // Walk all string fields. m.Workload.Name = resolve(m.Workload.Name) m.Workload.Image = resolve(m.Workload.Image) m.Workload.Description = resolve(m.Workload.Description) m.Kernel.Version = resolve(m.Kernel.Version) m.Kernel.Path = resolve(m.Kernel.Path) m.Kernel.Cmdline = resolve(m.Kernel.Cmdline) for i := range m.Kernel.Modules { m.Kernel.Modules[i] = resolve(m.Kernel.Modules[i]) } m.Security.LandlockProfile = resolve(m.Security.LandlockProfile) m.Security.SeccompProfile = resolve(m.Security.SeccompProfile) for i := range m.Security.Capabilities { m.Security.Capabilities[i] = resolve(m.Security.Capabilities[i]) } m.Resources.MemoryLimit = resolve(m.Resources.MemoryLimit) m.Resources.MemorySoft = resolve(m.Resources.MemorySoft) m.Resources.CPUSet = resolve(m.Resources.CPUSet) m.Network.Address = resolve(m.Network.Address) for i := range m.Network.DNS { m.Network.DNS[i] = resolve(m.Network.DNS[i]) } for i := range m.Network.Ports { m.Network.Ports[i] = resolve(m.Network.Ports[i]) } m.Storage.Rootfs = resolve(m.Storage.Rootfs) for i := range m.Storage.Volumes { m.Storage.Volumes[i].Host = resolve(m.Storage.Volumes[i].Host) m.Storage.Volumes[i].Container = resolve(m.Storage.Volumes[i].Container) } } // ── Default Values ─────────────────────────────────────────────────────────── // applyDefaults fills missing optional fields with sensible default values. // Mode-specific logic is applied — e.g. container mode clears kernel section. func applyDefaults(m *Manifest) { // ── Security defaults ──────────────────────────────────────────────── if m.Security.LandlockProfile == "" { m.Security.LandlockProfile = string(LandlockDefault) } if m.Security.SeccompProfile == "" { m.Security.SeccompProfile = "default" } // ── Resource defaults ──────────────────────────────────────────────── if m.Resources.CPUWeight == 0 { m.Resources.CPUWeight = 100 } if m.Resources.IOWeight == 0 { m.Resources.IOWeight = 100 } if m.Resources.PidsMax == 0 { m.Resources.PidsMax = 4096 } // ── Network defaults ───────────────────────────────────────────────── if m.Network.Mode == "" { m.Network.Mode = NetworkBridge } if len(m.Network.DNS) == 0 { m.Network.DNS = []string{"1.1.1.1", "1.0.0.1"} } // ── Storage defaults ───────────────────────────────────────────────── if m.Storage.WritableLayer == "" { m.Storage.WritableLayer = WritableOverlay } // ── Mode-specific adjustments ──────────────────────────────────────── switch m.Workload.Mode { case ModeContainer: // Container mode does not use a custom kernel. Clear the kernel // section to avoid confusion. m.Kernel = KernelSection{} case ModeHybridNative: // Ensure sensible kernel module defaults for hybrid-native. if len(m.Kernel.Modules) == 0 { m.Kernel.Modules = []string{"overlay", "br_netfilter", "veth"} } if m.Kernel.Cmdline == "" { m.Kernel.Cmdline = "console=ttyS0 quiet" } case ModeHybridKVM: // KVM mode benefits from slightly more memory by default. if m.Resources.MemoryLimit == "" { m.Resources.MemoryLimit = "1G" } if m.Kernel.Cmdline == "" { m.Kernel.Cmdline = "console=ttyS0 quiet" } case ModeHybridEmulated: // Emulated mode is CPU-heavy; give it a larger PID space. if m.Resources.PidsMax == 4096 { m.Resources.PidsMax = 8192 } } }