Merge pull request #28358 from aaron-ang/issue-17726-distribution-api

api: implement compat distribution inspect endpoint
This commit is contained in:
Jan Rodák 2026-09-23 10:49:31 +02:00 • committed by GitHub
commit 253e24f65e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 221 additions and 6 deletions

View file

@ -0,0 +1,148 @@
//go:build !remote && (linux || freebsd)
package compat
import (
"context"
"errors"
"fmt"
"net/http"
"github.com/hashicorp/go-multierror"
dockerRegistry "github.com/moby/moby/api/types/registry"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"go.podman.io/image/v5/docker"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/image"
"go.podman.io/image/v5/manifest"
"go.podman.io/image/v5/pkg/shortnames"
"go.podman.io/image/v5/types"
"go.podman.io/podman/v6/libpod"
"go.podman.io/podman/v6/pkg/api/handlers/utils"
api "go.podman.io/podman/v6/pkg/api/types"
"go.podman.io/podman/v6/pkg/auth"
)
func DistributionInspect(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
imageName := utils.GetName(r)
normalizedImageName, err := utils.NormalizeToDockerHub(r, imageName)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
if _, err := reference.ParseNormalizedNamed(normalizedImageName); err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
authConf, authfile, err := auth.GetCredentials(r)
if err != nil {
utils.Error(w, http.StatusBadRequest, err)
return
}
defer auth.RemoveAuthfile(authfile)
sys := runtime.SystemContext()
sys.AuthFilePath = authfile
sys.DockerAuthConfig = authConf
resolved, err := shortnames.Resolve(sys, normalizedImageName)
if err != nil {
utils.InternalServerError(w, err)
return
}
var merr *multierror.Error
for _, candidate := range resolved.PullCandidates {
inspect, err := inspectCandidate(r.Context(), sys, candidate.Value)
if err != nil {
merr = multierror.Append(merr, fmt.Errorf("%s: %w", candidate.Value.String(), err))
continue
}
utils.WriteResponse(w, http.StatusOK, inspect)
return
}
combinedErr := merr.ErrorOrNil()
if combinedErr == nil {
utils.InternalServerError(w, errors.New("no candidate succeeded but got no error"))
return
}
// Known compat limitation: with multiple short-name candidates, each may fail
// differently (e.g. 401 on one registry, 404 on another). The status code is
// best-effort: it comes from the earliest candidate error that carries a
// registry error code, so it may not match the first (preferred) candidate.
// The error text lists every candidate's failure.
utils.Error(w, utils.HTTPStatusFromRegistryError(combinedErr), combinedErr)
}
func inspectCandidate(ctx context.Context, sys *types.SystemContext, named reference.Named) (dockerRegistry.DistributionInspect, error) {
ref, err := docker.NewReference(named)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
src, err := ref.NewImageSource(ctx, sys)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
defer src.Close()
unparsed := image.UnparsedInstance(src, nil)
manifestBytes, manifestType, err := unparsed.Manifest(ctx)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
inspect := dockerRegistry.DistributionInspect{
Descriptor: ocispec.Descriptor{
MediaType: manifestType,
Size: int64(len(manifestBytes)),
},
}
if canonical, ok := named.(reference.Canonical); ok {
inspect.Descriptor.Digest = canonical.Digest()
} else {
digest, err := manifest.Digest(manifestBytes)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
inspect.Descriptor.Digest = digest
}
if manifest.MIMETypeIsMultiImage(manifestType) {
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
for _, d := range list.Instances() {
instance, err := list.Instance(d)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
if instance.ReadOnly.Platform != nil {
inspect.Platforms = append(inspect.Platforms, *instance.ReadOnly.Platform)
}
}
} else {
img, err := image.FromUnparsedImage(ctx, sys, unparsed)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
ociConfig, err := img.OCIConfig(ctx)
if err != nil {
return dockerRegistry.DistributionInspect{}, err
}
if ociConfig.OS != "" || ociConfig.Architecture != "" {
inspect.Platforms = append(inspect.Platforms, ociConfig.Platform)
}
}
return inspect, nil
}

View file

@ -16,6 +16,13 @@ type imageNotFound struct {
Body errorhandling.ErrorModel
}
// Failed authentication or no image found
// swagger:response
type distributionUnauthorized struct {
// in:body
Body errorhandling.ErrorModel
}
// No such file
// swagger:response
type fileNotFound struct {

View file

@ -39,6 +39,13 @@ type imageInspect struct {
Body handlers.ImageInspect
}
// Distribution Inspect
// swagger:response
type distributionInspectResponse struct {
// in:body
Body registry.DistributionInspect
}
// Image Load
// swagger:response
type imagesLoadResponseLibpod struct {

View file

@ -3,13 +3,44 @@
package server
import (
"net/http"
"github.com/gorilla/mux"
"go.podman.io/podman/v6/pkg/api/handlers/compat"
)
func (s *APIServer) registerDistributionHandlers(r *mux.Router) error {
r.HandleFunc(VersionedPath("/distribution/{name}/json"), compat.UnsupportedHandler)
// swagger:operation GET /distribution/{name}/json compat DistributionInspect
// ---
// tags:
// - distribution (compat)
// summary: Get image information from the registry
// description: |
// Return image digest and platform information by contacting the registry.
//
// If the name is a short name resolving to multiple candidates and all of them fail,
// the HTTP status code is best-effort and may not reflect the first (preferred) candidate
// in search order (e.g. a mix of 401 and 404 errors). The error message lists every
// candidate's failure.
// parameters:
// - in: path
// name: name
// type: string
// required: true
// description: the name of the image
// produces:
// - application/json
// responses:
// 200:
// $ref: "#/responses/distributionInspectResponse"
// 401:
// $ref: "#/responses/distributionUnauthorized"
// 404:
// $ref: "#/responses/imageNotFound"
// 500:
// $ref: "#/responses/internalError"
r.HandleFunc(VersionedPath("/distribution/{name:.*}/json"), s.APIHandler(compat.DistributionInspect)).Methods(http.MethodGet)
// Added non version path to URI to support docker non versioned paths
r.HandleFunc("/distribution/{name}/json", compat.UnsupportedHandler)
r.HandleFunc("/distribution/{name:.*}/json", s.APIHandler(compat.DistributionInspect)).Methods(http.MethodGet)
return nil
}

View file

@ -58,6 +58,32 @@ t GET images/$iid/json 200 \
.Id=sha256:$iid \
.RepoTags[0]=$IMAGE
# This depends on whether we're using local cache registry or real quay
expect_code=401
if [[ -n "$CI_USE_REGISTRY_CACHE" ]]; then
# local registry has no auth, so it can return 404
expect_code=404
fi
# Test versioned path (relative URL gets /vN.N/ prefix)
t GET "distribution/$IMAGE/json" 200 \
.Descriptor.digest~"sha256:[0-9a-f]\\{64\\}" \
.Descriptor.mediaType~".*"
# Test non-versioned path (absolute URL bypasses version prefix)
t GET "/distribution/$IMAGE/json" 200 \
.Descriptor.digest~"sha256:[0-9a-f]\\{64\\}" \
.Descriptor.mediaType~".*"
t GET "/distribution/NoCAPITALcharAllowed/json" 400
t GET "/distribution/quay.io/idonotexist/idonotexist:dummy/json" $expect_code
# Exercise the single-manifest path using a digest from the multi-arch test image.
single_manifest_digest=$(skopeo inspect --raw docker://$IMAGE | jq -r '.manifests[0].digest')
t GET "distribution/$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME@$single_manifest_digest/json" 200 \
.Descriptor.digest="$single_manifest_digest" \
.Descriptor.mediaType="application/vnd.oci.image.manifest.v1+json" \
.Platforms[0].architecture~".*" \
.Platforms[0].os~".*"
# Test VirtualSize field is present in API v1.43 for single image inspect (backward compatibility)
t GET /v1.43/images/$iid/json 200 \
.VirtualSize~[0-9]\\+
@ -173,12 +199,8 @@ for i in $iid ${iid:0:12} $PODMAN_TEST_IMAGE_NAME:$PODMAN_TEST_IMAGE_TAG; do
done
# compat api pull image unauthorized message error
# This depends on whether we're using local cache registry or real quay
expect_code=401
expect_msg="unauthorized: access to the requested resource is not authorized"
if [[ -n "$CI_USE_REGISTRY_CACHE" ]]; then
# local registry has no auth, so it can return 404
expect_code=404
expect_msg="manifest unknown: manifest unknown"
fi
t POST "/images/create?fromImage=quay.io/idonotexist/idonotexist:dummy" $expect_code \