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
This commit is contained in:
337
pkg/manifest/resolve.go
Normal file
337
pkg/manifest/resolve.go
Normal file
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user