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:
Karl Clinger
2026-03-21 00:30:23 -05:00
commit 0ebe75b2ca
155 changed files with 63317 additions and 0 deletions

277
pkg/manifest/manifest.go Normal file
View File

@@ -0,0 +1,277 @@
/*
Manifest v2 — Workload manifest format for the Volt hybrid platform.
Defines the data structures and TOML parser for Volt workload manifests.
A manifest describes everything needed to launch a workload: the execution
mode (container, hybrid-native, hybrid-kvm, hybrid-emulated), kernel config,
security policy, resource limits, networking, and storage layout.
The canonical serialization format is TOML. JSON round-tripping is supported
via struct tags for API use.
Copyright (c) Armored Gates LLC. All rights reserved.
*/
package manifest
import (
"fmt"
"os"
"github.com/BurntSushi/toml"
)
// ── Execution Modes ──────────────────────────────────────────────────────────
// Mode selects the workload execution strategy.
type Mode string
const (
// ModeContainer runs a standard systemd-nspawn container with no custom
// kernel. Fastest to start, smallest footprint.
ModeContainer Mode = "container"
// ModeHybridNative runs a systemd-nspawn container in boot mode with the
// host kernel. Full namespace isolation with shared kernel. This is the
// primary Volt mode.
ModeHybridNative Mode = "hybrid-native"
// ModeHybridKVM runs the workload inside a lightweight KVM guest using a
// custom kernel. Strongest isolation boundary.
ModeHybridKVM Mode = "hybrid-kvm"
// ModeHybridEmulated runs the workload under user-mode emulation (e.g.
// proot or QEMU user-mode) for cross-architecture support.
ModeHybridEmulated Mode = "hybrid-emulated"
)
// ValidModes is the set of recognized execution modes.
var ValidModes = map[Mode]bool{
ModeContainer: true,
ModeHybridNative: true,
ModeHybridKVM: true,
ModeHybridEmulated: true,
}
// ── Landlock Profile Names ───────────────────────────────────────────────────
// LandlockProfile selects a pre-built Landlock policy or a custom path.
type LandlockProfile string
const (
LandlockStrict LandlockProfile = "strict"
LandlockDefault LandlockProfile = "default"
LandlockPermissive LandlockProfile = "permissive"
LandlockCustom LandlockProfile = "custom"
)
// ValidLandlockProfiles is the set of recognized Landlock profile names.
var ValidLandlockProfiles = map[LandlockProfile]bool{
LandlockStrict: true,
LandlockDefault: true,
LandlockPermissive: true,
LandlockCustom: true,
}
// ── Network Mode Names ───────────────────────────────────────────────────────
// NetworkMode selects the container network topology.
type NetworkMode string
const (
NetworkBridge NetworkMode = "bridge"
NetworkHost NetworkMode = "host"
NetworkNone NetworkMode = "none"
NetworkCustom NetworkMode = "custom"
)
// ValidNetworkModes is the set of recognized network modes.
var ValidNetworkModes = map[NetworkMode]bool{
NetworkBridge: true,
NetworkHost: true,
NetworkNone: true,
NetworkCustom: true,
}
// ── Writable Layer Mode ──────────────────────────────────────────────────────
// WritableLayerMode selects how the writable layer on top of the CAS rootfs
// is implemented.
type WritableLayerMode string
const (
WritableOverlay WritableLayerMode = "overlay"
WritableTmpfs WritableLayerMode = "tmpfs"
WritableNone WritableLayerMode = "none"
)
// ValidWritableLayerModes is the set of recognized writable layer modes.
var ValidWritableLayerModes = map[WritableLayerMode]bool{
WritableOverlay: true,
WritableTmpfs: true,
WritableNone: true,
}
// ── Manifest v2 ──────────────────────────────────────────────────────────────
// Manifest is the top-level workload manifest. Every field maps to a TOML
// section or key. The zero value is not valid — at minimum [workload].name
// and [workload].mode must be set.
type Manifest struct {
Workload WorkloadSection `toml:"workload" json:"workload"`
Kernel KernelSection `toml:"kernel" json:"kernel"`
Security SecuritySection `toml:"security" json:"security"`
Resources ResourceSection `toml:"resources" json:"resources"`
Network NetworkSection `toml:"network" json:"network"`
Storage StorageSection `toml:"storage" json:"storage"`
// Extends allows inheriting from a base manifest. The value is a path
// (relative to the current manifest) or a CAS reference.
Extends string `toml:"extends,omitempty" json:"extends,omitempty"`
// SourcePath records where this manifest was loaded from (not serialized
// to TOML). Empty for manifests built programmatically.
SourcePath string `toml:"-" json:"-"`
}
// WorkloadSection identifies the workload and its execution mode.
type WorkloadSection struct {
Name string `toml:"name" json:"name"`
Mode Mode `toml:"mode" json:"mode"`
Image string `toml:"image,omitempty" json:"image,omitempty"`
Description string `toml:"description,omitempty" json:"description,omitempty"`
}
// KernelSection configures the kernel for hybrid modes. Ignored in container
// mode.
type KernelSection struct {
Version string `toml:"version,omitempty" json:"version,omitempty"`
Path string `toml:"path,omitempty" json:"path,omitempty"`
Modules []string `toml:"modules,omitempty" json:"modules,omitempty"`
Cmdline string `toml:"cmdline,omitempty" json:"cmdline,omitempty"`
}
// SecuritySection configures the security policy.
type SecuritySection struct {
LandlockProfile string `toml:"landlock_profile,omitempty" json:"landlock_profile,omitempty"`
SeccompProfile string `toml:"seccomp_profile,omitempty" json:"seccomp_profile,omitempty"`
Capabilities []string `toml:"capabilities,omitempty" json:"capabilities,omitempty"`
ReadOnlyRootfs bool `toml:"read_only_rootfs,omitempty" json:"read_only_rootfs,omitempty"`
}
// ResourceSection configures cgroups v2 resource limits. All values use
// human-readable strings (e.g. "512M", "2G") that are parsed at validation
// time.
type ResourceSection struct {
MemoryLimit string `toml:"memory_limit,omitempty" json:"memory_limit,omitempty"`
MemorySoft string `toml:"memory_soft,omitempty" json:"memory_soft,omitempty"`
CPUWeight int `toml:"cpu_weight,omitempty" json:"cpu_weight,omitempty"`
CPUSet string `toml:"cpu_set,omitempty" json:"cpu_set,omitempty"`
IOWeight int `toml:"io_weight,omitempty" json:"io_weight,omitempty"`
PidsMax int `toml:"pids_max,omitempty" json:"pids_max,omitempty"`
}
// NetworkSection configures the container network.
type NetworkSection struct {
Mode NetworkMode `toml:"mode,omitempty" json:"mode,omitempty"`
Address string `toml:"address,omitempty" json:"address,omitempty"`
DNS []string `toml:"dns,omitempty" json:"dns,omitempty"`
Ports []string `toml:"ports,omitempty" json:"ports,omitempty"`
}
// StorageSection configures the rootfs and volumes.
type StorageSection struct {
Rootfs string `toml:"rootfs,omitempty" json:"rootfs,omitempty"`
Volumes []VolumeMount `toml:"volumes,omitempty" json:"volumes,omitempty"`
WritableLayer WritableLayerMode `toml:"writable_layer,omitempty" json:"writable_layer,omitempty"`
}
// VolumeMount describes a bind mount from host to container.
type VolumeMount struct {
Host string `toml:"host" json:"host"`
Container string `toml:"container" json:"container"`
ReadOnly bool `toml:"readonly,omitempty" json:"readonly,omitempty"`
}
// ── Parser ───────────────────────────────────────────────────────────────────
// LoadFile reads a TOML manifest from disk and returns the parsed Manifest.
// No validation or resolution is performed — call Validate() and Resolve()
// separately.
func LoadFile(path string) (*Manifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read manifest: %w", err)
}
m, err := Parse(data)
if err != nil {
return nil, fmt.Errorf("parse %s: %w", path, err)
}
m.SourcePath = path
return m, nil
}
// Parse decodes a TOML document into a Manifest.
func Parse(data []byte) (*Manifest, error) {
var m Manifest
if err := toml.Unmarshal(data, &m); err != nil {
return nil, fmt.Errorf("toml decode: %w", err)
}
return &m, nil
}
// Encode serializes a Manifest to TOML bytes.
func Encode(m *Manifest) ([]byte, error) {
buf := new(tomlBuffer)
enc := toml.NewEncoder(buf)
if err := enc.Encode(m); err != nil {
return nil, fmt.Errorf("toml encode: %w", err)
}
return buf.Bytes(), nil
}
// tomlBuffer wraps a byte slice to satisfy io.Writer for the TOML encoder.
type tomlBuffer struct {
data []byte
}
func (b *tomlBuffer) Write(p []byte) (int, error) {
b.data = append(b.data, p...)
return len(p), nil
}
func (b *tomlBuffer) Bytes() []byte {
return b.data
}
// ── Convenience ──────────────────────────────────────────────────────────────
// IsHybrid returns true if the workload mode requires kernel isolation.
func (m *Manifest) IsHybrid() bool {
switch m.Workload.Mode {
case ModeHybridNative, ModeHybridKVM, ModeHybridEmulated:
return true
default:
return false
}
}
// NeedsKernel returns true if the workload mode requires a kernel path.
func (m *Manifest) NeedsKernel() bool {
return m.Workload.Mode == ModeHybridNative || m.Workload.Mode == ModeHybridKVM
}
// HasCASRootfs returns true if the storage rootfs references the CAS store.
func (m *Manifest) HasCASRootfs() bool {
return len(m.Storage.Rootfs) > 6 && m.Storage.Rootfs[:6] == "cas://"
}
// CASDigest extracts the digest from a cas:// reference, e.g.
// "cas://sha256:abc123" → "sha256:abc123". Returns empty string if the
// rootfs is not a CAS reference.
func (m *Manifest) CASDigest() string {
if !m.HasCASRootfs() {
return ""
}
return m.Storage.Rootfs[6:]
}

337
pkg/manifest/resolve.go Normal file
View 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
}
}
}

561
pkg/manifest/validate.go Normal file
View File

@@ -0,0 +1,561 @@
/*
Manifest Validation — Validates Volt v2 manifests before execution.
Checks include:
- Required fields (name, mode)
- Enum validation for mode, network, landlock, seccomp, writable_layer
- Resource limit parsing (human-readable: "512M", "2G")
- Port mapping parsing ("80:80/tcp", "443:443/udp")
- CAS reference validation ("cas://sha256:<hex>")
- Kernel path existence for hybrid modes
- Workload name safety (delegates to validate.WorkloadName)
Provides both strict Validate() and informational DryRun().
Copyright (c) Armored Gates LLC. All rights reserved.
*/
package manifest
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"github.com/armoredgate/volt/pkg/validate"
)
// ── Validation Errors ────────────────────────────────────────────────────────
// ValidationError collects one or more field-level errors.
type ValidationError struct {
Errors []FieldError
}
func (ve *ValidationError) Error() string {
var b strings.Builder
b.WriteString("manifest validation failed:\n")
for _, fe := range ve.Errors {
fmt.Fprintf(&b, " [%s] %s\n", fe.Field, fe.Message)
}
return b.String()
}
// FieldError records a single validation failure for a specific field.
type FieldError struct {
Field string // e.g. "workload.name", "resources.memory_limit"
Message string
}
// ── Dry Run Report ───────────────────────────────────────────────────────────
// Severity classifies a report finding.
type Severity string
const (
SeverityError Severity = "error"
SeverityWarning Severity = "warning"
SeverityInfo Severity = "info"
)
// Finding is a single line item in a DryRun report.
type Finding struct {
Severity Severity
Field string
Message string
}
// Report is the output of DryRun. It contains findings at varying severity
// levels and a summary of resolved resource values.
type Report struct {
Findings []Finding
// Resolved values (populated during dry run for display)
ResolvedMemoryLimit int64 // bytes
ResolvedMemorySoft int64 // bytes
ResolvedPortMaps []PortMapping
}
// HasErrors returns true if any finding is severity error.
func (r *Report) HasErrors() bool {
for _, f := range r.Findings {
if f.Severity == SeverityError {
return true
}
}
return false
}
// PortMapping is the parsed representation of a port string like "80:80/tcp".
type PortMapping struct {
HostPort int
ContainerPort int
Protocol string // "tcp" or "udp"
}
// ── Validate ─────────────────────────────────────────────────────────────────
// Validate performs strict validation of a manifest. Returns nil if the
// manifest is valid. Returns a *ValidationError containing all field errors
// otherwise.
func (m *Manifest) Validate() error {
var errs []FieldError
// ── workload ─────────────────────────────────────────────────────────
if m.Workload.Name == "" {
errs = append(errs, FieldError{
Field: "workload.name",
Message: "required field is empty",
})
} else if err := validate.WorkloadName(m.Workload.Name); err != nil {
errs = append(errs, FieldError{
Field: "workload.name",
Message: err.Error(),
})
}
if m.Workload.Mode == "" {
errs = append(errs, FieldError{
Field: "workload.mode",
Message: "required field is empty",
})
} else if !ValidModes[m.Workload.Mode] {
errs = append(errs, FieldError{
Field: "workload.mode",
Message: fmt.Sprintf("invalid mode %q (valid: container, hybrid-native, hybrid-kvm, hybrid-emulated)", m.Workload.Mode),
})
}
// ── kernel (hybrid modes only) ───────────────────────────────────────
if m.NeedsKernel() {
if m.Kernel.Path != "" {
if _, err := os.Stat(m.Kernel.Path); err != nil {
errs = append(errs, FieldError{
Field: "kernel.path",
Message: fmt.Sprintf("kernel not found: %s", m.Kernel.Path),
})
}
}
// If no path and no version, the kernel manager will use defaults at
// runtime — that's acceptable. We only error if an explicit path is
// given and missing.
}
// ── security ─────────────────────────────────────────────────────────
if m.Security.LandlockProfile != "" {
lp := LandlockProfile(m.Security.LandlockProfile)
if !ValidLandlockProfiles[lp] {
// Could be a file path for custom profile — check if it looks like
// a path (contains / or .)
if !looksLikePath(m.Security.LandlockProfile) {
errs = append(errs, FieldError{
Field: "security.landlock_profile",
Message: fmt.Sprintf("invalid profile %q (valid: strict, default, permissive, custom, or a file path)", m.Security.LandlockProfile),
})
}
}
}
if m.Security.SeccompProfile != "" {
validSeccomp := map[string]bool{
"strict": true, "default": true, "unconfined": true,
}
if !validSeccomp[m.Security.SeccompProfile] && !looksLikePath(m.Security.SeccompProfile) {
errs = append(errs, FieldError{
Field: "security.seccomp_profile",
Message: fmt.Sprintf("invalid profile %q (valid: strict, default, unconfined, or a file path)", m.Security.SeccompProfile),
})
}
}
if len(m.Security.Capabilities) > 0 {
for _, cap := range m.Security.Capabilities {
if !isValidCapability(cap) {
errs = append(errs, FieldError{
Field: "security.capabilities",
Message: fmt.Sprintf("unknown capability %q", cap),
})
}
}
}
// ── resources ────────────────────────────────────────────────────────
if m.Resources.MemoryLimit != "" {
if _, err := ParseMemorySize(m.Resources.MemoryLimit); err != nil {
errs = append(errs, FieldError{
Field: "resources.memory_limit",
Message: err.Error(),
})
}
}
if m.Resources.MemorySoft != "" {
if _, err := ParseMemorySize(m.Resources.MemorySoft); err != nil {
errs = append(errs, FieldError{
Field: "resources.memory_soft",
Message: err.Error(),
})
}
}
if m.Resources.CPUWeight != 0 {
if m.Resources.CPUWeight < 1 || m.Resources.CPUWeight > 10000 {
errs = append(errs, FieldError{
Field: "resources.cpu_weight",
Message: fmt.Sprintf("cpu_weight %d out of range [1, 10000]", m.Resources.CPUWeight),
})
}
}
if m.Resources.CPUSet != "" {
if err := validateCPUSet(m.Resources.CPUSet); err != nil {
errs = append(errs, FieldError{
Field: "resources.cpu_set",
Message: err.Error(),
})
}
}
if m.Resources.IOWeight != 0 {
if m.Resources.IOWeight < 1 || m.Resources.IOWeight > 10000 {
errs = append(errs, FieldError{
Field: "resources.io_weight",
Message: fmt.Sprintf("io_weight %d out of range [1, 10000]", m.Resources.IOWeight),
})
}
}
if m.Resources.PidsMax != 0 {
if m.Resources.PidsMax < 1 {
errs = append(errs, FieldError{
Field: "resources.pids_max",
Message: "pids_max must be positive",
})
}
}
// ── network ──────────────────────────────────────────────────────────
if m.Network.Mode != "" && !ValidNetworkModes[m.Network.Mode] {
errs = append(errs, FieldError{
Field: "network.mode",
Message: fmt.Sprintf("invalid network mode %q (valid: bridge, host, none, custom)", m.Network.Mode),
})
}
for i, port := range m.Network.Ports {
if _, err := ParsePortMapping(port); err != nil {
errs = append(errs, FieldError{
Field: fmt.Sprintf("network.ports[%d]", i),
Message: err.Error(),
})
}
}
// ── storage ──────────────────────────────────────────────────────────
if m.Storage.Rootfs != "" && m.HasCASRootfs() {
if err := validateCASRef(m.Storage.Rootfs); err != nil {
errs = append(errs, FieldError{
Field: "storage.rootfs",
Message: err.Error(),
})
}
}
if m.Storage.WritableLayer != "" && !ValidWritableLayerModes[m.Storage.WritableLayer] {
errs = append(errs, FieldError{
Field: "storage.writable_layer",
Message: fmt.Sprintf("invalid writable_layer %q (valid: overlay, tmpfs, none)", m.Storage.WritableLayer),
})
}
for i, vol := range m.Storage.Volumes {
if vol.Host == "" {
errs = append(errs, FieldError{
Field: fmt.Sprintf("storage.volumes[%d].host", i),
Message: "host path is required",
})
}
if vol.Container == "" {
errs = append(errs, FieldError{
Field: fmt.Sprintf("storage.volumes[%d].container", i),
Message: "container path is required",
})
}
}
if len(errs) > 0 {
return &ValidationError{Errors: errs}
}
return nil
}
// ── DryRun ───────────────────────────────────────────────────────────────────
// DryRun performs validation and additionally resolves human-readable resource
// values into machine values, returning a Report with findings and resolved
// values. Unlike Validate(), DryRun never returns an error — the Report itself
// carries severity information.
func (m *Manifest) DryRun() *Report {
r := &Report{}
// Run validation and collect errors as findings.
if err := m.Validate(); err != nil {
if ve, ok := err.(*ValidationError); ok {
for _, fe := range ve.Errors {
r.Findings = append(r.Findings, Finding{
Severity: SeverityError,
Field: fe.Field,
Message: fe.Message,
})
}
}
}
// ── Informational findings ───────────────────────────────────────────
// Resolve memory limits.
if m.Resources.MemoryLimit != "" {
if bytes, err := ParseMemorySize(m.Resources.MemoryLimit); err == nil {
r.ResolvedMemoryLimit = bytes
r.Findings = append(r.Findings, Finding{
Severity: SeverityInfo,
Field: "resources.memory_limit",
Message: fmt.Sprintf("resolved to %d bytes (%s)", bytes, m.Resources.MemoryLimit),
})
}
} else {
r.Findings = append(r.Findings, Finding{
Severity: SeverityWarning,
Field: "resources.memory_limit",
Message: "not set — workload will have no memory limit",
})
}
if m.Resources.MemorySoft != "" {
if bytes, err := ParseMemorySize(m.Resources.MemorySoft); err == nil {
r.ResolvedMemorySoft = bytes
}
}
// Resolve port mappings.
for _, port := range m.Network.Ports {
if pm, err := ParsePortMapping(port); err == nil {
r.ResolvedPortMaps = append(r.ResolvedPortMaps, pm)
}
}
// Warn about container mode with kernel section.
if m.Workload.Mode == ModeContainer && (m.Kernel.Path != "" || m.Kernel.Version != "") {
r.Findings = append(r.Findings, Finding{
Severity: SeverityWarning,
Field: "kernel",
Message: "kernel section is set but mode is 'container' — kernel config will be ignored",
})
}
// Warn about hybrid modes without kernel section.
if m.NeedsKernel() && m.Kernel.Path == "" && m.Kernel.Version == "" {
r.Findings = append(r.Findings, Finding{
Severity: SeverityWarning,
Field: "kernel",
Message: "hybrid mode selected but no kernel specified — will use host default",
})
}
// Check soft < hard memory.
if r.ResolvedMemoryLimit > 0 && r.ResolvedMemorySoft > 0 {
if r.ResolvedMemorySoft > r.ResolvedMemoryLimit {
r.Findings = append(r.Findings, Finding{
Severity: SeverityWarning,
Field: "resources.memory_soft",
Message: "memory_soft exceeds memory_limit — soft limit will have no effect",
})
}
}
// Info about writable layer.
if m.Storage.WritableLayer == WritableNone {
r.Findings = append(r.Findings, Finding{
Severity: SeverityInfo,
Field: "storage.writable_layer",
Message: "writable_layer is 'none' — rootfs will be completely read-only",
})
}
return r
}
// ── Parsers ──────────────────────────────────────────────────────────────────
// ParseMemorySize parses a human-readable memory size string into bytes.
// Supports: "512M", "2G", "1024K", "1T", "256m", "100" (raw bytes).
func ParseMemorySize(s string) (int64, error) {
s = strings.TrimSpace(s)
if s == "" {
return 0, fmt.Errorf("empty memory size")
}
// Raw integer (bytes).
if n, err := strconv.ParseInt(s, 10, 64); err == nil {
return n, nil
}
// Strip unit suffix.
upper := strings.ToUpper(s)
var multiplier int64 = 1
var numStr string
switch {
case strings.HasSuffix(upper, "T"):
multiplier = 1024 * 1024 * 1024 * 1024
numStr = s[:len(s)-1]
case strings.HasSuffix(upper, "G"):
multiplier = 1024 * 1024 * 1024
numStr = s[:len(s)-1]
case strings.HasSuffix(upper, "M"):
multiplier = 1024 * 1024
numStr = s[:len(s)-1]
case strings.HasSuffix(upper, "K"):
multiplier = 1024
numStr = s[:len(s)-1]
default:
return 0, fmt.Errorf("invalid memory size %q: expected a number with optional suffix K/M/G/T", s)
}
n, err := strconv.ParseFloat(strings.TrimSpace(numStr), 64)
if err != nil {
return 0, fmt.Errorf("invalid memory size %q: %w", s, err)
}
if n < 0 {
return 0, fmt.Errorf("invalid memory size %q: negative value", s)
}
return int64(n * float64(multiplier)), nil
}
// portRegex matches "hostPort:containerPort/protocol" or "hostPort:containerPort".
var portRegex = regexp.MustCompile(`^(\d+):(\d+)(?:/(tcp|udp))?$`)
// ParsePortMapping parses a port mapping string like "80:80/tcp".
func ParsePortMapping(s string) (PortMapping, error) {
s = strings.TrimSpace(s)
matches := portRegex.FindStringSubmatch(s)
if matches == nil {
return PortMapping{}, fmt.Errorf("invalid port mapping %q: expected hostPort:containerPort[/tcp|udp]", s)
}
hostPort, _ := strconv.Atoi(matches[1])
containerPort, _ := strconv.Atoi(matches[2])
proto := matches[3]
if proto == "" {
proto = "tcp"
}
if hostPort < 1 || hostPort > 65535 {
return PortMapping{}, fmt.Errorf("invalid host port %d: must be 1-65535", hostPort)
}
if containerPort < 1 || containerPort > 65535 {
return PortMapping{}, fmt.Errorf("invalid container port %d: must be 1-65535", containerPort)
}
return PortMapping{
HostPort: hostPort,
ContainerPort: containerPort,
Protocol: proto,
}, nil
}
// ── Internal Helpers ─────────────────────────────────────────────────────────
// casRefRegex matches "cas://sha256:<hex>" or "cas://sha512:<hex>".
var casRefRegex = regexp.MustCompile(`^cas://(sha256|sha512):([0-9a-fA-F]+)$`)
// validateCASRef validates a CAS reference string.
func validateCASRef(ref string) error {
if !casRefRegex.MatchString(ref) {
return fmt.Errorf("invalid CAS reference %q: expected cas://sha256:<hex> or cas://sha512:<hex>", ref)
}
return nil
}
// cpuSetRegex matches ranges like "0-3", "0,1,2,3", "0-3,8-11".
var cpuSetRegex = regexp.MustCompile(`^(\d+(-\d+)?)(,\d+(-\d+)?)*$`)
// validateCPUSet validates a cpuset string.
func validateCPUSet(s string) error {
if !cpuSetRegex.MatchString(s) {
return fmt.Errorf("invalid cpu_set %q: expected ranges like '0-3' or '0,1,2,3'", s)
}
// Verify ranges are valid (start <= end).
for _, part := range strings.Split(s, ",") {
if strings.Contains(part, "-") {
bounds := strings.SplitN(part, "-", 2)
start, _ := strconv.Atoi(bounds[0])
end, _ := strconv.Atoi(bounds[1])
if start > end {
return fmt.Errorf("invalid cpu_set range %q: start (%d) > end (%d)", part, start, end)
}
}
}
return nil
}
// looksLikePath returns true if s looks like a filesystem path.
func looksLikePath(s string) bool {
return strings.Contains(s, "/") || strings.Contains(s, ".")
}
// knownCapabilities is the set of recognized Linux capabilities (without the
// CAP_ prefix for convenience).
var knownCapabilities = map[string]bool{
"AUDIT_CONTROL": true,
"AUDIT_READ": true,
"AUDIT_WRITE": true,
"BLOCK_SUSPEND": true,
"BPF": true,
"CHECKPOINT_RESTORE": true,
"CHOWN": true,
"DAC_OVERRIDE": true,
"DAC_READ_SEARCH": true,
"FOWNER": true,
"FSETID": true,
"IPC_LOCK": true,
"IPC_OWNER": true,
"KILL": true,
"LEASE": true,
"LINUX_IMMUTABLE": true,
"MAC_ADMIN": true,
"MAC_OVERRIDE": true,
"MKNOD": true,
"NET_ADMIN": true,
"NET_BIND_SERVICE": true,
"NET_BROADCAST": true,
"NET_RAW": true,
"PERFMON": true,
"SETFCAP": true,
"SETGID": true,
"SETPCAP": true,
"SETUID": true,
"SYSLOG": true,
"SYS_ADMIN": true,
"SYS_BOOT": true,
"SYS_CHROOT": true,
"SYS_MODULE": true,
"SYS_NICE": true,
"SYS_PACCT": true,
"SYS_PTRACE": true,
"SYS_RAWIO": true,
"SYS_RESOURCE": true,
"SYS_TIME": true,
"SYS_TTY_CONFIG": true,
"WAKE_ALARM": true,
}
// isValidCapability checks if a capability name is recognized.
// Accepts with or without "CAP_" prefix.
func isValidCapability(name string) bool {
upper := strings.ToUpper(strings.TrimPrefix(name, "CAP_"))
return knownCapabilities[upper]
}