Skip to content

Commit 0ed7c81

Browse files
committed
Fixes: Distribution API #17726
Signed-off-by: Kan Cheung <[email protected]>
1 parent 350429c commit 0ed7c81

File tree

3 files changed

+176
-2
lines changed

3 files changed

+176
-2
lines changed
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
//go:build !remote
2+
3+
package compat
4+
5+
import (
6+
"context"
7+
"errors"
8+
"fmt"
9+
"net/http"
10+
11+
"github.com/containers/image/v5/docker"
12+
"github.com/containers/image/v5/docker/reference"
13+
"github.com/containers/image/v5/image"
14+
"github.com/containers/image/v5/manifest"
15+
"github.com/containers/image/v5/types"
16+
"github.com/containers/podman/v5/libpod"
17+
"github.com/containers/podman/v5/pkg/api/handlers/utils"
18+
api "github.com/containers/podman/v5/pkg/api/types"
19+
registrytypes "github.com/docker/docker/api/types/registry"
20+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
21+
)
22+
23+
func InspectDistribution(w http.ResponseWriter, r *http.Request) {
24+
w.Header().Set("Content-Type", "application/json")
25+
26+
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
27+
28+
_, imgRef, err := parseImageReference(utils.GetName(r))
29+
if err != nil {
30+
utils.Error(w, http.StatusUnauthorized, err)
31+
return
32+
}
33+
34+
imgSrc, err := imgRef.NewImageSource(r.Context(), nil)
35+
if err != nil {
36+
var unauthErr docker.ErrUnauthorizedForCredentials
37+
if errors.As(err, &unauthErr) {
38+
utils.Error(w, http.StatusUnauthorized, errors.New("401 Unauthorized"))
39+
} else {
40+
utils.Error(w, http.StatusUnauthorized, fmt.Errorf("image not found: %w", err))
41+
}
42+
return
43+
}
44+
defer imgSrc.Close()
45+
46+
unparsedImage := image.UnparsedInstance(imgSrc, nil)
47+
manBlob, manType, err := unparsedImage.Manifest(r.Context())
48+
if err != nil {
49+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("error getting manifest: %w", err))
50+
return
51+
}
52+
img, err := image.FromUnparsedImage(r.Context(), runtime.SystemContext(), unparsedImage)
53+
if err != nil {
54+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("error getting manifest: %w", err))
55+
return
56+
}
57+
58+
digest, err := manifest.Digest(manBlob)
59+
if err != nil {
60+
utils.Error(w, http.StatusInternalServerError, fmt.Errorf("error getting manifest digest: %w", err))
61+
return
62+
}
63+
64+
distributionInspect := registrytypes.DistributionInspect{
65+
Descriptor: ocispec.Descriptor{
66+
Digest: digest,
67+
Size: int64(len(manBlob)),
68+
MediaType: manType,
69+
},
70+
}
71+
72+
platforms, err := getPlatformsFromManifest(r.Context(), img, manBlob, manType)
73+
if err != nil {
74+
utils.Error(w, http.StatusInternalServerError, err)
75+
return
76+
}
77+
distributionInspect.Platforms = platforms
78+
79+
utils.WriteResponse(w, http.StatusOK, distributionInspect)
80+
}
81+
82+
func parseImageReference(name string) (reference.Named, types.ImageReference, error) {
83+
namedRef, err := reference.ParseNormalizedNamed(name)
84+
if err != nil {
85+
return nil, nil, fmt.Errorf("not a valid image reference: %q", name)
86+
}
87+
88+
namedRef = reference.TagNameOnly(namedRef)
89+
90+
imgRef, err := docker.NewReference(namedRef)
91+
if err != nil {
92+
return nil, nil, fmt.Errorf("error creating image reference: %w", err)
93+
}
94+
95+
return namedRef, imgRef, nil
96+
}
97+
98+
func getPlatformsFromManifest(ctx context.Context, img types.Image, manBlob []byte, manType string) ([]ocispec.Platform, error) {
99+
if manType == "" {
100+
manType = manifest.GuessMIMEType(manBlob)
101+
}
102+
103+
if manifest.MIMETypeIsMultiImage(manType) {
104+
manifestList, err := manifest.ListFromBlob(manBlob, manType)
105+
if err != nil {
106+
return nil, fmt.Errorf("error parsing manifest list: %w", err)
107+
}
108+
109+
instanceDigests := manifestList.Instances()
110+
platforms := make([]ocispec.Platform, 0, len(instanceDigests))
111+
for _, digest := range instanceDigests {
112+
instance, err := manifestList.Instance(digest)
113+
if err != nil {
114+
return nil, fmt.Errorf("error getting manifest list instance: %w", err)
115+
}
116+
if instance.ReadOnly.Platform == nil {
117+
continue
118+
}
119+
platforms = append(platforms, *instance.ReadOnly.Platform)
120+
}
121+
return platforms, nil
122+
}
123+
124+
switch manType {
125+
case ocispec.MediaTypeImageManifest, manifest.DockerV2Schema2MediaType, manifest.DockerV2Schema1MediaType, manifest.DockerV2Schema1SignedMediaType:
126+
config, err := img.OCIConfig(ctx)
127+
if err != nil {
128+
var nonImgErr manifest.NonImageArtifactError
129+
if errors.As(err, &nonImgErr) {
130+
return []ocispec.Platform{}, nil
131+
}
132+
return nil, fmt.Errorf("error getting OCI config: %w", err)
133+
}
134+
return []ocispec.Platform{config.Platform}, nil
135+
}
136+
return []ocispec.Platform{}, nil
137+
}

pkg/api/server/register_distribution.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
package server
44

55
import (
6+
"net/http"
7+
68
"github.com/containers/podman/v5/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+
r.HandleFunc(VersionedPath("/distribution/{name:.*}/json"), s.APIHandler(compat.InspectDistribution)).Methods(http.MethodGet)
14+
r.HandleFunc(VersionedPath("/libpod/distribution/{name:.*}/json"), s.APIHandler(compat.InspectDistribution)).Methods(http.MethodGet)
1215
// Added non version path to URI to support docker non versioned paths
13-
r.HandleFunc("/distribution/{name}/json", compat.UnsupportedHandler)
16+
r.HandleFunc("/distribution/{name:.*}/json", s.APIHandler(compat.InspectDistribution)).Methods(http.MethodGet)
1417
return nil
1518
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import unittest
2+
3+
import requests
4+
from .fixtures import APITestCase
5+
6+
7+
class DistributionTestCase(APITestCase):
8+
def test_distribution_inspect(self):
9+
# Make sure the image exists
10+
r = requests.post(self.uri("/images/pull?reference=alpine:latest"), timeout=15)
11+
self.assertEqual(r.status_code, 200, r.text)
12+
13+
r = requests.get(self.podman_url + "/v1.40/distribution/alpine/json")
14+
self.assertEqual(r.status_code, 200, r.text)
15+
16+
result = r.json()
17+
self.assertIn("Descriptor", result)
18+
self.assertIn("Platforms", result)
19+
20+
descriptor = result["Descriptor"]
21+
self.assertIn("mediaType", descriptor)
22+
self.assertIn("digest", descriptor)
23+
self.assertIn("size", descriptor)
24+
25+
for platform in result["Platforms"]:
26+
self.assertIn("architecture", platform)
27+
self.assertIn("os", platform)
28+
29+
def test_distribution_inspect_invalid_image(self):
30+
r = requests.get(self.podman_url + "/v1.40/distribution/nonexistentimage/json")
31+
self.assertEqual(r.status_code, 401, r.text)
32+
33+
if __name__ == "__main__":
34+
unittest.main()

0 commit comments

Comments
 (0)