blob: 120eb616731dcd8a60a96cd2061973752b29f7ea [file] [log] [blame]
Lorenz Brun901c7322023-07-13 20:10:37 +02001// Package localregistry implements a read-only OCI Distribution / Docker
2// V2 container image registry backed by local layers.
3package localregistry
4
5import (
6 "bytes"
7 "encoding/json"
8 "fmt"
9 "io"
10 "log"
11 "net/http"
12 "os"
Tim Windelschmidt0974b222024-01-16 14:04:15 +010013 "path/filepath"
Lorenz Brun901c7322023-07-13 20:10:37 +020014 "regexp"
15 "strconv"
16
Serge Bazanskif779b8f2024-04-17 16:30:55 +020017 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brun901c7322023-07-13 20:10:37 +020018 "github.com/docker/distribution"
Tim Windelschmidt0974b222024-01-16 14:04:15 +010019 "github.com/docker/distribution/manifest/manifestlist"
20 "github.com/docker/distribution/manifest/ocischema"
Lorenz Brun901c7322023-07-13 20:10:37 +020021 "github.com/docker/distribution/manifest/schema2"
22 "github.com/docker/distribution/reference"
23 "github.com/opencontainers/go-digest"
24 "google.golang.org/protobuf/encoding/prototext"
25
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020026 "source.monogon.dev/metropolis/test/localregistry/spec"
Lorenz Brun901c7322023-07-13 20:10:37 +020027)
28
29type Server struct {
30 manifests map[string][]byte
31 blobs map[digest.Digest]blobMeta
32}
33
34type blobMeta struct {
35 filePath string
36 mediaType string
37 contentLength int64
38}
39
Tim Windelschmidt0974b222024-01-16 14:04:15 +010040func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) {
Serge Bazanskif779b8f2024-04-17 16:30:55 +020041 indexPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "index.json"))
42 if err != nil {
43 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while locating manifest list file: %w", err)
44 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010045
46 manifestListRaw, err := os.ReadFile(indexPath)
Lorenz Brun901c7322023-07-13 20:10:37 +020047 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +010048 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err)
Lorenz Brun901c7322023-07-13 20:10:37 +020049 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010050
51 var imageManifestList manifestlist.ManifestList
52 if err := json.Unmarshal(manifestListRaw, &imageManifestList); err != nil {
53 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while unmarshaling manifest list for %q: %w", image.Name, err)
Lorenz Brun901c7322023-07-13 20:10:37 +020054 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010055
56 if len(imageManifestList.Manifests) != 1 {
57 return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1")
58 }
59
60 return imageManifestList.Manifests[0], nil
Lorenz Brun901c7322023-07-13 20:10:37 +020061}
62
Tim Windelschmidt0974b222024-01-16 14:04:15 +010063func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) {
Serge Bazanskif779b8f2024-04-17 16:30:55 +020064 manifestPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex()))
65 if err != nil {
66 return ocischema.Manifest{}, fmt.Errorf("while locating manifest file: %w", err)
67 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010068 manifestRaw, err := os.ReadFile(manifestPath)
69 if err != nil {
70 return ocischema.Manifest{}, fmt.Errorf("while opening manifest file: %w", err)
71 }
72
73 var imageManifest ocischema.Manifest
74 if err := json.Unmarshal(manifestRaw, &imageManifest); err != nil {
75 return ocischema.Manifest{}, fmt.Errorf("while unmarshaling manifest for %q: %w", image.Name, err)
76 }
77
78 // For Digest lookups
79 s.manifests[image.Name] = manifestRaw
80 s.manifests[md.Digest.String()] = manifestRaw
81
82 return imageManifest, nil
83}
84
Serge Bazanski6ea57622024-04-17 21:02:32 +020085func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) error {
86 path, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex()))
87 if err != nil {
88 return fmt.Errorf("while locating blob: %w", err)
89 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010090 s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
Serge Bazanski6ea57622024-04-17 21:02:32 +020091 return nil
Tim Windelschmidt0974b222024-01-16 14:04:15 +010092}
93
94func FromBazelManifest(mb []byte) (*Server, error) {
95 var bazelManifest spec.Manifest
96 if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
Lorenz Brun901c7322023-07-13 20:10:37 +020097 log.Fatalf("failed to parse manifest: %v", err)
98 }
99 s := Server{
100 manifests: make(map[string][]byte),
101 blobs: make(map[digest.Digest]blobMeta),
102 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100103 for _, i := range bazelManifest.Images {
104 md, err := manifestDescriptorFromBazel(i)
Lorenz Brun901c7322023-07-13 20:10:37 +0200105 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100106 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +0200107 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100108
Serge Bazanski6ea57622024-04-17 21:02:32 +0200109 if err := addBazelBlobFromDescriptor(&s, i, md.Descriptor); err != nil {
110 return nil, err
111 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100112
113 m, err := manifestFromBazel(&s, i, md)
Lorenz Brun901c7322023-07-13 20:10:37 +0200114 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100115 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +0200116 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100117
Serge Bazanski6ea57622024-04-17 21:02:32 +0200118 if err := addBazelBlobFromDescriptor(&s, i, m.Config); err != nil {
119 return nil, err
120 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100121 for _, l := range m.Layers {
Serge Bazanski6ea57622024-04-17 21:02:32 +0200122 if err := addBazelBlobFromDescriptor(&s, i, l); err != nil {
123 return nil, err
124 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100125 }
Lorenz Brun901c7322023-07-13 20:10:37 +0200126 }
127 return &s, nil
128}
129
130var (
131 versionCheckEp = regexp.MustCompile(`^/v2/$`)
132 manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
133 blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
134)
135
136func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
137 if req.Method != http.MethodGet && req.Method != http.MethodHead {
138 w.WriteHeader(http.StatusMethodNotAllowed)
139 fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
140 return
141 }
142 w.Header().Set("Content-Type", "application/json")
143 if versionCheckEp.MatchString(req.URL.Path) {
144 w.WriteHeader(http.StatusOK)
145 fmt.Fprintf(w, "{}")
146 return
147 } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
148 name := matches[1]
149 manifest, ok := s.manifests[name]
150 if !ok {
151 w.WriteHeader(http.StatusNotFound)
152 fmt.Fprintf(w, "Image not found")
153 return
154 }
155 w.Header().Set("Content-Type", schema2.MediaTypeManifest)
156 w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
157 w.WriteHeader(http.StatusOK)
158 io.Copy(w, bytes.NewReader(manifest))
159 } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
160 bm, ok := s.blobs[digest.Digest(matches[2])]
161 if !ok {
162 w.WriteHeader(http.StatusNotFound)
163 fmt.Fprintf(w, "Blob not found")
164 return
165 }
166 w.Header().Set("Content-Type", bm.mediaType)
167 w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
168 http.ServeFile(w, req, bm.filePath)
169 } else {
170 w.WriteHeader(http.StatusNotFound)
171 }
172}