Skip to content

Commit 59dd46d

Browse files
committed
Merge remote-tracking branch 'origin/main' into codex/snapshot-schedule-retention
# Conflicts: # lib/oapi/oapi.go
2 parents 7e59da6 + f985341 commit 59dd46d

70 files changed

Lines changed: 4643 additions & 389 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@
1919

2020
---
2121

22+
## Features
23+
24+
- **Docker-compatible CLI**`run`, `exec`, `stop`, `ps`, `logs`, `pull` work like you'd expect
25+
- **Multiple hypervisors** — Cloud Hypervisor, Firecracker, QEMU on Linux; Virtualization.framework on macOS
26+
- **Standby & restore** — snapshot a VM to disk and resume it in milliseconds
27+
- **Built-in ingress** — reverse proxy with TLS termination and subdomain routing
28+
- **GPU passthrough** — vGPU and VFIO device support
29+
- **OCI image support** — pull and run standard container images
30+
- **Remote API** — JWT-authenticated server with a separate CLI client
31+
2232
## Requirements
2333

2434
### Linux

cmd/api/api/api_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ func ctxWithImage(svc *ApiService, name string) context.Context {
146146
// Returns the image name on success, or fails the test on error/timeout.
147147
func createAndWaitForImage(t *testing.T, svc *ApiService, imageName string, timeout time.Duration) string {
148148
t.Helper()
149+
imageName = apiTestImageRef(t, imageName)
149150

150151
t.Logf("Creating image %s...", imageName)
151152
imgResp, err := svc.CreateImage(ctx(), oapi.CreateImageRequestObject{

cmd/api/api/cp_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ func TestCpToAndFromInstance(t *testing.T) {
2828
}
2929

3030
svc := newTestService(t)
31+
imageName := "docker.io/library/nginx:alpine"
3132

3233
// Ensure system files (kernel and initrd) are available
3334
t.Log("Ensuring system files...")
@@ -37,19 +38,20 @@ func TestCpToAndFromInstance(t *testing.T) {
3738
t.Log("System files ready")
3839

3940
// Create and wait for nginx image (has a long-running process)
40-
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 30*time.Second)
41+
imageName = createAndWaitForImage(t, svc, imageName, 30*time.Second)
4142

4243
// Create instance
4344
t.Log("Creating instance...")
4445
networkEnabled := false
4546
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
4647
Body: &oapi.CreateInstanceRequest{
4748
Name: "cp-test",
48-
Image: "docker.io/library/nginx:alpine",
49+
Image: imageName,
4950
Network: &struct {
50-
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
51-
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
52-
Enabled *bool `json:"enabled,omitempty"`
51+
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
52+
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
53+
Egress *oapi.CreateInstanceRequestNetworkEgress `json:"egress,omitempty"`
54+
Enabled *bool `json:"enabled,omitempty"`
5355
}{
5456
Enabled: &networkEnabled,
5557
},
@@ -168,6 +170,7 @@ func TestCpDirectoryToInstance(t *testing.T) {
168170
}
169171

170172
svc := newTestService(t)
173+
imageName := "docker.io/library/nginx:alpine"
171174

172175
// Ensure system files
173176
t.Log("Ensuring system files...")
@@ -176,19 +179,20 @@ func TestCpDirectoryToInstance(t *testing.T) {
176179
require.NoError(t, err)
177180

178181
// Create and wait for nginx image (has a long-running process)
179-
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 30*time.Second)
182+
imageName = createAndWaitForImage(t, svc, imageName, 30*time.Second)
180183

181184
// Create instance
182185
t.Log("Creating instance...")
183186
networkEnabled := false
184187
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
185188
Body: &oapi.CreateInstanceRequest{
186189
Name: "cp-dir-test",
187-
Image: "docker.io/library/nginx:alpine",
190+
Image: imageName,
188191
Network: &struct {
189-
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
190-
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
191-
Enabled *bool `json:"enabled,omitempty"`
192+
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
193+
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
194+
Egress *oapi.CreateInstanceRequestNetworkEgress `json:"egress,omitempty"`
195+
Enabled *bool `json:"enabled,omitempty"`
192196
}{
193197
Enabled: &networkEnabled,
194198
},

cmd/api/api/exec_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func TestExecInstanceNonTTY(t *testing.T) {
2929
}
3030

3131
svc := newTestService(t)
32+
imageName := "docker.io/library/nginx:alpine"
3233

3334
// Ensure system files (kernel and initrd) are available
3435
t.Log("Ensuring system files...")
@@ -38,19 +39,20 @@ func TestExecInstanceNonTTY(t *testing.T) {
3839
t.Log("System files ready")
3940

4041
// Create and wait for nginx image (has a proper long-running process)
41-
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 30*time.Second)
42+
imageName = createAndWaitForImage(t, svc, imageName, 30*time.Second)
4243

4344
// Create instance
4445
t.Log("Creating instance...")
4546
networkEnabled := false
4647
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
4748
Body: &oapi.CreateInstanceRequest{
4849
Name: "exec-test",
49-
Image: "docker.io/library/nginx:alpine",
50+
Image: imageName,
5051
Network: &struct {
51-
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
52-
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
53-
Enabled *bool `json:"enabled,omitempty"`
52+
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
53+
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
54+
Egress *oapi.CreateInstanceRequestNetworkEgress `json:"egress,omitempty"`
55+
Enabled *bool `json:"enabled,omitempty"`
5456
}{
5557
Enabled: &networkEnabled,
5658
},
@@ -170,6 +172,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
170172
}
171173

172174
svc := newTestService(t)
175+
imageName := "docker.io/library/debian:12-slim"
173176

174177
// Ensure system files (kernel and initrd) are available
175178
t.Log("Ensuring system files...")
@@ -179,7 +182,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
179182
t.Log("System files ready")
180183

181184
// Create Debian 12 slim image (minimal, no iproute2)
182-
createAndWaitForImage(t, svc, "docker.io/library/debian:12-slim", 60*time.Second)
185+
imageName = createAndWaitForImage(t, svc, imageName, 60*time.Second)
183186

184187
// Create instance with a long-running command so the VM stays alive for exec.
185188
// Debian's default CMD is "bash" which exits immediately (no stdin),
@@ -190,12 +193,13 @@ func TestExecWithDebianMinimal(t *testing.T) {
190193
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
191194
Body: &oapi.CreateInstanceRequest{
192195
Name: "debian-exec-test",
193-
Image: "docker.io/library/debian:12-slim",
196+
Image: imageName,
194197
Cmd: &cmdOverride,
195198
Network: &struct {
196-
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
197-
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
198-
Enabled *bool `json:"enabled,omitempty"`
199+
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
200+
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
201+
Egress *oapi.CreateInstanceRequestNetworkEgress `json:"egress,omitempty"`
202+
Enabled *bool `json:"enabled,omitempty"`
199203
}{
200204
Enabled: &networkEnabled,
201205
},

cmd/api/api/images_test.go

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package api
22

33
import (
44
"fmt"
5-
"strings"
65
"testing"
76
"time"
87

@@ -42,8 +41,8 @@ func TestCreateImage_Async(t *testing.T) {
4241
// Create images before alpine to populate the queue
4342
t.Log("Creating image queue...")
4443
queueImages := []string{
45-
"docker.io/library/busybox:latest",
46-
"docker.io/library/nginx:alpine",
44+
apiTestImageRef(t, "docker.io/library/busybox:latest"),
45+
apiTestImageRef(t, "docker.io/library/nginx:alpine"),
4746
}
4847
for _, name := range queueImages {
4948
_, err := svc.CreateImage(ctx, oapi.CreateImageRequestObject{
@@ -54,9 +53,10 @@ func TestCreateImage_Async(t *testing.T) {
5453

5554
// Create alpine (should be last in queue)
5655
t.Log("Creating alpine image (should be queued)...")
56+
alpineName := apiTestImageRef(t, "docker.io/library/alpine:latest")
5757
createResp, err := svc.CreateImage(ctx, oapi.CreateImageRequestObject{
5858
Body: &oapi.CreateImageRequest{
59-
Name: "docker.io/library/alpine:latest",
59+
Name: alpineName,
6060
},
6161
})
6262
require.NoError(t, err)
@@ -65,14 +65,16 @@ func TestCreateImage_Async(t *testing.T) {
6565
require.True(t, ok, "expected 202 accepted response")
6666

6767
img := oapi.Image(acceptedResp)
68-
require.Equal(t, "docker.io/library/alpine:latest", img.Name)
68+
require.Equal(t, alpineName, img.Name)
6969
require.NotEmpty(t, img.Digest, "digest should be populated immediately")
7070
t.Logf("Image created: name=%s, digest=%s, initial_status=%s, queue_position=%v",
7171
img.Name, img.Digest, img.Status, img.QueuePosition)
7272

7373
// Construct digest reference for polling: repository@digest
7474
// GetImage expects format like "docker.io/library/alpine@sha256:..."
75-
digestRef := "docker.io/library/alpine@" + img.Digest
75+
alpineRef, err := images.ParseNormalizedRef(alpineName)
76+
require.NoError(t, err)
77+
digestRef := alpineRef.Repository() + "@" + img.Digest
7678
t.Logf("Polling with digest reference: %s", digestRef)
7779

7880
// Poll until ready using digest (tag symlink doesn't exist until status=ready)
@@ -135,7 +137,7 @@ func TestCreateImage_InvalidTag(t *testing.T) {
135137
t.Log("Creating image with invalid tag...")
136138
createResp, err := svc.CreateImage(ctx, oapi.CreateImageRequestObject{
137139
Body: &oapi.CreateImageRequest{
138-
Name: "docker.io/library/busybox:foobar",
140+
Name: apiTestImageRef(t, "docker.io/library/busybox:foobar"),
139141
},
140142
})
141143
require.NoError(t, err)
@@ -181,13 +183,13 @@ func TestCreateImage_Idempotent(t *testing.T) {
181183
ctx := ctx()
182184

183185
// Create first image to occupy queue position 0
184-
t.Log("Creating first image (busybox) to occupy queue...")
186+
t.Log("Creating first image (nginx) to occupy queue...")
185187
_, err := svc.CreateImage(ctx, oapi.CreateImageRequestObject{
186-
Body: &oapi.CreateImageRequest{Name: "docker.io/library/busybox:latest"},
188+
Body: &oapi.CreateImageRequest{Name: apiTestImageRef(t, "docker.io/library/nginx:alpine")},
187189
})
188190
require.NoError(t, err)
189191

190-
imageName := "docker.io/library/alpine:3.18"
192+
imageName := apiTestImageRef(t, "docker.io/library/alpine:latest")
191193

192194
// First call - should create and queue at position 1
193195
t.Log("First CreateImage call (alpine)...")
@@ -245,9 +247,9 @@ func TestCreateImage_Idempotent(t *testing.T) {
245247
}
246248

247249
// Construct digest reference: repository@digest
248-
// Extract repository from imageName (strip tag part)
249-
repository := strings.Split(imageName, ":")[0]
250-
digestRef := repository + "@" + img1.Digest
250+
imageRef, err := images.ParseNormalizedRef(imageName)
251+
require.NoError(t, err)
252+
digestRef := imageRef.Repository() + "@" + img1.Digest
251253
t.Logf("Polling with digest reference: %s", digestRef)
252254

253255
// Wait for build to complete - poll by digest (tag symlink doesn't exist until status=ready)

cmd/api/api/instances.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,41 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
135135
if request.Body.Network != nil && request.Body.Network.Enabled != nil {
136136
networkEnabled = *request.Body.Network.Enabled
137137
}
138+
var networkEgress *instances.NetworkEgressPolicy
139+
if request.Body.Network != nil && request.Body.Network.Egress != nil {
140+
enabled := request.Body.Network.Egress.Enabled != nil && *request.Body.Network.Egress.Enabled
141+
networkEgress = &instances.NetworkEgressPolicy{Enabled: enabled}
142+
if request.Body.Network.Egress.Enforcement != nil && request.Body.Network.Egress.Enforcement.Mode != nil {
143+
networkEgress.EnforcementMode = instances.EgressEnforcementMode(*request.Body.Network.Egress.Enforcement.Mode)
144+
} else if enabled {
145+
networkEgress.EnforcementMode = instances.EgressEnforcementModeAll
146+
}
147+
}
148+
var credentials map[string]instances.CredentialPolicy
149+
if request.Body.Credentials != nil {
150+
credentials = make(map[string]instances.CredentialPolicy, len(*request.Body.Credentials))
151+
for credentialName, credential := range *request.Body.Credentials {
152+
policy := instances.CredentialPolicy{
153+
Source: instances.CredentialSource{
154+
Env: credential.Source.Env,
155+
},
156+
Inject: make([]instances.CredentialInjectRule, 0, len(credential.Inject)),
157+
}
158+
for _, inject := range credential.Inject {
159+
rule := instances.CredentialInjectRule{
160+
As: instances.CredentialInjectAs{
161+
Header: inject.As.Header,
162+
Format: inject.As.Format,
163+
},
164+
}
165+
if inject.Hosts != nil {
166+
rule.Hosts = append([]string(nil), (*inject.Hosts)...)
167+
}
168+
policy.Inject = append(policy.Inject, rule)
169+
}
170+
credentials[credentialName] = policy
171+
}
172+
}
138173

139174
// Parse network bandwidth limits (0 = auto)
140175
// Supports both bit-based (e.g., "1Gbps") and byte-based (e.g., "125MB/s") formats
@@ -255,6 +290,8 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
255290
Env: env,
256291
Tags: resourceTags,
257292
NetworkEnabled: networkEnabled,
293+
NetworkEgress: networkEgress,
294+
Credentials: credentials,
258295
Devices: deviceRefs,
259296
Volumes: volumes,
260297
Hypervisor: hvType,

0 commit comments

Comments
 (0)