Skip to content

Commit 12e4664

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

4 files changed

Lines changed: 237 additions & 2 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
//go:build !remote
2+
3+
package compat
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"errors"
9+
"fmt"
10+
"io"
11+
"net/http"
12+
13+
"github.com/containers/podman/v6/libpod"
14+
"github.com/containers/podman/v6/pkg/api/handlers/utils"
15+
api "github.com/containers/podman/v6/pkg/api/types"
16+
"github.com/containers/podman/v6/pkg/auth"
17+
"github.com/docker/distribution/registry/api/errcode"
18+
dockerRegistry "github.com/moby/moby/api/types/registry"
19+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
20+
"go.podman.io/image/v5/docker/reference"
21+
"go.podman.io/image/v5/image"
22+
"go.podman.io/image/v5/manifest"
23+
"go.podman.io/image/v5/pkg/blobinfocache/none"
24+
"go.podman.io/image/v5/pkg/shortnames"
25+
"go.podman.io/image/v5/transports/alltransports"
26+
"go.podman.io/image/v5/types"
27+
)
28+
29+
func DistributionInspect(w http.ResponseWriter, r *http.Request) {
30+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
31+
32+
normalizedImageName, err := utils.NormalizeToDockerHub(r, utils.GetName(r))
33+
if err != nil {
34+
utils.InternalServerError(w, fmt.Errorf("normalizing image: %w", err))
35+
return
36+
}
37+
if _, err := reference.ParseNormalizedNamed(normalizedImageName); err != nil {
38+
utils.Error(w, http.StatusBadRequest, err)
39+
return
40+
}
41+
42+
authConf, authfile, err := auth.GetCredentials(r)
43+
if err != nil {
44+
utils.Error(w, http.StatusBadRequest, err)
45+
return
46+
}
47+
defer auth.RemoveAuthfile(authfile)
48+
49+
sys := runtime.SystemContext()
50+
sys.AuthFilePath = authfile
51+
sys.DockerAuthConfig = authConf
52+
if err := utils.PossiblyEnforceDockerHub(r, sys); err != nil {
53+
utils.InternalServerError(w, err)
54+
return
55+
}
56+
57+
resolved, err := shortnames.Resolve(sys, normalizedImageName)
58+
if err != nil {
59+
utils.InternalServerError(w, err)
60+
return
61+
}
62+
63+
var latestErr error
64+
appendErr := func(e error) {
65+
if latestErr == nil {
66+
latestErr = e
67+
return
68+
}
69+
latestErr = fmt.Errorf("tried %v: %w", e, latestErr)
70+
}
71+
72+
for _, candidate := range resolved.PullCandidates {
73+
ref, err := alltransports.ParseImageName("docker://" + candidate.Value.String())
74+
if err != nil {
75+
appendErr(err)
76+
continue
77+
}
78+
79+
src, err := ref.NewImageSource(r.Context(), sys)
80+
if err != nil {
81+
appendErr(err)
82+
continue
83+
}
84+
85+
manifestBytes, manifestType, err := image.UnparsedInstance(src, nil).Manifest(r.Context())
86+
if err != nil {
87+
_ = src.Close()
88+
appendErr(err)
89+
continue
90+
}
91+
92+
inspect, err := distributionInspectFromManifest(r.Context(), src, candidate.Value, manifestBytes, manifestType)
93+
_ = src.Close()
94+
if err != nil {
95+
appendErr(err)
96+
continue
97+
}
98+
99+
utils.WriteResponse(w, http.StatusOK, inspect)
100+
return
101+
}
102+
103+
if latestErr == nil {
104+
utils.InternalServerError(w, errors.New("failed to inspect distribution"))
105+
return
106+
}
107+
108+
var registryErrCode errcode.ErrorCoder
109+
if errors.As(latestErr, &registryErrCode) {
110+
switch registryErrCode.ErrorCode().Descriptor().HTTPStatusCode {
111+
case http.StatusUnauthorized, http.StatusNotFound:
112+
utils.Error(w, http.StatusUnauthorized, latestErr)
113+
default:
114+
utils.InternalServerError(w, latestErr)
115+
}
116+
return
117+
}
118+
utils.InternalServerError(w, latestErr)
119+
}
120+
121+
func distributionInspectFromManifest(ctx context.Context, src types.ImageSource, named reference.Named, manifestBytes []byte, manifestType string) (dockerRegistry.DistributionInspect, error) {
122+
inspect := dockerRegistry.DistributionInspect{
123+
Descriptor: ocispec.Descriptor{
124+
MediaType: manifestType,
125+
Size: int64(len(manifestBytes)),
126+
},
127+
}
128+
129+
if canonical, ok := named.(reference.Canonical); ok {
130+
inspect.Descriptor.Digest = canonical.Digest()
131+
} else {
132+
digest, err := manifest.Digest(manifestBytes)
133+
if err != nil {
134+
return dockerRegistry.DistributionInspect{}, err
135+
}
136+
inspect.Descriptor.Digest = digest
137+
}
138+
139+
switch manifest.NormalizedMIMEType(manifestType) {
140+
case manifest.DockerV2ListMediaType:
141+
list, err := manifest.Schema2ListFromManifest(manifestBytes)
142+
if err != nil {
143+
return dockerRegistry.DistributionInspect{}, err
144+
}
145+
for _, m := range list.Manifests {
146+
inspect.Platforms = append(inspect.Platforms, ocispec.Platform{
147+
Architecture: m.Platform.Architecture,
148+
OS: m.Platform.OS,
149+
OSVersion: m.Platform.OSVersion,
150+
OSFeatures: m.Platform.OSFeatures,
151+
Variant: m.Platform.Variant,
152+
})
153+
}
154+
case ocispec.MediaTypeImageIndex:
155+
index, err := manifest.OCI1IndexFromManifest(manifestBytes)
156+
if err != nil {
157+
return dockerRegistry.DistributionInspect{}, err
158+
}
159+
for _, m := range index.Manifests {
160+
if m.Platform != nil {
161+
inspect.Platforms = append(inspect.Platforms, *m.Platform)
162+
}
163+
}
164+
case manifest.DockerV2Schema2MediaType:
165+
schema2Manifest, err := manifest.Schema2FromManifest(manifestBytes)
166+
if err != nil {
167+
return dockerRegistry.DistributionInspect{}, err
168+
}
169+
maybeAddPlatformFromConfig(ctx, src, manifest.BlobInfoFromSchema2Descriptor(schema2Manifest.ConfigDescriptor), &inspect.Platforms)
170+
case ocispec.MediaTypeImageManifest:
171+
ociManifest, err := manifest.OCI1FromManifest(manifestBytes)
172+
if err != nil {
173+
return dockerRegistry.DistributionInspect{}, err
174+
}
175+
maybeAddPlatformFromConfig(ctx, src, manifest.BlobInfoFromOCI1Descriptor(ociManifest.Config), &inspect.Platforms)
176+
}
177+
178+
return inspect, nil
179+
}
180+
181+
func maybeAddPlatformFromConfig(ctx context.Context, src types.ImageSource, blobInfo types.BlobInfo, platforms *[]ocispec.Platform) {
182+
reader, _, err := src.GetBlob(ctx, blobInfo, none.NoCache)
183+
if err != nil {
184+
return
185+
}
186+
defer reader.Close()
187+
188+
configJSON, err := io.ReadAll(reader)
189+
if err != nil {
190+
return
191+
}
192+
193+
var platform ocispec.Platform
194+
if err := json.Unmarshal(configJSON, &platform); err != nil {
195+
return
196+
}
197+
if platform.OS != "" || platform.Architecture != "" {
198+
*platforms = append(*platforms, platform)
199+
}
200+
}

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/imageNotFound"
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: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ t GET images/$iid/json 200 \
5858
.Id=sha256:$iid \
5959
.RepoTags[0]=$IMAGE
6060

61+
t GET "distribution/$IMAGE/json" 200 \
62+
.Descriptor.digest~"sha256:[0-9a-f]\\{64\\}" \
63+
.Descriptor.mediaType~".*"
64+
6165
# Test VirtualSize field is present in API v1.43 for single image inspect (backward compatibility)
6266
t GET /v1.43/images/$iid/json 200 \
6367
.VirtualSize~[0-9]\\+

0 commit comments

Comments
 (0)