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
49 changes: 49 additions & 0 deletions cmd/core/ensure_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
23 changes: 23 additions & 0 deletions cmd/core/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 != ""
Expand Down Expand Up @@ -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 {
Expand Down
Loading