Skip to content

Commit 279c12b

Browse files
committed
update port expose docker style
1 parent b5f2c4a commit 279c12b

File tree

12 files changed

+707
-17
lines changed

12 files changed

+707
-17
lines changed

docs/3_guides/1_networking.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,94 @@ For bridged mode, the orchestrator computes:
7171
- netmask derived from subnet mask (formatNetmask)
7272

7373
This is passed to the runtime; the guest should configure eth0 accordingly on boot.
74+
75+
## Port Mapping (Docker-Style)
76+
77+
Volant supports Docker-style port mapping to expose VM services to the host. Port exposure works in two phases:
78+
79+
### 1. Image Manifest (Declaration)
80+
81+
Images declare what ports the application listens on inside the VM using the `network.expose` array in `manifest.json`:
82+
83+
```toml
84+
[[network.expose]]
85+
port = 3000
86+
protocol = "tcp"
87+
88+
[[network.expose]]
89+
port = 8080
90+
protocol = "tcp"
91+
host_port = 9000 # Optional: explicit host port mapping
92+
```
93+
94+
This is similar to Docker's `EXPOSE` directive - it's documentation about what ports the app uses, but doesn't actually map them.
95+
96+
### 2. VM Creation (Runtime Mapping)
97+
98+
At VM creation time, you can:
99+
100+
**Auto-assign host ports** (default behavior):
101+
```bash
102+
volar vms create myvm --image myapp
103+
# Manifest ports get auto-assigned: 3000→2234, 8080→2235
104+
```
105+
106+
**Explicitly map host ports** using `--expose`:
107+
```bash
108+
# Docker-style syntax: [HOST_PORT:]CONTAINER_PORT[:PROTOCOL]
109+
volar vms create myvm --image myapp --expose 8080:3000:tcp --expose 9090:8080:tcp
110+
```
111+
112+
**Disable all port exposure** (secure by default):
113+
```bash
114+
volar vms create myvm --image myapp --no-expose
115+
```
116+
117+
### Host Port Auto-Allocation
118+
119+
When ports are not explicitly mapped:
120+
- Host ports are auto-assigned sequentially starting from **2234**
121+
- Volant tracks all allocated ports across VMs to prevent conflicts
122+
- If an explicit port is already in use, VM creation will fail
123+
124+
### Integration with driftd
125+
126+
Port mappings are automatically programmed into driftd's eBPF TC dataplane:
127+
- NAT rules forward `host_ip:host_port``vm_ip:vm_port`
128+
- Stateful connection tracking for TCP and UDP
129+
- Routes persist across VM restarts
130+
- No manual configuration required
131+
132+
### Examples
133+
134+
**Web application with auto-assigned ports:**
135+
```toml
136+
# manifest.toml
137+
[[network.expose]]
138+
port = 80
139+
protocol = "tcp"
140+
```
141+
```bash
142+
volar vms create web --image nginx
143+
# Access via http://host_ip:2234 (auto-assigned)
144+
```
145+
146+
**Database with explicit port:**
147+
```bash
148+
volar vms create postgres --image postgres:alpine --expose 5432:5432:tcp
149+
# Access via postgresql://host_ip:5432
150+
```
151+
152+
**Multi-port service:**
153+
```bash
154+
volar vms create app --image myapp \
155+
--expose 8080:3000:tcp \
156+
--expose 8081:3001:tcp \
157+
--expose 9000:9000:udp
158+
```
159+
160+
**Secure deployment (no external ports):**
161+
```bash
162+
volar vms create internal-service --image worker --no-expose
163+
# Only accessible via vsock or internal VM network
164+
```

docs/6_reference/1_manifest-schema.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Optional fields:
3030
- image, image_digest (for OCI lineage)
3131
- disks[]: { name, source, format?: raw|qcow2, checksum?, readonly, target? }
3232
- cloud_init: { datasource, seed_mode (default vfat), user_data/meta_data/network_config }
33-
- network: { mode: vsock|bridged|dhcp, subnet?, gateway?, auto_assign? }
33+
- network: { mode: vsock|bridged|dhcp, subnet?, gateway?, auto_assign?, expose?: [{port, protocol?, host_port?}, ...] }
3434
- devices: { pci_passthrough?: ["0000:01:00.0"...], allowlist?: ["vendor:device" or "vendor:*"] }
3535
- actions: map<string, { description?, method, path, timeout_ms? }>
3636
- health_check: { endpoint, timeout_ms }

docs/6_reference/3_manifest-toml.md

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,16 @@ Network modes:
135135
- **vsock**: No Ethernet, vsock-only communication
136136
- **dhcp**: Tap device provided, VM runs DHCP client
137137

138-
Port exposures:
139-
- Defines which ports the workload listens on
140-
- Can be completely replaced with `--port` flags at VM creation
138+
Port exposures (Docker-style):
139+
- **Declaration**: Defines which ports the workload listens on inside the VM
140+
- **Like Docker EXPOSE**: Documents what ports the app uses, but doesn't actually map them
141+
- **Runtime mapping**: At VM creation, use `--expose` to map host ports
142+
- Auto-assigned: `--expose 3000` → host port 2234 (auto)
143+
- Explicit: `--expose 8080:3000:tcp` → host:8080 → vm:3000
144+
- Disable all: `--no-expose` → no ports mapped (secure by default)
145+
- **host_port field**: Optional static host port in manifest (rare, usually auto-assigned)
146+
- **Auto-allocation**: Host ports start from 2234 and increment, tracked across all VMs
147+
- **Integration**: Automatically programmed into driftd's eBPF TC NAT dataplane
141148

142149
### Actions
143150

docs/6_reference/cli-volar.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Source: internal/cli/standard.
2121
- --runtime <type>
2222
- --cpu <n>
2323
- --memory <mb>
24+
- --env KEY=VALUE (repeatable) — set environment variables
25+
- --expose [HOST_PORT:]CONTAINER_PORT[:PROTOCOL] (repeatable) — Docker-style port mapping
26+
- --no-expose — disable all port exposure (secure by default)
2427
- --kernel-cmdline <extra>
2528
- --config <path to JSON>
2629
- --api-host <host> / --api-port <port>

internal/cli/standard/vms.go

Lines changed: 96 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,14 @@ func newVMsCreateCmd() *cobra.Command {
288288
if err != nil {
289289
return err
290290
}
291+
exposeFlags, err := cmd.Flags().GetStringSlice("expose")
292+
if err != nil {
293+
return err
294+
}
295+
noExposeFlag, err := cmd.Flags().GetBool("no-expose")
296+
if err != nil {
297+
return err
298+
}
291299

292300
// Build Overrides struct
293301
var overrides vmconfig.Overrides
@@ -311,7 +319,7 @@ func newVMsCreateCmd() *cobra.Command {
311319
}
312320
}
313321

314-
// Parse port exposures from PORT or HOST_PORT:CONTAINER_PORT format
322+
// Parse port exposures from PORT or HOST_PORT:CONTAINER_PORT format (legacy flag)
315323
if len(portFlags) > 0 {
316324
overrides.ExposePorts = make([]vmconfig.Expose, 0, len(portFlags))
317325
for _, p := range portFlags {
@@ -341,6 +349,90 @@ func newVMsCreateCmd() *cobra.Command {
341349
}
342350
}
343351

352+
// Parse Docker-style port exposures: [HOST_PORT:]CONTAINER_PORT[:PROTOCOL]
353+
// Examples: 8080:3000:tcp, 8080:3000, 3000:tcp, 3000
354+
if len(exposeFlags) > 0 {
355+
if len(portFlags) > 0 {
356+
return fmt.Errorf("cannot use both --port and --expose flags together")
357+
}
358+
overrides.ExposePorts = make([]vmconfig.Expose, 0, len(exposeFlags))
359+
for _, e := range exposeFlags {
360+
var expose vmconfig.Expose
361+
expose.Protocol = "tcp" // default protocol
362+
363+
parts := strings.Split(e, ":")
364+
switch len(parts) {
365+
case 1:
366+
// Format: CONTAINER_PORT (auto-assign host port)
367+
containerPort, err := strconv.Atoi(parts[0])
368+
if err != nil {
369+
return fmt.Errorf("invalid port in %q: %w", e, err)
370+
}
371+
expose.Port = containerPort
372+
expose.HostPort = 0 // Auto-assign
373+
374+
case 2:
375+
// Could be: HOST_PORT:CONTAINER_PORT or CONTAINER_PORT:PROTOCOL
376+
// Try to parse second part as protocol first
377+
protocol := strings.ToLower(parts[1])
378+
if protocol == "tcp" || protocol == "udp" {
379+
// Format: CONTAINER_PORT:PROTOCOL
380+
containerPort, err := strconv.Atoi(parts[0])
381+
if err != nil {
382+
return fmt.Errorf("invalid port in %q: %w", e, err)
383+
}
384+
expose.Port = containerPort
385+
expose.Protocol = protocol
386+
expose.HostPort = 0 // Auto-assign
387+
} else {
388+
// Format: HOST_PORT:CONTAINER_PORT
389+
hostPort, err := strconv.Atoi(parts[0])
390+
if err != nil {
391+
return fmt.Errorf("invalid host port in %q: %w", e, err)
392+
}
393+
containerPort, err := strconv.Atoi(parts[1])
394+
if err != nil {
395+
return fmt.Errorf("invalid container port in %q: %w", e, err)
396+
}
397+
expose.HostPort = hostPort
398+
expose.Port = containerPort
399+
}
400+
401+
case 3:
402+
// Format: HOST_PORT:CONTAINER_PORT:PROTOCOL
403+
hostPort, err := strconv.Atoi(parts[0])
404+
if err != nil {
405+
return fmt.Errorf("invalid host port in %q: %w", e, err)
406+
}
407+
containerPort, err := strconv.Atoi(parts[1])
408+
if err != nil {
409+
return fmt.Errorf("invalid container port in %q: %w", e, err)
410+
}
411+
protocol := strings.ToLower(parts[2])
412+
if protocol != "tcp" && protocol != "udp" {
413+
return fmt.Errorf("invalid protocol in %q: must be tcp or udp", e)
414+
}
415+
expose.HostPort = hostPort
416+
expose.Port = containerPort
417+
expose.Protocol = protocol
418+
419+
default:
420+
return fmt.Errorf("invalid expose format %q: expected [HOST_PORT:]CONTAINER_PORT[:PROTOCOL]", e)
421+
}
422+
423+
overrides.ExposePorts = append(overrides.ExposePorts, expose)
424+
}
425+
}
426+
427+
// Handle --no-expose flag: explicitly disable all port exposure (secure by default)
428+
if noExposeFlag {
429+
if len(portFlags) > 0 || len(exposeFlags) > 0 {
430+
return fmt.Errorf("cannot use --no-expose with --port or --expose flags")
431+
}
432+
// Set to empty slice to override manifest ports
433+
overrides.ExposePorts = []vmconfig.Expose{}
434+
}
435+
344436
req := client.CreateVMRequest{
345437
Name: args[0],
346438
Image: imageName,
@@ -439,7 +531,9 @@ func newVMsCreateCmd() *cobra.Command {
439531
cmd.Flags().Int("cpu", 0, "Number of virtual CPU cores (overrides manifest default)")
440532
cmd.Flags().Int("memory", 0, "Memory in MB (overrides manifest default)")
441533
cmd.Flags().StringSlice("env", nil, "Environment variables in KEY=VALUE format (repeatable, overrides manifest defaults)")
442-
cmd.Flags().StringSlice("port", nil, "Expose ports in format PORT or HOST_PORT:CONTAINER_PORT (repeatable)")
534+
cmd.Flags().StringSlice("port", nil, "Expose ports in format PORT or HOST_PORT:CONTAINER_PORT (repeatable, legacy)")
535+
cmd.Flags().StringSlice("expose", nil, "Expose ports in Docker format: [HOST_PORT:]CONTAINER_PORT[:PROTOCOL] (repeatable)")
536+
cmd.Flags().Bool("no-expose", false, "Disable all port exposure (overrides manifest defaults for secure-by-default)")
443537
cmd.Flags().String("kernel-cmdline", "", "Additional kernel cmdline parameters")
444538
cmd.Flags().String("kernel", "", "Override kernel image path (vmlinux)")
445539
cmd.Flags().String("initramfs", "", "Override initramfs image path (.cpio.gz)")

0 commit comments

Comments
 (0)