Files
volt/pkg/manifest/resolve.go
Karl Clinger 0ebe75b2ca Volt CLI: source-available under AGPSL v5.0
Complete infrastructure platform CLI:
- Container runtime (systemd-nspawn)
- VoltVisor VMs (Neutron Stardust / QEMU)
- Stellarium CAS (content-addressed storage)
- ORAS Registry
- GitOps integration
- Landlock LSM security
- Compose orchestration
- Mesh networking

Copyright (c) Armored Gates LLC. All rights reserved.
Licensed under AGPSL v5.0
2026-03-21 02:08:15 -05:00

338 lines
11 KiB
Go

/*
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
}
}
}