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
50 changes: 50 additions & 0 deletions cmd/api/api/instances_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,56 @@ func TestGetInstance_NotFound(t *testing.T) {
require.Error(t, err)
}

func TestCreateInstance_AutoPullImage(t *testing.T) {
t.Parallel()
if _, err := os.Stat("/dev/kvm"); os.IsNotExist(err) {
t.Skip("/dev/kvm not available, skipping on this platform")
}

svc := newTestService(t)

// NOTE: intentionally NOT calling createAndWaitForImage here.
// The auto-pull logic in CreateInstance should handle pulling the image.
// The auto-pull has a 5s timeout — alpine:latest is small enough to
// complete within that window.

// Ensure system files (kernel and initramfs) are available
t.Log("Ensuring system files (kernel and initramfs)...")
systemMgr := system.NewManager(paths.New(svc.Config.DataDir))
err := systemMgr.EnsureSystemFiles(ctx())
require.NoError(t, err)

t.Log("Creating instance without pre-pulling image (testing auto-pull)...")
networkEnabled := false
resp, err := svc.CreateInstance(ctx(), oapi.CreateInstanceRequestObject{
Body: &oapi.CreateInstanceRequest{
Name: "test-auto-pull",
Image: "docker.io/library/alpine:latest",
Network: &struct {
BandwidthDownload *string `json:"bandwidth_download,omitempty"`
BandwidthUpload *string `json:"bandwidth_upload,omitempty"`
Enabled *bool `json:"enabled,omitempty"`
}{
Enabled: &networkEnabled,
},
},
})
require.NoError(t, err)

created, ok := resp.(oapi.CreateInstance201JSONResponse)
require.True(t, ok, "expected 201 response — auto-pull should have fetched the image")
t.Logf("Instance created via auto-pull: %s", created.Id)

// Cleanup: delete the instance
instanceID := created.Id
t.Log("Deleting instance...")
deleteResp, err := svc.DeleteInstance(ctxWithInstance(svc, instanceID), oapi.DeleteInstanceRequestObject{Id: instanceID})
require.NoError(t, err)
_, ok = deleteResp.(oapi.DeleteInstance204Response)
require.True(t, ok, "expected 204 response for delete")
t.Log("Instance deleted successfully")
}

func TestCreateInstance_ParsesHumanReadableSizes(t *testing.T) {
t.Parallel()
// Require KVM access for VM creation
Expand Down
30 changes: 26 additions & 4 deletions lib/instances/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,15 +89,37 @@ func (m *manager) createInstance(
return nil, err
}

// 2. Validate image exists and is ready
// 2. Validate image exists and is ready; auto-pull if not found
log.DebugContext(ctx, "validating image", "image", req.Image)
imageInfo, err := m.imageManager.GetImage(ctx, req.Image)
if err != nil {
log.ErrorContext(ctx, "failed to get image", "image", req.Image, "error", err)
if err == images.ErrNotFound {
return nil, fmt.Errorf("image %s: %w", req.Image, err)
// Auto-pull: image not found locally, kick off the pull in the
// background and wait up to 5 seconds for it to complete.
log.InfoContext(ctx, "image not found locally, auto-pulling", "image", req.Image)
_, pullErr := m.imageManager.CreateImage(ctx, images.CreateImageRequest{Name: req.Image})
if pullErr != nil {
log.ErrorContext(ctx, "failed to auto-pull image", "image", req.Image, "error", pullErr)
return nil, fmt.Errorf("auto-pull image %s: %w", req.Image, pullErr)
}
// Wait with a short timeout — if the pull doesn't finish in time
// we return an error but let it continue in the background.
pullCtx, pullCancel := context.WithTimeout(ctx, 5*time.Second)
defer pullCancel()
if waitErr := m.imageManager.WaitForReady(pullCtx, req.Image); waitErr != nil {
log.InfoContext(ctx, "image pull not ready within timeout, pull continues in background", "image", req.Image, "error", waitErr)
return nil, fmt.Errorf("%w: image %s is being pulled, please try again shortly", ErrImageNotReady, req.Image)
}
// Re-fetch after successful pull
imageInfo, err = m.imageManager.GetImage(ctx, req.Image)
if err != nil {
log.ErrorContext(ctx, "failed to get image after auto-pull", "image", req.Image, "error", err)
return nil, fmt.Errorf("get image after auto-pull: %w", err)
}
} else {
log.ErrorContext(ctx, "failed to get image", "image", req.Image, "error", err)
return nil, fmt.Errorf("get image: %w", err)
}
return nil, fmt.Errorf("get image: %w", err)
}

if imageInfo.Status != images.StatusReady {
Expand Down
Loading