Skip to content

Commit df2ff09

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

4 files changed

Lines changed: 258 additions & 2 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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+
sys, cleanupAuth, err := getDistributionSystemContext(runtime, r)
43+
if err != nil {
44+
utils.Error(w, http.StatusBadRequest, err)
45+
return
46+
}
47+
defer cleanupAuth()
48+
49+
inspect, err := inspectDistributionReference(r.Context(), sys, normalizedImageName)
50+
if err != nil {
51+
handleDistributionInspectError(w, err)
52+
return
53+
}
54+
55+
utils.WriteResponse(w, http.StatusOK, inspect)
56+
}
57+
58+
func getDistributionSystemContext(runtime *libpod.Runtime, r *http.Request) (*types.SystemContext, func(), error) {
59+
authConf, authfile, err := auth.GetCredentials(r)
60+
if err != nil {
61+
return nil, nil, err
62+
}
63+
cleanupAuth := func() {
64+
auth.RemoveAuthfile(authfile)
65+
}
66+
67+
sys := runtime.SystemContext()
68+
sys.AuthFilePath = authfile
69+
sys.DockerAuthConfig = authConf
70+
if err := utils.PossiblyEnforceDockerHub(r, sys); err != nil {
71+
cleanupAuth()
72+
return nil, nil, err
73+
}
74+
75+
return sys, cleanupAuth, nil
76+
}
77+
78+
func inspectDistributionReference(ctx context.Context, sys *types.SystemContext, imageName string) (dockerRegistry.DistributionInspect, error) {
79+
resolved, err := shortnames.Resolve(sys, imageName)
80+
if err != nil {
81+
return dockerRegistry.DistributionInspect{}, err
82+
}
83+
84+
var latestErr error
85+
appendErr := func(e error) {
86+
if latestErr == nil {
87+
latestErr = e
88+
return
89+
}
90+
latestErr = fmt.Errorf("tried %v: %w", e, latestErr)
91+
}
92+
93+
for _, candidate := range resolved.PullCandidates {
94+
ref, err := alltransports.ParseImageName("docker://" + candidate.Value.String())
95+
if err != nil {
96+
appendErr(err)
97+
continue
98+
}
99+
100+
src, err := ref.NewImageSource(ctx, sys)
101+
if err != nil {
102+
appendErr(err)
103+
continue
104+
}
105+
106+
manifestBytes, manifestType, err := image.UnparsedInstance(src, nil).Manifest(ctx)
107+
if err != nil {
108+
_ = src.Close()
109+
appendErr(err)
110+
continue
111+
}
112+
113+
inspect, err := distributionInspectFromManifest(ctx, src, candidate.Value, manifestBytes, manifestType)
114+
_ = src.Close()
115+
if err != nil {
116+
appendErr(err)
117+
continue
118+
}
119+
return inspect, nil
120+
}
121+
122+
if latestErr == nil {
123+
return dockerRegistry.DistributionInspect{}, errors.New("failed to inspect distribution")
124+
}
125+
return dockerRegistry.DistributionInspect{}, latestErr
126+
}
127+
128+
func handleDistributionInspectError(w http.ResponseWriter, err error) {
129+
var registryErrCode errcode.ErrorCoder
130+
if errors.As(err, &registryErrCode) {
131+
switch registryErrCode.ErrorCode().Descriptor().HTTPStatusCode {
132+
case http.StatusUnauthorized, http.StatusNotFound:
133+
utils.Error(w, http.StatusUnauthorized, err)
134+
default:
135+
utils.InternalServerError(w, err)
136+
}
137+
return
138+
}
139+
utils.InternalServerError(w, err)
140+
}
141+
142+
func distributionInspectFromManifest(ctx context.Context, src types.ImageSource, named reference.Named, manifestBytes []byte, manifestType string) (dockerRegistry.DistributionInspect, error) {
143+
inspect := dockerRegistry.DistributionInspect{
144+
Descriptor: ocispec.Descriptor{
145+
MediaType: manifestType,
146+
Size: int64(len(manifestBytes)),
147+
},
148+
}
149+
150+
if canonical, ok := named.(reference.Canonical); ok {
151+
inspect.Descriptor.Digest = canonical.Digest()
152+
} else {
153+
digest, err := manifest.Digest(manifestBytes)
154+
if err != nil {
155+
return dockerRegistry.DistributionInspect{}, err
156+
}
157+
inspect.Descriptor.Digest = digest
158+
}
159+
160+
switch manifest.NormalizedMIMEType(manifestType) {
161+
case manifest.DockerV2ListMediaType:
162+
list, err := manifest.Schema2ListFromManifest(manifestBytes)
163+
if err != nil {
164+
return dockerRegistry.DistributionInspect{}, err
165+
}
166+
for _, m := range list.Manifests {
167+
inspect.Platforms = append(inspect.Platforms, ocispec.Platform{
168+
Architecture: m.Platform.Architecture,
169+
OS: m.Platform.OS,
170+
OSVersion: m.Platform.OSVersion,
171+
OSFeatures: m.Platform.OSFeatures,
172+
Variant: m.Platform.Variant,
173+
})
174+
}
175+
case ocispec.MediaTypeImageIndex:
176+
index, err := manifest.OCI1IndexFromManifest(manifestBytes)
177+
if err != nil {
178+
return dockerRegistry.DistributionInspect{}, err
179+
}
180+
for _, m := range index.Manifests {
181+
if m.Platform != nil {
182+
inspect.Platforms = append(inspect.Platforms, *m.Platform)
183+
}
184+
}
185+
case manifest.DockerV2Schema2MediaType:
186+
schema2Manifest, err := manifest.Schema2FromManifest(manifestBytes)
187+
if err != nil {
188+
return dockerRegistry.DistributionInspect{}, err
189+
}
190+
maybeAddPlatformFromConfig(ctx, src, manifest.BlobInfoFromSchema2Descriptor(schema2Manifest.ConfigDescriptor), &inspect.Platforms)
191+
case ocispec.MediaTypeImageManifest:
192+
ociManifest, err := manifest.OCI1FromManifest(manifestBytes)
193+
if err != nil {
194+
return dockerRegistry.DistributionInspect{}, err
195+
}
196+
maybeAddPlatformFromConfig(ctx, src, manifest.BlobInfoFromOCI1Descriptor(ociManifest.Config), &inspect.Platforms)
197+
}
198+
199+
return inspect, nil
200+
}
201+
202+
func maybeAddPlatformFromConfig(ctx context.Context, src types.ImageSource, blobInfo types.BlobInfo, platforms *[]ocispec.Platform) {
203+
reader, _, err := src.GetBlob(ctx, blobInfo, none.NoCache)
204+
if err != nil {
205+
return
206+
}
207+
defer reader.Close()
208+
209+
configJSON, err := io.ReadAll(reader)
210+
if err != nil {
211+
return
212+
}
213+
214+
var platform ocispec.Platform
215+
if err := json.Unmarshal(configJSON, &platform); err != nil {
216+
return
217+
}
218+
if platform.OS != "" || platform.Architecture != "" {
219+
*platforms = append(*platforms, platform)
220+
}
221+
}

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)