feat(dl): tag/digest-aware download endpoint#4
Merged
Merged
Conversation
Promote /dl/{name}/{ref} as the canonical form. ref is a tag (e.g.
"latest", "22h2-20260510") or a digest reference ("sha256:..."), routed
via the existing isDigestRef check that the /v2/ manifest endpoint
already uses.
Backward compat: the legacy /dl/{name} form (latest hardcoded) used to
also accept multi-segment URLs like /dl/simular/win11 — under the new
two-segment route, those split as name=simular, ref=win11. The handler
catches the resulting 404 and retries name+"/"+ref with implicit
"latest", then flags the response with a Deprecation header and a
successor-version Link so callers know the URL form needs to be updated.
Motivation: hot snapshots pin their base image by tag (e.g.
simular/win10:22h2-20260510), but the previous /dl/ endpoint hardcoded
ManifestJSON(name, "latest"). That made tagged base images
unreachable through the cloudimg HTTP-stream channel that cocoon's
cloudimg backend relies on, which surfaced as the clone-time
"Backing file I/O error" reported in cocoonstack/cocoon#37 #38.
The OCI registry on /v2/ already supports tags and digests fine. This
patch closes the gap on the cloudimg-stream side without teaching
cocoon's HTTP-only cloudimg backend to speak OCI.
Drop WHAT-style enumerations and inline comments that restated the code. The handler godoc keeps the only non-obvious bit (the legacy fallback WHY); the route registration in server.go and the field-by-field comment on the Deprecation header lose redundant prose. Helper godoc shrinks to one line about the resolved-pair return convention.
There was a problem hiding this comment.
Pull request overview
This PR updates the server’s unauthenticated /dl/ download endpoint to support tag- and digest-qualified downloads (aligning behavior with the existing /v2/ manifest handling), while preserving backward compatibility for older /dl/ URL shapes.
Changes:
- Add canonical
/dl/{name}/{ref}routing whererefmay be a tag orsha256:...digest reference, while keeping/dl/{name}as a transitional “latest” default. - Implement handler-side legacy fallback to reinterpret older multi-segment-name URLs when the initial lookup 404s, and emit deprecation/migration response headers when fallback is used.
- Add gorilla/mux route-resolution tests for the supported
/dl/URL forms and a public-path check for the new canonical URLs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
server/server.go |
Adds the canonical /dl/{name}/{ref} route (tag/digest aware) ahead of the existing /dl/{name} route. |
server/download.go |
Updates the download handler to accept ref, fetch manifests by tag/digest, and implement legacy fallback + deprecation headers. |
server/dl_routes_test.go |
Adds unit tests validating mux route splitting for /dl/ paths and confirming new canonical paths remain auth-exempt. |
Comments suppressed due to low confidence (3)
server/download.go:117
- In fetchManifestWithLegacyFallback, if the legacy retry (legacyName, "latest") fails, the function returns the original (name, ref) error (
err) instead of the legacy error (lerr). This can incorrectly turn a legacy-path internal error into a 404, and it also makes debugging harder. Returnlerr(or prefer it when it’s non-404) so callers get the correct status/cause.
StreamBlob(ctx context.Context, digest string) (io.ReadCloser, int64, error)
}
type manifestStreamer interface {
blobStreamer
server/download.go:115
- The legacy fallback currently triggers for any 404, including when
refis a digest reference (e.g.sha256:...). That means a digest-pinned URL that should be immutable can silently fall back to fetching a different repository (name/ref:latest) if it exists, returning unexpected content instead of a 404. Consider disabling the fallback whenisDigestRef(ref)is true (and possibly tightening when fallback is allowed) so digest lookups fail deterministically.
return nil, name, ref, err
}
legacyName := name + "/" + ref
legacy, lerr := s.loadManifestRaw(r, legacyName, "latest")
if lerr != nil {
return nil, name, ref, err
}
return legacy, legacyName, "latest", nil
}
type blobStreamer interface {
StreamBlob(ctx context.Context, digest string) (io.ReadCloser, int64, error)
}
server/server.go:151
- The comment above the /dl routes is misleading about which route covers the legacy
/dl/simular/win11form. With/dl/{name:.+}/{ref}registered first,/dl/simular/win11will match the 2-arg route (name=simular, ref=win11) and rely on the handler fallback; it will not hit the single-arg/dl/{name:.+}route. Updating the comment to reflect the actual match behavior would avoid confusion for future maintainers.
s.router.HandleFunc("/dl/{name:.+}/{ref}", s.handleArtifactDownload).Methods(http.MethodGet)
s.router.HandleFunc("/dl/{name:.+}", s.handleArtifactDownload).Methods(http.MethodGet)
s.router.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
}).Methods(http.MethodGet)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/dl/{name}/{ref}as the canonical download URL form.refis either a tag (latest,22h2-20260510) or a digest reference (sha256:...), dispatched via the existingisDigestRefpredicate the/v2/manifest endpoint already uses./dl/{name}as a transitional route (single-segment URLs continue to meanlatest)./dl/simular/win11: under the new route this initially splits asname=simular, ref=win11and 404s; the handler then retriesname=simular/win11, ref=latest. Successful fallbacks attachDeprecation: true+Link: </dl/{name}/{ref}>; rel="successor-version"so callers can migrate without breakage.Why
Hot snapshots pin their base image by tag (e.g.
simular/win10:22h2-20260510), but the existing/dl/handler hardcodedManifestJSON(name, "latest"). Tagged base images were unreachable via the cloudimg HTTP-stream channel that cocoon's cloudimg backend relies on, which surfaced as the clone-timeBacking file I/O errorreported in cocoonstack/cocoon#37 and #38.The
/v2/registry endpoint already handles tags and digests fine — this PR closes the gap on the cloudimg-stream side. cocoon's cloudimg backend stays a plain HTTP fetcher (no OCI awareness required), so non-epoch sources (https://cloud-images.ubuntu.com/...) keep working unchanged.Test plan
go test ./server/... -run 'TestDLRoute|TestDLPublicPath'passes — covers route resolution for all five URL forms (1-seg, 1-seg+ref, multi-seg, multi-seg+ref, digest)go test ./server/...passes (no existing regressions)go vet ./...cleancocoon image pull https://epoch.simular.cloud/dl/simular/win10/22h2-20260510and.../sha256:<hex>(covered incocoon-specs/tests/vk-epoch-pull-tests.mdper existing convention)Caller migration
https://epoch.../dl/win11https://epoch.../dl/simular/win11Deprecation: trueheader. Migrate to/dl/simular/win11/latesthttps://epoch.../dl/simular/win11/latesthttps://epoch.../dl/simular/win11/22h2-20260510https://epoch.../dl/simular/win11/sha256:adafd938...Companion changes (separate PRs, not blocked by this one):
cocoonstack/cocoon:cmd/core/helpers.go:EnsureImage— passforce=truewhenvmCfg.ImageDigest != ""so the URL-level short-circuit atimages/cloudimg/pull.go:62doesn't mask a stale local blob (issue 37)cocoonstack/vk-cocoon: writecocoonstack.snapshot.baseimageannotations as the new digest-pinned URL form (issue 38)