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 81ad0b597c
106 changed files with 35984 additions and 0 deletions

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]
}