Skip to content
Open
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
156 changes: 156 additions & 0 deletions pkg/api/handlers/compat/distribution.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//go:build !remote && (linux || freebsd)

package compat

import (
"context"
"errors"
"fmt"
"net/http"

"github.com/docker/distribution/registry/api/errcode"
"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
}

var registryErrCode errcode.ErrorCoder
if errors.As(combinedErr, &registryErrCode) {
switch registryErrCode.ErrorCode().Descriptor().HTTPStatusCode {
case http.StatusUnauthorized:
utils.Error(w, http.StatusUnauthorized, combinedErr)
case http.StatusNotFound:
utils.Error(w, http.StatusNotFound, combinedErr)
default:
utils.InternalServerError(w, combinedErr)
}
return
}
utils.InternalServerError(w, 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
}
7 changes: 7 additions & 0 deletions pkg/api/handlers/swagger/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions pkg/api/handlers/swagger/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"github.com/moby/moby/api/types/container"
dockerImage "github.com/moby/moby/api/types/image"
"github.com/moby/moby/api/types/network"
dockerRegistry "github.com/moby/moby/api/types/registry"
"github.com/moby/moby/api/types/volume"
"go.podman.io/common/libnetwork/types"
"go.podman.io/image/v5/manifest"
Expand Down Expand Up @@ -38,6 +39,13 @@ type imageInspect struct {
Body handlers.ImageInspect
}

// Distribution Inspect
// swagger:response
type distributionInspectResponse struct {
// in:body
Body dockerRegistry.DistributionInspect
}

// Image Load
// swagger:response
type imagesLoadResponseLibpod struct {
Expand Down
29 changes: 27 additions & 2 deletions pkg/api/server/register_distribution.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,38 @@
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.
// parameters:
// - in: path
// name: name
// type: string
// required: true
// description: the name of the image
// produces:
// - application/json
// responses:
// 200:
// $ref: "#/responses/distributionInspectResponse"
// 401:
Comment thread
Honny1 marked this conversation as resolved.
// $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
}
30 changes: 26 additions & 4 deletions test/apiv2/10-images.at
Original file line number Diff line number Diff line change
Expand Up @@ -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~".*"
Comment thread
mtrmac marked this conversation as resolved.
t GET "/distribution/NoCAPITALcharAllowed/json" 400
t GET "/distribution/quay.io/idonotexist/idonotexist:dummy/json" $expect_code

Comment thread
Honny1 marked this conversation as resolved.
# 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]\\+
Expand Down Expand Up @@ -151,12 +177,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 \
Expand Down