mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-10-11 08:27:36 +00:00
Merge pull request #28358 from aaron-ang/issue-17726-distribution-api
api: implement compat distribution inspect endpoint
This commit is contained in:
commit
253e24f65e
5 changed files with 221 additions and 6 deletions
148
pkg/api/handlers/compat/distribution.go
Normal file
148
pkg/api/handlers/compat/distribution.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue