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
338 lines
11 KiB
Go
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
|
|
}
|
|
}
|
|
}
|