Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
e28a8f6
Stabilize integration network and ingress test flakes
sjmiller609 Mar 7, 2026
e66e759
Speed up fork-from-running integration tests
sjmiller609 Mar 8, 2026
4433590
Reduce fork test agent wait latency
sjmiller609 Mar 8, 2026
b708aef
feat(instances): add optional egress MITM proxy secret rewriting
sjmiller609 Mar 8, 2026
d8a64b9
test(instances): use prewarmed nginx image in egress proxy integratio…
sjmiller609 Mar 8, 2026
e3beda6
feat(egress-proxy): source secret rewrites from instance env
sjmiller609 Mar 8, 2026
d56d8b5
chore: remove agents notes from this PR
sjmiller609 Mar 8, 2026
cbe7082
test(ci): mirror API/integration image refs and prewarm missing images
sjmiller609 Mar 8, 2026
c58e231
Add egress proxy enforcement modes with strict default
sjmiller609 Mar 8, 2026
2d76140
Fix CI image pull flakes in API and e2e install tests
sjmiller609 Mar 8, 2026
63fa947
Add per-env domain-gated HTTPS secret substitution
sjmiller609 Mar 9, 2026
a9231cd
Fix review issues in cert cache and prewarm logging
sjmiller609 Mar 9, 2026
26c4a29
Fix follow-up proxy review findings
sjmiller609 Mar 9, 2026
3266c10
Address latest bugbot follow-ups
sjmiller609 Mar 9, 2026
060e1d1
Harden HTTP proxy replacement destination handling
sjmiller609 Mar 9, 2026
e648719
Merge remote-tracking branch 'origin/main' into feature/egress-mitm-p…
sjmiller609 Mar 10, 2026
2ad0511
Merge remote-tracking branch 'origin/main' into feature/egress-mitm-p…
sjmiller609 Mar 10, 2026
717e16f
Fix restore proxy config refresh and async image queue coverage
sjmiller609 Mar 10, 2026
3ef9157
Remove unused egress proxy host normalizer
sjmiller609 Mar 10, 2026
8c3775f
Format instances types after merge
sjmiller609 Mar 10, 2026
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
1 change: 1 addition & 0 deletions cmd/api/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ func ctxWithImage(svc *ApiService, name string) context.Context {
// Returns the image name on success, or fails the test on error/timeout.
func createAndWaitForImage(t *testing.T, svc *ApiService, imageName string, timeout time.Duration) string {
t.Helper()
imageName = apiTestImageRef(t, imageName)

t.Logf("Creating image %s...", imageName)
imgResp, err := svc.CreateImage(ctx(), oapi.CreateImageRequestObject{
Expand Down
10 changes: 6 additions & 4 deletions cmd/api/api/cp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ func TestCpToAndFromInstance(t *testing.T) {
}

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

// Ensure system files (kernel and initrd) are available
t.Log("Ensuring system files...")
Expand All @@ -37,15 +38,15 @@ func TestCpToAndFromInstance(t *testing.T) {
t.Log("System files ready")

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

// Create instance
t.Log("Creating instance...")
networkEnabled := false
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "cp-test",
Image: "docker.io/library/nginx:alpine",
Image: imageName,
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Expand Down Expand Up @@ -168,6 +169,7 @@ func TestCpDirectoryToInstance(t *testing.T) {
}

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

// Ensure system files
t.Log("Ensuring system files...")
Expand All @@ -176,15 +178,15 @@ func TestCpDirectoryToInstance(t *testing.T) {
require.NoError(t, err)

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

// Create instance
t.Log("Creating instance...")
networkEnabled := false
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "cp-dir-test",
Image: "docker.io/library/nginx:alpine",
Image: imageName,
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Expand Down
10 changes: 6 additions & 4 deletions cmd/api/api/exec_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ func TestExecInstanceNonTTY(t *testing.T) {
}

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

// Ensure system files (kernel and initrd) are available
t.Log("Ensuring system files...")
Expand All @@ -38,15 +39,15 @@ func TestExecInstanceNonTTY(t *testing.T) {
t.Log("System files ready")

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

// Create instance
t.Log("Creating instance...")
networkEnabled := false
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "exec-test",
Image: "docker.io/library/nginx:alpine",
Image: imageName,
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Expand Down Expand Up @@ -170,6 +171,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
}

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

// Ensure system files (kernel and initrd) are available
t.Log("Ensuring system files...")
Expand All @@ -179,7 +181,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
t.Log("System files ready")

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

// Create instance with a long-running command so the VM stays alive for exec.
// Debian's default CMD is "bash" which exits immediately (no stdin),
Expand All @@ -190,7 +192,7 @@ func TestExecWithDebianMinimal(t *testing.T) {
instResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "debian-exec-test",
Image: "docker.io/library/debian:12-slim",
Image: imageName,
Cmd: &cmdOverride,
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
Expand Down
28 changes: 15 additions & 13 deletions cmd/api/api/images_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package api

import (
"fmt"
"strings"
"testing"
"time"

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

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

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

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

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

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

imageName := "docker.io/library/alpine:3.18"
imageName := apiTestImageRef(t, "docker.io/library/alpine:latest")

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

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

// Wait for build to complete - poll by digest (tag symlink doesn't exist until status=ready)
Expand Down
20 changes: 20 additions & 0 deletions cmd/api/api/instances.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,25 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
if request.Body.Network != nil && request.Body.Network.Enabled != nil {
networkEnabled = *request.Body.Network.Enabled
}
var egressProxyConfig *instances.EgressProxyConfig
if request.Body.EgressProxy != nil {
enabled := request.Body.EgressProxy.Enabled != nil && *request.Body.EgressProxy.Enabled
egressProxyConfig = &instances.EgressProxyConfig{Enabled: enabled}
if request.Body.EgressProxy.MockEnvVars != nil {
egressProxyConfig.MockEnvVars = append([]string(nil), (*request.Body.EgressProxy.MockEnvVars)...)
}
if request.Body.EgressProxy.MockEnvVarDomains != nil {
egressProxyConfig.MockEnvVarDomains = make(map[string][]string, len(*request.Body.EgressProxy.MockEnvVarDomains))
for envVar, domains := range *request.Body.EgressProxy.MockEnvVarDomains {
egressProxyConfig.MockEnvVarDomains[envVar] = append([]string(nil), domains...)
}
}
if request.Body.EgressProxy.EnforcementMode != nil {
egressProxyConfig.EnforcementMode = instances.EgressProxyEnforcementMode(*request.Body.EgressProxy.EnforcementMode)
} else if enabled {
egressProxyConfig.EnforcementMode = instances.EgressProxyEnforcementModeAll
}
}

// Parse network bandwidth limits (0 = auto)
// Supports both bit-based (e.g., "1Gbps") and byte-based (e.g., "125MB/s") formats
Expand Down Expand Up @@ -255,6 +274,7 @@ func (s *ApiService) CreateInstance(ctx context.Context, request oapi.CreateInst
Env: env,
Tags: resourceTags,
NetworkEnabled: networkEnabled,
EgressProxy: egressProxyConfig,
Devices: deviceRefs,
Volumes: volumes,
Hypervisor: hvType,
Expand Down
98 changes: 94 additions & 4 deletions cmd/api/api/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ func TestCreateInstance_ParsesHumanReadableSizes(t *testing.T) {
svc := newTestService(t)

// Create and wait for alpine image
createAndWaitForImage(t, svc, "docker.io/library/alpine:latest", 30*time.Second)
imageName := createAndWaitForImage(t, svc, "docker.io/library/alpine:latest", 30*time.Second)

// Ensure system files (kernel and initramfs) are available
t.Log("Ensuring system files (kernel and initramfs)...")
Expand All @@ -69,7 +69,7 @@ func TestCreateInstance_ParsesHumanReadableSizes(t *testing.T) {
resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-sizes",
Image: "docker.io/library/alpine:latest",
Image: imageName,
Size: &size,
HotplugSize: &hotplugSize,
OverlaySize: &overlaySize,
Expand Down Expand Up @@ -215,6 +215,96 @@ func TestCreateInstance_OmittedHotplugSizeDefaultsToZero(t *testing.T) {
assert.Equal(t, int64(0), int64(hotplugBytes), "response should report zero hotplug_size when omitted")
}

func TestCreateInstance_MapsEgressProxyMockEnvVars(t *testing.T) {
t.Parallel()
svc := newTestService(t)

origMgr := svc.InstanceManager
mockMgr := &captureCreateManager{Manager: origMgr}
svc.InstanceManager = mockMgr

enabled := true
mockEnvVars := []string{"OUTBOUND_OPENAI_KEY", "GITHUB_TOKEN"}
mockEnvVarDomains := map[string][]string{
"OUTBOUND_OPENAI_KEY": []string{"api.openai.com", "*.openai.com"},
}
env := map[string]string{
"OUTBOUND_OPENAI_KEY": "real-openai-key-123",
"GITHUB_TOKEN": "real-gh-token-456",
}

resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-egress-proxy-mock-env-vars",
Image: "docker.io/library/alpine:latest",
Env: &env,
EgressProxy: &struct {
Enabled *bool `json:"enabled,omitempty"`
EnforcementMode *oapi.CreateInstanceRequestEgressProxyEnforcementMode `json:"enforcement_mode,omitempty"`
MockEnvVarDomains *map[string][]string `json:"mock_env_var_domains,omitempty"`
MockEnvVars *[]string `json:"mock_env_vars,omitempty"`
}{
Enabled: &enabled,
MockEnvVarDomains: &mockEnvVarDomains,
MockEnvVars: &mockEnvVars,
},
},
})
require.NoError(t, err)
_, ok := resp.(oapi.CreateInstance201JSONResponse)
require.True(t, ok, "expected 201 response")

require.NotNil(t, mockMgr.lastReq)
require.NotNil(t, mockMgr.lastReq.EgressProxy)
assert.True(t, mockMgr.lastReq.EgressProxy.Enabled)
assert.Equal(t, instances.EgressProxyEnforcementModeAll, mockMgr.lastReq.EgressProxy.EnforcementMode)
assert.Equal(t, []string{"OUTBOUND_OPENAI_KEY", "GITHUB_TOKEN"}, mockMgr.lastReq.EgressProxy.MockEnvVars)
assert.Equal(t, map[string][]string{"OUTBOUND_OPENAI_KEY": []string{"api.openai.com", "*.openai.com"}}, mockMgr.lastReq.EgressProxy.MockEnvVarDomains)
assert.Equal(t, "real-openai-key-123", mockMgr.lastReq.Env["OUTBOUND_OPENAI_KEY"])
assert.Equal(t, "real-gh-token-456", mockMgr.lastReq.Env["GITHUB_TOKEN"])
}

func TestCreateInstance_MapsEgressProxyEnforcementMode(t *testing.T) {
t.Parallel()
svc := newTestService(t)

origMgr := svc.InstanceManager
mockMgr := &captureCreateManager{Manager: origMgr}
svc.InstanceManager = mockMgr

enabled := true
mode := oapi.HttpHttpsOnly
env := map[string]string{
"OUTBOUND_OPENAI_KEY": "real-openai-key-123",
}
mockEnvVars := []string{"OUTBOUND_OPENAI_KEY"}

resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-egress-proxy-enforcement-mode",
Image: "docker.io/library/alpine:latest",
Env: &env,
EgressProxy: &struct {
Enabled *bool `json:"enabled,omitempty"`
EnforcementMode *oapi.CreateInstanceRequestEgressProxyEnforcementMode `json:"enforcement_mode,omitempty"`
MockEnvVarDomains *map[string][]string `json:"mock_env_var_domains,omitempty"`
MockEnvVars *[]string `json:"mock_env_vars,omitempty"`
}{
Enabled: &enabled,
EnforcementMode: &mode,
MockEnvVars: &mockEnvVars,
},
},
})
require.NoError(t, err)
_, ok := resp.(oapi.CreateInstance201JSONResponse)
require.True(t, ok, "expected 201 response")

require.NotNil(t, mockMgr.lastReq)
require.NotNil(t, mockMgr.lastReq.EgressProxy)
assert.Equal(t, instances.EgressProxyEnforcementModeHTTPHTTPSOnly, mockMgr.lastReq.EgressProxy.EnforcementMode)
}

func TestForkInstance_Success(t *testing.T) {
t.Parallel()
svc := newTestService(t)
Expand Down Expand Up @@ -408,7 +498,7 @@ func TestInstanceLifecycle_StopStart(t *testing.T) {
svc := newTestService(t)

// Use nginx:alpine so the VM runs a real workload (not just exits immediately)
createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 60*time.Second)
imageName := createAndWaitForImage(t, svc, "docker.io/library/nginx:alpine", 60*time.Second)

// Ensure system files (kernel and initramfs) are available
t.Log("Ensuring system files (kernel and initramfs)...")
Expand All @@ -423,7 +513,7 @@ func TestInstanceLifecycle_StopStart(t *testing.T) {
createResp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-lifecycle",
Image: "docker.io/library/nginx:alpine",
Image: imageName,
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Expand Down
Loading