Skip to content

Commit 03b2ea2

Browse files
committed
api: implement compat distribution inspect endpoint
Signed-off-by: Aaron Ang <aaron.angyd@gmail.com>
1 parent 12c6562 commit 03b2ea2

5 files changed

Lines changed: 208 additions & 2 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
//go:build !remote
2+
3+
package compat
4+
5+
import (
6+
"context"
7+
"errors"
8+
"net/http"
9+
10+
"github.com/containers/podman/v6/libpod"
11+
"github.com/containers/podman/v6/pkg/api/handlers/utils"
12+
api "github.com/containers/podman/v6/pkg/api/types"
13+
"github.com/containers/podman/v6/pkg/auth"
14+
"github.com/docker/distribution/registry/api/errcode"
15+
"github.com/hashicorp/go-multierror"
16+
dockerRegistry "github.com/moby/moby/api/types/registry"
17+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
18+
"go.podman.io/image/v5/docker"
19+
"go.podman.io/image/v5/docker/reference"
20+
"go.podman.io/image/v5/image"
21+
"go.podman.io/image/v5/manifest"
22+
"go.podman.io/image/v5/pkg/shortnames"
23+
"go.podman.io/image/v5/types"
24+
)
25+
26+
func DistributionInspect(w http.ResponseWriter, r *http.Request) {
27+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
28+
imageName := utils.GetName(r)
29+
30+
named, err := reference.ParseNormalizedNamed(imageName)
31+
if err != nil {
32+
utils.Error(w, http.StatusBadRequest, err)
33+
return
34+
}
35+
36+
authConf, authfile, err := auth.GetCredentials(r)
37+
if err != nil {
38+
utils.Error(w, http.StatusBadRequest, err)
39+
return
40+
}
41+
defer auth.RemoveAuthfile(authfile)
42+
43+
sys := runtime.SystemContext()
44+
sys.AuthFilePath = authfile
45+
sys.DockerAuthConfig = authConf
46+
47+
resolved, err := shortnames.Resolve(sys, named.String())
48+
if err != nil {
49+
utils.InternalServerError(w, err)
50+
return
51+
}
52+
53+
var merr *multierror.Error
54+
55+
for _, candidate := range resolved.PullCandidates {
56+
inspect, err := inspectCandidate(r.Context(), sys, candidate.Value)
57+
if err != nil {
58+
merr = multierror.Append(merr, err)
59+
continue
60+
}
61+
62+
utils.WriteResponse(w, http.StatusOK, inspect)
63+
return
64+
}
65+
66+
if merr.ErrorOrNil() == nil {
67+
utils.InternalServerError(w, errors.New("no candidate succeeded but got no error"))
68+
return
69+
}
70+
71+
lastErr := merr.Errors[len(merr.Errors)-1]
72+
var registryErrCode errcode.ErrorCoder
73+
if errors.As(lastErr, &registryErrCode) {
74+
switch registryErrCode.ErrorCode().Descriptor().HTTPStatusCode {
75+
case http.StatusUnauthorized, http.StatusNotFound:
76+
utils.Error(w, http.StatusUnauthorized, lastErr)
77+
default:
78+
utils.InternalServerError(w, lastErr)
79+
}
80+
return
81+
}
82+
utils.InternalServerError(w, lastErr)
83+
}
84+
85+
func inspectCandidate(ctx context.Context, sys *types.SystemContext, named reference.Named) (dockerRegistry.DistributionInspect, error) {
86+
ref, err := docker.NewReference(named)
87+
if err != nil {
88+
return dockerRegistry.DistributionInspect{}, err
89+
}
90+
91+
src, err := ref.NewImageSource(ctx, sys)
92+
if err != nil {
93+
return dockerRegistry.DistributionInspect{}, err
94+
}
95+
defer src.Close()
96+
97+
unparsed := image.UnparsedInstance(src, nil)
98+
manifestBytes, manifestType, err := unparsed.Manifest(ctx)
99+
if err != nil {
100+
return dockerRegistry.DistributionInspect{}, err
101+
}
102+
103+
inspect := dockerRegistry.DistributionInspect{
104+
Descriptor: ocispec.Descriptor{
105+
MediaType: manifestType,
106+
Size: int64(len(manifestBytes)),
107+
},
108+
}
109+
110+
if canonical, ok := named.(reference.Canonical); ok {
111+
inspect.Descriptor.Digest = canonical.Digest()
112+
} else {
113+
digest, err := manifest.Digest(manifestBytes)
114+
if err != nil {
115+
return dockerRegistry.DistributionInspect{}, err
116+
}
117+
inspect.Descriptor.Digest = digest
118+
}
119+
120+
if manifest.MIMETypeIsMultiImage(manifestType) {
121+
list, err := manifest.ListFromBlob(manifestBytes, manifestType)
122+
if err != nil {
123+
return dockerRegistry.DistributionInspect{}, err
124+
}
125+
for _, d := range list.Instances() {
126+
instance, err := list.Instance(d)
127+
if err != nil {
128+
return dockerRegistry.DistributionInspect{}, err
129+
}
130+
if instance.ReadOnly.Platform != nil {
131+
inspect.Platforms = append(inspect.Platforms, *instance.ReadOnly.Platform)
132+
}
133+
}
134+
} else {
135+
img, err := image.FromUnparsedImage(ctx, sys, unparsed)
136+
if err != nil {
137+
return dockerRegistry.DistributionInspect{}, err
138+
}
139+
ociConfig, err := img.OCIConfig(ctx)
140+
if err != nil {
141+
return dockerRegistry.DistributionInspect{}, err
142+
}
143+
if ociConfig.OS != "" || ociConfig.Architecture != "" {
144+
inspect.Platforms = append(inspect.Platforms, ociConfig.Platform)
145+
}
146+
}
147+
148+
return inspect, nil
149+
}

pkg/api/handlers/swagger/errors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,13 @@ type imageNotFound struct {
1616
Body errorhandling.ErrorModel
1717
}
1818

19+
// Failed authentication or no image found
20+
// swagger:response
21+
type distributionUnauthorized struct {
22+
// in:body
23+
Body errorhandling.ErrorModel
24+
}
25+
1926
// No such file
2027
// swagger:response
2128
type fileNotFound struct {

pkg/api/handlers/swagger/responses.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/moby/moby/api/types/container"
1313
dockerImage "github.com/moby/moby/api/types/image"
1414
"github.com/moby/moby/api/types/network"
15+
dockerRegistry "github.com/moby/moby/api/types/registry"
1516
"github.com/moby/moby/api/types/volume"
1617
"go.podman.io/common/libnetwork/types"
1718
"go.podman.io/image/v5/manifest"
@@ -38,6 +39,13 @@ type imageInspect struct {
3839
Body handlers.ImageInspect
3940
}
4041

42+
// Distribution Inspect
43+
// swagger:response
44+
type distributionInspectResponse struct {
45+
// in:body
46+
Body dockerRegistry.DistributionInspect
47+
}
48+
4149
// Image Load
4250
// swagger:response
4351
type imagesLoadResponseLibpod struct {

pkg/api/server/register_distribution.go

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,36 @@
33
package server
44

55
import (
6+
"net/http"
7+
68
"github.com/containers/podman/v6/pkg/api/handlers/compat"
79
"github.com/gorilla/mux"
810
)
911

1012
func (s *APIServer) registerDistributionHandlers(r *mux.Router) error {
11-
r.HandleFunc(VersionedPath("/distribution/{name}/json"), compat.UnsupportedHandler)
13+
// swagger:operation GET /distribution/{name}/json compat DistributionInspect
14+
// ---
15+
// tags:
16+
// - distribution (compat)
17+
// summary: Get image information from the registry
18+
// description: Return image digest and platform information by contacting the registry.
19+
// parameters:
20+
// - in: path
21+
// name: name
22+
// type: string
23+
// required: true
24+
// description: the name of the image
25+
// produces:
26+
// - application/json
27+
// responses:
28+
// 200:
29+
// $ref: "#/responses/distributionInspectResponse"
30+
// 401:
31+
// $ref: "#/responses/distributionUnauthorized"
32+
// 500:
33+
// $ref: "#/responses/internalError"
34+
r.HandleFunc(VersionedPath("/distribution/{name:.*}/json"), s.APIHandler(compat.DistributionInspect)).Methods(http.MethodGet)
1235
// Added non version path to URI to support docker non versioned paths
13-
r.HandleFunc("/distribution/{name}/json", compat.UnsupportedHandler)
36+
r.HandleFunc("/distribution/{name:.*}/json", s.APIHandler(compat.DistributionInspect)).Methods(http.MethodGet)
1437
return nil
1538
}

test/apiv2/10-images.at

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,25 @@ t GET images/$iid/json 200 \
5858
.Id=sha256:$iid \
5959
.RepoTags[0]=$IMAGE
6060

61+
# Test versioned path (relative URL gets /vN.N/ prefix)
62+
t GET "distribution/$IMAGE/json" 200 \
63+
.Descriptor.digest~"sha256:[0-9a-f]\\{64\\}" \
64+
.Descriptor.mediaType~".*"
65+
# Test non-versioned path (absolute URL bypasses version prefix)
66+
t GET "/distribution/$IMAGE/json" 200 \
67+
.Descriptor.digest~"sha256:[0-9a-f]\\{64\\}" \
68+
.Descriptor.mediaType~".*"
69+
t GET "/distribution/NoCAPITALcharAllowed/json" 400
70+
t GET "/distribution/quay.io/idonotexist/idonotexist:dummy/json" 401
71+
72+
# Exercise the single-manifest path using a digest from the multi-arch test image.
73+
single_manifest_digest=$(skopeo inspect --raw docker://$IMAGE | jq -r '.manifests[0].digest')
74+
t GET "distribution/$PODMAN_TEST_IMAGE_REGISTRY/$PODMAN_TEST_IMAGE_USER/$PODMAN_TEST_IMAGE_NAME@$single_manifest_digest/json" 200 \
75+
.Descriptor.digest="$single_manifest_digest" \
76+
.Descriptor.mediaType="application/vnd.oci.image.manifest.v1+json" \
77+
.Platforms[0].architecture~".*" \
78+
.Platforms[0].os~".*"
79+
6180
# Test VirtualSize field is present in API v1.43 for single image inspect (backward compatibility)
6281
t GET /v1.43/images/$iid/json 200 \
6382
.VirtualSize~[0-9]\\+

0 commit comments

Comments
 (0)