Files
volt/scripts/build-images.sh
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

423 lines
11 KiB
Bash
Executable File

#!/bin/bash
#
# Volt Platform - Image Builder
# Creates TinyVol images from definitions
#
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
CONFIG_DIR="$PROJECT_DIR/configs/images"
OUTPUT_DIR="${OUTPUT_DIR:-/var/lib/volt/images}"
CACHE_DIR="${CACHE_DIR:-/var/cache/volt/packages}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
log() { echo -e "${GREEN}[volt]${NC} $1"; }
info() { echo -e "${BLUE}[volt]${NC} $1"; }
warn() { echo -e "${YELLOW}[volt]${NC} $1"; }
error() { echo -e "${RED}[volt]${NC} $1" >&2; }
# Base packages for each userland type
declare -A USERLAND_PACKAGES=(
["musl-minimal"]="musl busybox"
["glibc-standard"]="glibc bash coreutils util-linux systemd"
["busybox-tiny"]="busybox-static"
)
# Image definitions
declare -A IMAGES=(
["volt/server"]="server.yaml"
["volt/server-db-postgres"]="server-db-postgres.yaml"
["volt/dev"]="dev.yaml"
["volt/desktop-minimal"]="desktop-minimal.yaml"
["volt/desktop-productivity"]="desktop-productivity.yaml"
["volt/edge"]="edge.yaml"
["volt/k8s-node"]="k8s-node.yaml"
)
build_rootfs() {
local image_name="$1"
local config_file="$2"
local rootfs_dir="$3"
log "Building rootfs for: $image_name"
# Create directory structure
mkdir -p "$rootfs_dir"/{bin,sbin,usr/{bin,sbin,lib},lib,lib64,etc,var,tmp,proc,sys,dev,run,home,root,app}
# Parse YAML config (simplified - in production use proper YAML parser)
local userland=$(grep "userland:" "$config_file" 2>/dev/null | awk '{print $2}' || echo "musl-minimal")
info " Userland: $userland"
# Install base userland
case "$userland" in
musl-minimal)
install_musl_minimal "$rootfs_dir"
;;
glibc-standard)
install_glibc_standard "$rootfs_dir"
;;
busybox-tiny)
install_busybox_tiny "$rootfs_dir"
;;
*)
warn "Unknown userland: $userland, using musl-minimal"
install_musl_minimal "$rootfs_dir"
;;
esac
# Create essential files
create_essential_files "$rootfs_dir"
# Set permissions
chmod 1777 "$rootfs_dir/tmp"
chmod 755 "$rootfs_dir"
}
install_musl_minimal() {
local rootfs="$1"
info " Installing musl-minimal userland..."
# Download and install BusyBox static binary
local busybox_url="https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox"
local busybox_path="$rootfs/bin/busybox"
if [[ ! -f "$CACHE_DIR/busybox" ]]; then
mkdir -p "$CACHE_DIR"
curl -fSL -o "$CACHE_DIR/busybox" "$busybox_url" || {
# Fallback: create minimal shell script
warn "Could not download busybox, creating minimal placeholder"
cat > "$busybox_path" << 'BUSYBOX'
#!/bin/sh
echo "Volt minimal shell"
exec /bin/sh "$@"
BUSYBOX
chmod +x "$busybox_path"
return 0
}
fi
cp "$CACHE_DIR/busybox" "$busybox_path"
chmod +x "$busybox_path"
# Create symlinks for common utilities
local utils="sh ash ls cat cp mv rm mkdir rmdir ln echo pwd env grep sed awk head tail sort uniq wc cut tr sleep date hostname uname id whoami ps kill"
for util in $utils; do
ln -sf busybox "$rootfs/bin/$util"
done
# Create sbin links
local sbin_utils="init halt reboot poweroff mount umount ifconfig route"
for util in $sbin_utils; do
ln -sf ../bin/busybox "$rootfs/sbin/$util"
done
}
install_glibc_standard() {
local rootfs="$1"
info " Installing glibc-standard userland..."
# For now, use Alpine as a base (it's actually musl but good enough for development)
# In production, this would pull from ArmoredGateHub registry
# Create minimal glibc-like structure
install_musl_minimal "$rootfs"
# Add bash if available
if command -v bash &>/dev/null; then
cp "$(command -v bash)" "$rootfs/bin/bash" 2>/dev/null || true
fi
# Copy essential libraries from host (for development only)
# In production, these come from TinyVol images
for lib in /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libpthread.so.0; do
if [[ -f "$lib" ]]; then
mkdir -p "$rootfs/lib/x86_64-linux-gnu"
cp "$lib" "$rootfs/lib/x86_64-linux-gnu/" 2>/dev/null || true
fi
done
# Copy ld-linux
if [[ -f /lib64/ld-linux-x86-64.so.2 ]]; then
mkdir -p "$rootfs/lib64"
cp /lib64/ld-linux-x86-64.so.2 "$rootfs/lib64/" 2>/dev/null || true
fi
}
install_busybox_tiny() {
local rootfs="$1"
info " Installing busybox-tiny userland..."
# Absolute minimal - just busybox
install_musl_minimal "$rootfs"
# Remove non-essential symlinks
rm -f "$rootfs/bin/awk" "$rootfs/bin/sed" "$rootfs/bin/grep"
}
create_essential_files() {
local rootfs="$1"
# /etc/passwd
cat > "$rootfs/etc/passwd" << 'EOF'
root:x:0:0:root:/root:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin
volt:x:1000:1000:Volt User:/home/volt:/bin/sh
EOF
# /etc/group
cat > "$rootfs/etc/group" << 'EOF'
root:x:0:
nobody:x:65534:
volt:x:1000:
EOF
# /etc/shadow (empty passwords - VMs use keys)
cat > "$rootfs/etc/shadow" << 'EOF'
root:*:19000:0:99999:7:::
nobody:*:19000:0:99999:7:::
volt:*:19000:0:99999:7:::
EOF
chmod 640 "$rootfs/etc/shadow"
# /etc/hosts
cat > "$rootfs/etc/hosts" << 'EOF'
127.0.0.1 localhost
::1 localhost ip6-localhost ip6-loopback
EOF
# /etc/hostname
echo "volt" > "$rootfs/etc/hostname"
# /etc/resolv.conf
cat > "$rootfs/etc/resolv.conf" << 'EOF'
nameserver 8.8.8.8
nameserver 8.8.4.4
EOF
# /etc/nsswitch.conf
cat > "$rootfs/etc/nsswitch.conf" << 'EOF'
passwd: files
group: files
shadow: files
hosts: files dns
networks: files
protocols: files
services: files
EOF
# /etc/os-release
cat > "$rootfs/etc/os-release" << 'EOF'
NAME="Volt Platform"
VERSION="1.0"
ID=volt
ID_LIKE=alpine
VERSION_ID=1.0
PRETTY_NAME="Volt Platform VM"
HOME_URL="https://voltvisor.io"
EOF
# Init script
cat > "$rootfs/sbin/init" << 'INIT'
#!/bin/sh
# Volt Init
# Mount essential filesystems
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
mkdir -p /dev/pts /dev/shm
mount -t devpts devpts /dev/pts
mount -t tmpfs tmpfs /dev/shm
mount -t tmpfs tmpfs /tmp
mount -t tmpfs tmpfs /run
# Set hostname
hostname -F /etc/hostname
# Network (if configured)
if [ -f /etc/network/interfaces ]; then
ifconfig lo up
fi
# Run init scripts
for script in /etc/init.d/S*; do
[ -x "$script" ] && "$script" start
done
# Start shell or configured service
if [ -f /etc/volt/service ]; then
exec $(cat /etc/volt/service)
else
exec /bin/sh
fi
INIT
chmod +x "$rootfs/sbin/init"
# Create init.d directory
mkdir -p "$rootfs/etc/init.d"
mkdir -p "$rootfs/etc/volt"
}
create_tinyvol() {
local image_name="$1"
local rootfs_dir="$2"
local output_path="$3"
log "Creating TinyVol: $output_path"
# Create squashfs image (TinyVol format)
# In production, this would use the actual TinyVol format
if command -v mksquashfs &>/dev/null; then
mksquashfs "$rootfs_dir" "$output_path" \
-comp zstd \
-Xcompression-level 19 \
-all-root \
-noappend \
-no-progress
else
# Fallback: create tar archive
warn "mksquashfs not found, creating tar archive"
tar -czf "$output_path" -C "$rootfs_dir" .
fi
local size=$(du -h "$output_path" | cut -f1)
info " Image size: $size"
}
generate_sbom() {
local image_name="$1"
local rootfs_dir="$2"
local output_path="$3"
log "Generating SBOM for: $image_name"
cat > "$output_path" << EOF
{
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"version": 1,
"metadata": {
"timestamp": "$(date -Iseconds)",
"component": {
"type": "operating-system",
"name": "$image_name",
"version": "1.0"
}
},
"components": [
{
"type": "application",
"name": "busybox",
"version": "1.35.0"
}
]
}
EOF
}
sign_image() {
local image_name="$1"
local image_path="$2"
log "Signing image: $image_name"
# Generate checksums
sha256sum "$image_path" > "${image_path}.sha256"
# TODO: Integrate with ArmoredForge signing
# armored-forge sign "$image_path" --key volt-image-key
}
build_image() {
local image_name="$1"
local config_file="$CONFIG_DIR/$2"
log "=========================================="
log "Building image: $image_name"
log "=========================================="
if [[ ! -f "$config_file" ]]; then
warn "Config file not found: $config_file"
# Create default config
config_file="$CONFIG_DIR/server.yaml"
fi
local safe_name=$(echo "$image_name" | tr '/' '_')
local work_dir="$OUTPUT_DIR/.build/$safe_name"
local rootfs_dir="$work_dir/rootfs"
local image_path="$OUTPUT_DIR/$safe_name.tinyvol"
# Clean and create work directory
rm -rf "$work_dir"
mkdir -p "$work_dir" "$rootfs_dir"
# Build rootfs
build_rootfs "$image_name" "$config_file" "$rootfs_dir"
# Create TinyVol image
create_tinyvol "$image_name" "$rootfs_dir" "$image_path"
# Generate SBOM
generate_sbom "$image_name" "$rootfs_dir" "${image_path}.sbom.json"
# Sign image
sign_image "$image_name" "$image_path"
# Create image metadata
cat > "${image_path}.json" << EOF
{
"name": "$image_name",
"version": "1.0",
"created": "$(date -Iseconds)",
"size": "$(du -h "$image_path" | cut -f1)",
"sha256": "$(sha256sum "$image_path" | cut -d' ' -f1)",
"sbom": "${image_path}.sbom.json"
}
EOF
# Cleanup work directory
rm -rf "$work_dir"
log "Image built: $image_path"
}
main() {
log "Volt Platform Image Builder"
log "=============================="
mkdir -p "$OUTPUT_DIR" "$CACHE_DIR"
# Build all defined images
for image_name in "${!IMAGES[@]}"; do
build_image "$image_name" "${IMAGES[$image_name]}"
done
# If no images defined, build defaults
if [[ ${#IMAGES[@]} -eq 0 ]]; then
build_image "volt/server" "server.yaml"
build_image "volt/desktop-productivity" "desktop-productivity.yaml"
fi
log ""
log "Build complete!"
log "Images installed to: $OUTPUT_DIR"
ls -la "$OUTPUT_DIR"/*.tinyvol 2>/dev/null || ls -la "$OUTPUT_DIR"
}
# Run if executed directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi