Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
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
4 changes: 2 additions & 2 deletions .air.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ tmp_dir = "tmp"
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html", "yaml"]
include_file = []
kill_delay = "0s"
kill_delay = "5s"
log = "build-errors.log"
poll = false
poll_interval = 0
post_cmd = []
rerun = false
rerun_delay = 500
send_interrupt = false
send_interrupt = true
stop_on_error = false

[color]
Expand Down
78 changes: 77 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,78 @@
# Required
JWT_SECRET='your-secret-key-here'
DATA_DIR=/home/your-user/hypeman/.datadir # or leave unset to default to /var/lib/hypeman

# Data directory (default: /var/lib/hypeman)
DATA_DIR=/var/lib/hypeman

# Server configuration
# PORT=8080

# Network configuration
# BRIDGE_NAME=vmbr0
# SUBNET_CIDR=10.100.0.0/16
# SUBNET_GATEWAY= # empty = derived from SUBNET_CIDR
# UPLINK_INTERFACE= # empty = auto-detect from default route
# DNS_SERVER=1.1.1.1

# Logging
# LOG_LEVEL=info # debug, info, warn, error

# Caddy / Ingress configuration
# CADDY_LISTEN_ADDRESS=0.0.0.0
# CADDY_ADMIN_ADDRESS=127.0.0.1
# CADDY_ADMIN_PORT=0 # 0 = random port (prevents conflicts on shared dev machines)
# CADDY_STOP_ON_SHUTDOWN=false # Set to true if you want Caddy to stop when hypeman stops

# =============================================================================
# TLS / ACME Configuration (for HTTPS ingresses)
# =============================================================================
# Required for TLS ingresses:
# ACME_EMAIL=admin@example.com
# ACME_DNS_PROVIDER=cloudflare

# IMPORTANT: You must specify which domains are allowed for TLS certificates.
# This prevents typos and ensures you only request certificates for domains you control.
# TLS_ALLOWED_DOMAINS=*.example.com,api.other.com
# Supports:
# - Exact matches: api.example.com
# - Wildcard subdomains: *.example.com (matches foo.example.com, NOT foo.bar.example.com)
# If not set, no TLS ingresses are allowed.

# Optional ACME settings:
# ACME_CA= # empty = Let's Encrypt production
# Use https://acme-staging-v02.api.letsencrypt.org/directory for testing

# DNS propagation settings (applies to all providers):
# DNS_PROPAGATION_TIMEOUT=2m # Max time to wait for DNS propagation
# DNS_RESOLVERS=1.1.1.1,8.8.8.8 # Custom DNS resolvers for propagation checking

# -----------------------------------------------------------------------------
# Cloudflare DNS Provider (ACME_DNS_PROVIDER=cloudflare)
# -----------------------------------------------------------------------------
# CLOUDFLARE_API_TOKEN=your-api-token
# Token needs Zone:DNS:Edit permissions for the domains you want certificates for
# =============================================================================
# OpenTelemetry Configuration
# =============================================================================
# OTEL_ENABLED=false
# OTEL_ENDPOINT=127.0.0.1:4317
# OTEL_SERVICE_NAME=hypeman
# OTEL_SERVICE_INSTANCE_ID= # default: hostname
# OTEL_INSECURE=true
# ENV=dev # deployment environment

# =============================================================================
# Resource Limits
# =============================================================================
# Per-instance limits
# MAX_VCPUS_PER_INSTANCE=16
# MAX_MEMORY_PER_INSTANCE=32GB

# Aggregate limits (0 or empty = unlimited)
# MAX_TOTAL_VCPUS=0
# MAX_TOTAL_MEMORY=
# MAX_TOTAL_VOLUME_STORAGE=

# Other limits
# MAX_CONCURRENT_BUILDS=1
# MAX_OVERLAY_SIZE=100GB
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,14 @@ jobs:
run: make build

- name: Run tests
env:
# Docker auth for tests running as root (sudo)
DOCKER_CONFIG: /home/debianuser/.docker
# TLS/ACME testing (optional - tests will skip if not configured)
ACME_EMAIL: ${{ secrets.ACME_EMAIL }}
ACME_DNS_PROVIDER: "cloudflare"
ACME_CA: "https://acme-staging-v02.api.letsencrypt.org/directory"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
TLS_TEST_DOMAIN: "test.hypeman-development.com"
TLS_ALLOWED_DOMAINS: '*.hypeman-development.com'
run: make test
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ cloud-hypervisor/**
lib/system/exec_agent/exec-agent

# Envoy binaries
lib/ingress/binaries/envoy/*/*/envoy
lib/ingress/binaries/**
dist/**
122 changes: 68 additions & 54 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 download-envoy-binaries ensure-envoy-binaries release-prep
.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

# Directory where local binaries will be installed
BIN_DIR ?= $(CURDIR)/bin
Expand All @@ -12,6 +12,7 @@ OAPI_CODEGEN ?= $(BIN_DIR)/oapi-codegen
AIR ?= $(BIN_DIR)/air
WIRE ?= $(BIN_DIR)/wire
GODOTENV ?= $(BIN_DIR)/godotenv
XCADDY ?= $(BIN_DIR)/xcaddy

# Install oapi-codegen
$(OAPI_CODEGEN): | $(BIN_DIR)
Expand All @@ -29,7 +30,11 @@ $(WIRE): | $(BIN_DIR)
$(GODOTENV): | $(BIN_DIR)
GOBIN=$(BIN_DIR) go install github.com/joho/godotenv/cmd/godotenv@latest

install-tools: $(OAPI_CODEGEN) $(AIR) $(WIRE) $(GODOTENV)
# Install xcaddy for building Caddy with plugins
$(XCADDY): | $(BIN_DIR)
GOBIN=$(BIN_DIR) go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

install-tools: $(OAPI_CODEGEN) $(AIR) $(WIRE) $(GODOTENV) $(XCADDY)

# Download Cloud Hypervisor binaries
download-ch-binaries:
Expand All @@ -49,18 +54,46 @@ download-ch-binaries:
@chmod +x lib/vmm/binaries/cloud-hypervisor/v*/*/cloud-hypervisor
@echo "Binaries downloaded successfully"

# Download Envoy binaries
download-envoy-binaries:
@echo "Downloading Envoy binaries..."
@mkdir -p lib/ingress/binaries/envoy/v1.36/{x86_64,aarch64}
@echo "Downloading Envoy v1.36.3 for x86_64..."
@curl -L -o lib/ingress/binaries/envoy/v1.36/x86_64/envoy \
https://github.com/envoyproxy/envoy/releases/download/v1.36.3/envoy-1.36.3-linux-x86_64
@echo "Downloading Envoy v1.36.3 for aarch64..."
@curl -L -o lib/ingress/binaries/envoy/v1.36/aarch64/envoy \
https://github.com/envoyproxy/envoy/releases/download/v1.36.3/envoy-1.36.3-linux-aarch_64
@chmod +x lib/ingress/binaries/envoy/v1.36/*/envoy
@echo "Envoy binaries downloaded successfully"
# Caddy version and modules
CADDY_VERSION := v2.10.2
CADDY_DNS_MODULES := --with github.com/caddy-dns/cloudflare

# Build Caddy with DNS modules using xcaddy
# xcaddy builds Caddy from source with the specified modules
build-caddy-binaries: $(XCADDY)
@echo "Building Caddy $(CADDY_VERSION) with DNS modules..."
@mkdir -p lib/ingress/binaries/caddy/$(CADDY_VERSION)/x86_64
@mkdir -p lib/ingress/binaries/caddy/$(CADDY_VERSION)/aarch64
@echo "Building Caddy $(CADDY_VERSION) for x86_64..."
GOOS=linux GOARCH=amd64 $(XCADDY) build $(CADDY_VERSION) \
$(CADDY_DNS_MODULES) \
--output lib/ingress/binaries/caddy/$(CADDY_VERSION)/x86_64/caddy
@echo "Building Caddy $(CADDY_VERSION) for aarch64..."
GOOS=linux GOARCH=arm64 $(XCADDY) build $(CADDY_VERSION) \
$(CADDY_DNS_MODULES) \
--output lib/ingress/binaries/caddy/$(CADDY_VERSION)/aarch64/caddy
@chmod +x lib/ingress/binaries/caddy/$(CADDY_VERSION)/*/caddy
@echo "Caddy binaries built successfully with DNS modules"

# Build Caddy for current architecture only (faster for development)
build-caddy: $(XCADDY)
@echo "Building Caddy $(CADDY_VERSION) with DNS modules for current architecture..."
@ARCH=$$(uname -m); \
if [ "$$ARCH" = "x86_64" ]; then \
CADDY_ARCH=x86_64; \
GOARCH=amd64; \
elif [ "$$ARCH" = "aarch64" ] || [ "$$ARCH" = "arm64" ]; then \
CADDY_ARCH=aarch64; \
GOARCH=arm64; \
else \
echo "Unsupported architecture: $$ARCH"; exit 1; \
fi; \
mkdir -p lib/ingress/binaries/caddy/$(CADDY_VERSION)/$$CADDY_ARCH; \
GOOS=linux GOARCH=$$GOARCH $(XCADDY) build $(CADDY_VERSION) \
$(CADDY_DNS_MODULES) \
--output lib/ingress/binaries/caddy/$(CADDY_VERSION)/$$CADDY_ARCH/caddy; \
chmod +x lib/ingress/binaries/caddy/$(CADDY_VERSION)/$$CADDY_ARCH/caddy
@echo "Caddy binary built successfully"

# Download Cloud Hypervisor API spec
download-ch-spec:
Expand Down Expand Up @@ -107,12 +140,20 @@ ensure-ch-binaries:
$(MAKE) download-ch-binaries; \
fi

# Check if Envoy binaries exist, download if missing
.PHONY: ensure-envoy-binaries
ensure-envoy-binaries:
@if [ ! -f lib/ingress/binaries/envoy/v1.36/x86_64/envoy ]; then \
echo "Envoy binaries not found, downloading..."; \
$(MAKE) download-envoy-binaries; \
# Check if Caddy binaries exist, build if missing
.PHONY: ensure-caddy-binaries
ensure-caddy-binaries:
@ARCH=$$(uname -m); \
if [ "$$ARCH" = "x86_64" ]; then \
CADDY_ARCH=x86_64; \
elif [ "$$ARCH" = "aarch64" ] || [ "$$ARCH" = "arm64" ]; then \
CADDY_ARCH=aarch64; \
else \
echo "Unsupported architecture: $$ARCH"; exit 1; \
fi; \
if [ ! -f lib/ingress/binaries/caddy/$(CADDY_VERSION)/$$CADDY_ARCH/caddy ]; then \
echo "Caddy binary not found, building with xcaddy..."; \
$(MAKE) build-caddy; \
fi

# Build exec-agent (guest binary) into its own directory for embedding
Expand All @@ -121,7 +162,7 @@ lib/system/exec_agent/exec-agent: lib/system/exec_agent/main.go
cd lib/system/exec_agent && CGO_ENABLED=0 go build -ldflags="-s -w" -o exec-agent .

# Build the binary
build: ensure-ch-binaries ensure-envoy-binaries lib/system/exec_agent/exec-agent | $(BIN_DIR)
build: ensure-ch-binaries ensure-caddy-binaries lib/system/exec_agent/exec-agent | $(BIN_DIR)
go build -tags containers_image_openpgp -o $(BIN_DIR)/hypeman ./cmd/api

# Build exec CLI
Expand All @@ -132,45 +173,18 @@ build-exec: | $(BIN_DIR)
build-all: build build-exec

# Run in development mode with hot reload
dev: $(AIR)
dev: ensure-ch-binaries ensure-caddy-binaries lib/system/exec_agent/exec-agent $(AIR)
$(AIR) -c .air.toml

# Run tests
# Compile test binaries and grant network capabilities (runs as user, not root)
# Run tests (as root for network capabilities, enables caching and parallelism)
# Usage: make test - runs all tests
# make test TEST=TestCreateInstanceWithNetwork - runs specific test
test: ensure-ch-binaries ensure-envoy-binaries lib/system/exec_agent/exec-agent
@echo "Building test binaries..."
@mkdir -p $(BIN_DIR)/tests
@for pkg in $$(go list -tags containers_image_openpgp ./...); do \
pkg_name=$$(basename $$pkg); \
go test -c -tags containers_image_openpgp -o $(BIN_DIR)/tests/$$pkg_name.test $$pkg 2>/dev/null || true; \
done
@echo "Granting capabilities to test binaries..."
@for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
sudo setcap 'cap_net_admin,cap_net_bind_service=+eip' $$test 2>/dev/null || true; \
fi; \
done
@echo "Running tests as current user with capabilities..."
test: ensure-ch-binaries ensure-caddy-binaries lib/system/exec_agent/exec-agent
@if [ -n "$(TEST)" ]; then \
echo "Running specific test: $(TEST)"; \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Checking $$(basename $$test) for $(TEST)..."; \
$$test -test.run=$(TEST) -test.v -test.timeout=60s 2>&1 | grep -q "PASS\|FAIL" && \
$$test -test.run=$(TEST) -test.v -test.timeout=60s || true; \
fi; \
done; \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -run=$(TEST) -v -timeout=180s ./...; \
else \
for test in $(BIN_DIR)/tests/*.test; do \
if [ -f "$$test" ]; then \
echo ""; \
echo "Running $$(basename $$test)..."; \
$$test -test.v -test.parallel=10 -test.timeout=60s || exit 1; \
fi; \
done; \
sudo env "PATH=$$PATH" "DOCKER_CONFIG=$${DOCKER_CONFIG:-$$HOME/.docker}" go test -tags containers_image_openpgp -v -timeout=180s ./...; \
fi

# Generate JWT token for testing
Expand All @@ -189,5 +203,5 @@ clean:

# Prepare for release build (called by GoReleaser)
# Downloads all embedded binaries and builds embedded components
release-prep: download-ch-binaries download-envoy-binaries lib/system/exec_agent/exec-agent
release-prep: download-ch-binaries ensure-caddy-binaries lib/system/exec_agent/exec-agent
go mod tidy
54 changes: 47 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ getcap ./bin/hypeman

**File Descriptor Limits:**

Envoy (used for ingress) requires a higher file descriptor limit than the default on some systems (root defaults to 1024 on many systems). If you see "Too many open files" errors, increase the limit:
Caddy (used for ingress) requires a higher file descriptor limit than the default on some systems. If you see "Too many open files" errors, increase the limit:

```bash
# Check current limit (also check with: sudo bash -c 'ulimit -n')
Expand Down Expand Up @@ -98,11 +98,18 @@ Hypeman can be configured using the following environment variables:
| `OTEL_ENDPOINT` | OTLP gRPC endpoint | `127.0.0.1:4317` |
| `OTEL_SERVICE_INSTANCE_ID` | Instance ID for telemetry (differentiates multiple servers) | hostname |
| `LOG_LEVEL` | Default log level (debug, info, warn, error) | `info` |
| `LOG_LEVEL_<SUBSYSTEM>` | Per-subsystem log level (API, IMAGES, INSTANCES, NETWORK, VOLUMES, VMM, SYSTEM, EXEC) | inherits default |
| `ENVOY_LISTEN_ADDRESS` | Address for Envoy ingress listeners | `0.0.0.0` |
| `ENVOY_ADMIN_ADDRESS` | Address for Envoy admin API | `127.0.0.1` |
| `ENVOY_ADMIN_PORT` | Port for Envoy admin API | `9901` |
| `ENVOY_STOP_ON_SHUTDOWN` | Stop Envoy when hypeman shuts down (if false, Envoy continues running) | `false` |
| `LOG_LEVEL_<SUBSYSTEM>` | Per-subsystem log level (API, IMAGES, INSTANCES, NETWORK, VOLUMES, VMM, SYSTEM, EXEC, CADDY) | inherits default |
| `CADDY_LISTEN_ADDRESS` | Address for Caddy ingress listeners | `0.0.0.0` |
| `CADDY_ADMIN_ADDRESS` | Address for Caddy admin API | `127.0.0.1` |
| `CADDY_ADMIN_PORT` | Port for Caddy admin API | `2019` |
| `CADDY_STOP_ON_SHUTDOWN` | Stop Caddy when hypeman shuts down (set to `true` for dev) | `false` |
| `ACME_EMAIL` | Email for ACME certificate registration (required for TLS ingresses) | _(empty)_ |
| `ACME_DNS_PROVIDER` | DNS provider for ACME challenges: `cloudflare` | _(empty)_ |
| `ACME_CA` | ACME CA URL (empty = Let's Encrypt production) | _(empty)_ |
| `TLS_ALLOWED_DOMAINS` | Comma-separated allowed domains for TLS (e.g., `*.example.com,api.other.com`) | _(empty)_ |
| `DNS_PROPAGATION_TIMEOUT` | Max time to wait for DNS propagation (e.g., `2m`) | _(empty)_ |
| `DNS_RESOLVERS` | Comma-separated DNS resolvers for propagation checking | _(empty)_ |
| `CLOUDFLARE_API_TOKEN` | Cloudflare API token (when using `cloudflare` provider) | _(empty)_ |

**Important: Subnet Configuration**

Expand All @@ -111,7 +118,7 @@ The default subnet `10.100.0.0/16` is chosen to avoid common conflicts. Hypeman
If you need a different subnet, set `SUBNET_CIDR` in your environment. The gateway is automatically derived as the first IP in the subnet (e.g., `10.100.0.0/16` → `10.100.0.1`).

**Alternative subnets if needed:**
- `172.30.0.0/16` - Private range between common Docker (172.17.x.x) and AWS (172.31.x.x) ranges
- `172.30.0.0/16` - Private range between common Docker (172.17.x.x) and cloud provider (172.31.x.x) ranges
- `10.200.0.0/16` - Another private range option

**Example:**
Expand Down Expand Up @@ -144,6 +151,39 @@ ip route show
```
Pick the interface used by the default route (usually the line starting with `default`). Avoid using local bridges like `docker0`, `br-...`, `virbr0`, or `vmbr0` as the uplink; those are typically internal virtual networks, not your actual internet-facing interface.

**TLS Ingress (HTTPS)**

Hypeman uses Caddy with automatic ACME certificates for TLS termination. Certificates are issued via DNS-01 challenges (Cloudflare).

To enable TLS ingresses:

1. Configure ACME credentials in your `.env`:
```bash
# Required for any TLS ingress
ACME_EMAIL=admin@example.com

# For Cloudflare
ACME_DNS_PROVIDER=cloudflare
CLOUDFLARE_API_TOKEN=your-api-token
```

2. Create an ingress with TLS enabled:
```bash
curl -X POST http://localhost:8080/v1/ingresses \
-H "Content-Type: application/json" \
-d '{
"name": "my-https-app",
"rules": [{
"match": {"hostname": "app.example.com", "port": 443},
"target": {"instance": "my-instance", "port": 8080},
"tls": true,
"redirect_http": true
}]
}'
```

Certificates are stored in `$DATA_DIR/caddy/data/` and auto-renewed by Caddy.

**Setup:**

```bash
Expand Down
Loading
Loading