blob: aaf0d993b182d6691aeb7c329ca40d8671be8bb2 [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
26 "source.monogon.dev/metropolis/pkg/localregistry/spec"
27)
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
85func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) {
86 path := filepath.Join(image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex())
87 s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
88}
89
90func FromBazelManifest(mb []byte) (*Server, error) {
91 var bazelManifest spec.Manifest
92 if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
Lorenz Brun901c7322023-07-13 20:10:37 +020093 log.Fatalf("failed to parse manifest: %v", err)
94 }
95 s := Server{
96 manifests: make(map[string][]byte),
97 blobs: make(map[digest.Digest]blobMeta),
98 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010099 for _, i := range bazelManifest.Images {
100 md, err := manifestDescriptorFromBazel(i)
Lorenz Brun901c7322023-07-13 20:10:37 +0200101 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100102 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +0200103 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100104
105 addBazelBlobFromDescriptor(&s, i, md.Descriptor)
106
107 m, err := manifestFromBazel(&s, i, md)
Lorenz Brun901c7322023-07-13 20:10:37 +0200108 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100109 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +0200110 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100111
112 addBazelBlobFromDescriptor(&s, i, m.Config)
113 for _, l := range m.Layers {
114 addBazelBlobFromDescriptor(&s, i, l)
115 }
Lorenz Brun901c7322023-07-13 20:10:37 +0200116 }
117 return &s, nil
118}
119
120var (
121 versionCheckEp = regexp.MustCompile(`^/v2/$`)
122 manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
123 blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
124)
125
126func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
127 if req.Method != http.MethodGet && req.Method != http.MethodHead {
128 w.WriteHeader(http.StatusMethodNotAllowed)
129 fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
130 return
131 }
132 w.Header().Set("Content-Type", "application/json")
133 if versionCheckEp.MatchString(req.URL.Path) {
134 w.WriteHeader(http.StatusOK)
135 fmt.Fprintf(w, "{}")
136 return
137 } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
138 name := matches[1]
139 manifest, ok := s.manifests[name]
140 if !ok {
141 w.WriteHeader(http.StatusNotFound)
142 fmt.Fprintf(w, "Image not found")
143 return
144 }
145 w.Header().Set("Content-Type", schema2.MediaTypeManifest)
146 w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
147 w.WriteHeader(http.StatusOK)
148 io.Copy(w, bytes.NewReader(manifest))
149 } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
150 bm, ok := s.blobs[digest.Digest(matches[2])]
151 if !ok {
152 w.WriteHeader(http.StatusNotFound)
153 fmt.Fprintf(w, "Blob not found")
154 return
155 }
156 w.Header().Set("Content-Type", bm.mediaType)
157 w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
158 http.ServeFile(w, req, bm.filePath)
159 } else {
160 w.WriteHeader(http.StatusNotFound)
161 }
162}