Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,17 @@ cloud-hypervisor/**
lib/system/exec_agent/exec-agent
lib/system/guest_agent/guest-agent
lib/system/init/init
lib/hypervisor/vz/vz-shim/vz-shim

# Envoy binaries
lib/ingress/binaries/**
dist/**

# UTM VM - downloaded ISO files
scripts/utm/images/

# IDE and editor
.cursor/

# Build artifacts
api
62 changes: 53 additions & 9 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
SHELL := /bin/bash
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build test install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries build-caddy-binaries build-caddy ensure-caddy-binaries release-prep clean build-embedded
.PHONY: oapi-generate generate-vmm-client generate-wire generate-all dev build build-linux build-darwin test test-linux test-darwin install-tools gen-jwt download-ch-binaries download-ch-spec ensure-ch-binaries build-caddy-binaries build-caddy ensure-caddy-binaries release-prep clean build-embedded

# Directory where local binaries will be installed
BIN_DIR ?= $(CURDIR)/bin
Expand Down Expand Up @@ -174,33 +174,57 @@ ensure-caddy-binaries:
fi

# Build guest-agent (guest binary) into its own directory for embedding
# Cross-compile for Linux since it runs inside the VM
lib/system/guest_agent/guest-agent: lib/system/guest_agent/*.go
@echo "Building guest-agent..."
cd lib/system/guest_agent && CGO_ENABLED=0 go build -ldflags="-s -w" -o guest-agent .
@echo "Building guest-agent for Linux..."
cd lib/system/guest_agent && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o guest-agent .

# Build init binary (runs as PID 1 in guest VM) for embedding
# Cross-compile for Linux since it runs inside the VM
lib/system/init/init: lib/system/init/*.go
@echo "Building init binary..."
cd lib/system/init && CGO_ENABLED=0 go build -ldflags="-s -w" -o init .
@echo "Building init binary for Linux..."
cd lib/system/init && CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o init .

build-embedded: lib/system/guest_agent/guest-agent lib/system/init/init

# Build the binary
build: ensure-ch-binaries ensure-caddy-binaries build-embedded | $(BIN_DIR)
build:
ifeq ($(shell uname -s),Darwin)
$(MAKE) build-darwin
else
$(MAKE) build-linux
endif

build-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded | $(BIN_DIR)
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api

# Build for macOS (no CH/Caddy needed; guest binaries cross-compiled for Linux)
build-darwin: build-embedded | $(BIN_DIR)
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api

# Build all binaries
build-all: build

# Run in development mode with hot reload
dev: ensure-ch-binaries ensure-caddy-binaries build-embedded $(AIR)
dev: dev-linux

# Linux development mode with hot reload
dev-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded $(AIR)
@rm -f ./tmp/main
$(AIR) -c .air.toml

# Run tests (as root for network capabilities, enables caching and parallelism)
# Run tests
# Usage: make test - runs all tests
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
test: ensure-ch-binaries ensure-caddy-binaries build-embedded
test:
ifeq ($(shell uname -s),Darwin)
$(MAKE) test-darwin
else
$(MAKE) test-linux
endif

# Linux tests (as root for network capabilities)
test-linux: ensure-ch-binaries ensure-caddy-binaries build-embedded
@VERBOSE_FLAG=""; \
if [ -n "$(VERBOSE)" ]; then VERBOSE_FLAG="-v"; fi; \
if [ -n "$(TEST)" ]; then \
Expand All @@ -210,6 +234,24 @@ test: ensure-ch-binaries ensure-caddy-binaries build-embedded
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=180s ./...; \
fi

# macOS tests (no sudo needed, adds e2fsprogs to PATH)
# Uses 'go list' to discover compilable packages, then filters out packages
# whose test files reference Linux-only symbols (network, devices, system/init).
DARWIN_EXCLUDE_PKGS := /lib/network|/lib/devices|/lib/system/init
test-darwin: build-embedded
@VERBOSE_FLAG=""; \
if [ -n "$(VERBOSE)" ]; then VERBOSE_FLAG="-v"; fi; \
PKGS=$$(PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
go list -tags containers_image_openpgp ./... 2>/dev/null | grep -Ev '$(DARWIN_EXCLUDE_PKGS)'); \
if [ -n "$(TEST)" ]; then \
echo "Running specific test: $(TEST)"; \
PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
go test -tags containers_image_openpgp -run=$(TEST) $$VERBOSE_FLAG -timeout=180s $$PKGS; \
else \
PATH="/opt/homebrew/opt/e2fsprogs/sbin:$(PATH)" \
go test -tags containers_image_openpgp $$VERBOSE_FLAG -timeout=180s $$PKGS; \
fi

# Generate JWT token for testing
# Usage: make gen-jwt [USER_ID=test-user]
gen-jwt: $(GODOTENV)
Expand All @@ -233,8 +275,10 @@ clean:
rm -rf lib/ingress/binaries/
rm -f lib/system/guest_agent/guest-agent
rm -f lib/system/init/init
rm -f lib/hypervisor/vz/vz-shim/vz-shim

# Prepare for release build (called by GoReleaser)
# Downloads all embedded binaries and builds embedded components
release-prep: download-ch-binaries build-caddy-binaries build-embedded
go mod tidy

2 changes: 2 additions & 0 deletions cmd/api/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ type Config struct {
RegistryCACertFile string // Path to CA certificate file for registry TLS verification
BuildTimeout int // Default build timeout in seconds
BuildSecretsDir string // Directory containing build secrets (optional)
DockerSocket string // Path to Docker socket (for building builder image)

// Hypervisor configuration
DefaultHypervisor string // Default hypervisor type: "cloud-hypervisor" or "qemu"
Expand Down Expand Up @@ -213,6 +214,7 @@ func Load() *Config {
RegistryCACertFile: getEnv("REGISTRY_CA_CERT_FILE", ""), // Path to CA cert for registry TLS
BuildTimeout: getEnvInt("BUILD_TIMEOUT", 600),
BuildSecretsDir: getEnv("BUILD_SECRETS_DIR", ""), // Optional: path to directory with build secrets
DockerSocket: getEnv("DOCKER_SOCKET", "/var/run/docker.sock"),

// Hypervisor configuration
DefaultHypervisor: getEnv("DEFAULT_HYPERVISOR", "cloud-hypervisor"),
Expand Down
31 changes: 31 additions & 0 deletions cmd/api/hypervisor_check_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//go:build darwin

package main

import (
"fmt"
"runtime"

"github.com/Code-Hex/vz/v3"
)

// checkHypervisorAccess verifies Virtualization.framework is available on macOS
func checkHypervisorAccess() error {
if runtime.GOARCH != "arm64" {
return fmt.Errorf("Virtualization.framework on macOS requires Apple Silicon (arm64), got %s", runtime.GOARCH)
}

// Validate virtualization is usable by attempting to get max CPU count
// This will fail if entitlements are missing or virtualization is not available
maxCPU := vz.VirtualMachineConfigurationMaximumAllowedCPUCount()
if maxCPU < 1 {
return fmt.Errorf("Virtualization.framework reports 0 max CPUs - check entitlements")
}

return nil
}

// hypervisorAccessCheckName returns the name of the hypervisor access check for logging
func hypervisorAccessCheckName() string {
return "Virtualization.framework"
}
29 changes: 29 additions & 0 deletions cmd/api/hypervisor_check_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//go:build linux

package main

import (
"fmt"
"os"
)

// checkHypervisorAccess verifies KVM is available and the user has permission to use it
func checkHypervisorAccess() error {
f, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("/dev/kvm not found - KVM not enabled or not supported")
}
if os.IsPermission(err) {
return fmt.Errorf("permission denied accessing /dev/kvm - user not in 'kvm' group")
}
return fmt.Errorf("cannot access /dev/kvm: %w", err)
}
f.Close()
return nil
}

// hypervisorAccessCheckName returns the name of the hypervisor access check for logging
func hypervisorAccessCheckName() string {
return "KVM"
}
23 changes: 4 additions & 19 deletions cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,11 +130,11 @@ func run() error {
logger.Warn("JWT_SECRET not configured - API authentication will fail")
}

// Verify KVM access (required for VM creation)
if err := checkKVMAccess(); err != nil {
return fmt.Errorf("KVM access check failed: %w\n\nEnsure:\n 1. KVM is enabled (check /dev/kvm exists)\n 2. User is in 'kvm' group: sudo usermod -aG kvm $USER\n 3. Log out and back in, or use: newgrp kvm", err)
// Verify hypervisor access (KVM on Linux, Virtualization.framework on macOS)
if err := checkHypervisorAccess(); err != nil {
return fmt.Errorf("hypervisor access check failed: %w", err)
}
logger.Info("KVM access verified")
logger.Info("Hypervisor access verified", "type", hypervisorAccessCheckName())

// Check if QEMU is available (optional - only warn if not present)
if _, err := (&qemu.Starter{}).GetBinaryPath(nil, ""); err != nil {
Expand Down Expand Up @@ -465,18 +465,3 @@ func run() error {
return err
}

// checkKVMAccess verifies KVM is available and the user has permission to use it
func checkKVMAccess() error {
f, err := os.OpenFile("/dev/kvm", os.O_RDWR, 0)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("/dev/kvm not found - KVM not enabled or not supported")
}
if os.IsPermission(err) {
return fmt.Errorf("permission denied accessing /dev/kvm - user not in 'kvm' group")
}
return fmt.Errorf("cannot access /dev/kvm: %w", err)
}
f.Close()
return nil
}
Loading