diff --git a/cmd/core/ensure_image_test.go b/cmd/core/ensure_image_test.go index 5eb2414e..5dde2b4d 100644 --- a/cmd/core/ensure_image_test.go +++ b/cmd/core/ensure_image_test.go @@ -98,6 +98,55 @@ func TestEnsureImage_ForceWhenDigestPinned(t *testing.T) { } } +// A cloudimg ref without an http(s) scheme reaching Pull surfaces as +// `unsupported protocol scheme` from http.Get; the shape guard short-circuits. +func TestEnsureImage_SkipsBadShape(t *testing.T) { + tests := []struct { + name string + image string + imageType string + }{ + {"cloudimg bare OCI ref", "simular/win10:22h2-20260510", types.ImageTypeCloudImg}, + {"cloudimg local name", "win11", types.ImageTypeCloudImg}, + {"cloudimg non-http scheme", "file:///foo.img", types.ImageTypeCloudImg}, + {"oci malformed ref", "::bad::", types.ImageTypeOCI}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &fakeImageBackend{typ: tt.imageType} + EnsureImage(t.Context(), []imagebackend.Images{f}, &types.VMConfig{ + Config: types.Config{Image: tt.image, ImageType: tt.imageType}, + }) + if len(f.pullRefs) != 0 { + t.Errorf("Pull called %d time(s) with %v, want 0 (shape should have failed)", len(f.pullRefs), f.pullRefs) + } + }) + } +} + +// Acceptance counterpart: well-formed refs must reach Pull. +func TestEnsureImage_AcceptsGoodShape(t *testing.T) { + tests := []struct { + name string + image string + imageType string + }{ + {"cloudimg https url", "https://cloud-images.ubuntu.com/x.img", types.ImageTypeCloudImg}, + {"oci tagged ref", "ghcr.io/cocoonstack/cocoon/ubuntu:24.04", types.ImageTypeOCI}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + f := &fakeImageBackend{typ: tt.imageType} + EnsureImage(t.Context(), []imagebackend.Images{f}, &types.VMConfig{ + Config: types.Config{Image: tt.image, ImageType: tt.imageType}, + }) + if len(f.pullRefs) != 1 || f.pullRefs[0] != tt.image { + t.Errorf("Pull = %v, want one call for %q", f.pullRefs, tt.image) + } + }) + } +} + func TestEnsureImage_SkipsPullWhenDigestLocal(t *testing.T) { const digest = "sha256:adafd938488daa114be898848eb24b9b0afffc21ac18f8b11f3f0057644b11e1" f := &fakeImageBackend{ diff --git a/cmd/core/helpers.go b/cmd/core/helpers.go index f49f81f5..b707ad22 100644 --- a/cmd/core/helpers.go +++ b/cmd/core/helpers.go @@ -248,6 +248,10 @@ func EnsureImage(ctx context.Context, backends []imagebackend.Images, vmCfg *typ // Pull by digest reference when available — ensures we get the exact // version recorded at snapshot time, not whatever the tag points to now. pullRef := digestPullRef(vmCfg.Image, vmCfg.ImageDigest, vmCfg.ImageType) + if shapeErr := validateRefShape(pullRef, vmCfg.ImageType); shapeErr != nil { + logger.Warnf(ctx, "skipping auto-pull of %s: %v — pre-pull manually if missing", pullRef, shapeErr) + return + } logger.Infof(ctx, "base image not found locally, pulling %s ...", pullRef) // Pinned digest with no local hit: force past cloudimg's URL-keyed cache. needForce := vmCfg.ImageDigest != "" @@ -491,6 +495,25 @@ func IsURL(ref string) bool { return strings.HasPrefix(ref, "http://") || strings.HasPrefix(ref, "https://") } +// validateRefShape catches malformed base-image refs before they hit a backend +// that would surface a misleading downstream error. cloudimg fetches over HTTP +// (ref must start with http:// or https://); OCI pulls via registry protocol +// (ref must parse via name.ParseReference). A bare OCI ref leaking into the +// cloudimg path would otherwise surface as `unsupported protocol scheme ""`. +func validateRefShape(ref, imageType string) error { + switch imageType { + case types.ImageTypeCloudImg: + if !IsURL(ref) { + return fmt.Errorf("cloudimg ref %q is not an http(s) URL (imported or bare OCI ref?)", ref) + } + case types.ImageTypeOCI: + if _, err := name.ParseReference(ref); err != nil { + return fmt.Errorf("oci ref %q is not a valid OCI reference: %w", ref, err) + } + } + return nil +} + // digestPullRef pins OCI pulls by digest; returns image as-is for others. func digestPullRef(image, digest, imageType string) string { if digest == "" || imageType != types.ImageTypeOCI {