From 048ee7311c39d6c3c7efad9c662fa2a1993ced97 Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sat, 29 Nov 2025 15:57:28 -0500 Subject: [PATCH 1/2] fix(run): wait for image to be ready before creating instance The CLI was immediately attempting to create an instance after calling POST /images, which returns 202 Accepted with status 'pending'. Since image builds are asynchronous, the instance creation would fail with 'image_not_ready' error. This fix adds polling logic to wait for the image status to become 'ready' (or 'failed') before proceeding to create the instance. Status updates are shown to the user as the image progresses through: pending -> pulling -> converting -> ready Fixes race condition where first 'hypeman run' would fail but second would succeed after background build completed. --- pkg/cmd/run.go | 78 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/run.go b/pkg/cmd/run.go index f5f8222..03c6723 100644 --- a/pkg/cmd/run.go +++ b/pkg/cmd/run.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "strings" + "time" "github.com/onkernel/hypeman-go" "github.com/onkernel/hypeman-go/option" @@ -65,25 +66,29 @@ func handleRun(ctx context.Context, cmd *cli.Command) error { client := hypeman.NewClient(getDefaultRequestOptions(cmd)...) - // Check if image exists, pull if not - _, err := client.Images.Get(ctx, image) + // Check if image exists and is ready + imgInfo, err := client.Images.Get(ctx, image) if err != nil { // Image not found, try to pull it var apiErr *hypeman.Error if ok := isNotFoundError(err, &apiErr); ok { fmt.Fprintf(os.Stderr, "Image not found locally. Pulling %s...\n", image) - _, err = client.Images.New(ctx, hypeman.ImageNewParams{ + imgInfo, err = client.Images.New(ctx, hypeman.ImageNewParams{ Name: image, }) if err != nil { return fmt.Errorf("failed to pull image: %w", err) } - fmt.Fprintf(os.Stderr, "Pull complete.\n") } else { return fmt.Errorf("failed to check image: %w", err) } } + // Wait for image to be ready (build is asynchronous) + if err := waitForImageReady(ctx, &client, image, imgInfo); err != nil { + return err + } + // Generate name if not provided name := cmd.String("name") if name == "" { @@ -139,3 +144,68 @@ func isNotFoundError(err error, target **hypeman.Error) bool { return false } +// waitForImageReady polls image status until it becomes ready or failed +func waitForImageReady(ctx context.Context, client *hypeman.Client, imageName string, img *hypeman.Image) error { + if img.Status == hypeman.ImageStatusReady { + return nil + } + if img.Status == hypeman.ImageStatusFailed { + if img.Error != "" { + return fmt.Errorf("image build failed: %s", img.Error) + } + return fmt.Errorf("image build failed") + } + + // Poll until ready + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + // Show initial status + showImageStatus(img) + + for { + select { + case <-ctx.Done(): + return ctx.Err() + case <-ticker.C: + updated, err := client.Images.Get(ctx, imageName) + if err != nil { + return fmt.Errorf("failed to check image status: %w", err) + } + + // Show status update if changed + if updated.Status != img.Status { + showImageStatus(updated) + img = updated + } + + switch updated.Status { + case hypeman.ImageStatusReady: + return nil + case hypeman.ImageStatusFailed: + if updated.Error != "" { + return fmt.Errorf("image build failed: %s", updated.Error) + } + return fmt.Errorf("image build failed") + } + } + } +} + +// showImageStatus prints image build status to stderr +func showImageStatus(img *hypeman.Image) { + switch img.Status { + case hypeman.ImageStatusPending: + if img.QueuePosition > 0 { + fmt.Fprintf(os.Stderr, "Queued (position %d)...\n", img.QueuePosition) + } else { + fmt.Fprintf(os.Stderr, "Queued...\n") + } + case hypeman.ImageStatusPulling: + fmt.Fprintf(os.Stderr, "Pulling image...\n") + case hypeman.ImageStatusConverting: + fmt.Fprintf(os.Stderr, "Converting to disk image...\n") + case hypeman.ImageStatusReady: + fmt.Fprintf(os.Stderr, "Image ready.\n") + } +} From fdd215c0f542a93574320509384d6c6078f3227b Mon Sep 17 00:00:00 2001 From: Rafael Garcia Date: Sun, 30 Nov 2025 06:20:07 -0500 Subject: [PATCH 2/2] fix missing commands --- pkg/cmd/cmd.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/cmd/cmd.go b/pkg/cmd/cmd.go index b69373e..c2e4f6e 100644 --- a/pkg/cmd/cmd.go +++ b/pkg/cmd/cmd.go @@ -98,6 +98,8 @@ func init() { &instancesList, &instancesPutInStandby, &instancesRestoreFromStandby, + &instancesLogs, + &instancesDelete, }, }, {