blob: 885259b868aaa37f9a91b2f95d7edd0224870e7e [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
17 "github.com/docker/distribution"
Tim Windelschmidt0974b222024-01-16 14:04:15 +010018 "github.com/docker/distribution/manifest/manifestlist"
19 "github.com/docker/distribution/manifest/ocischema"
Lorenz Brun901c7322023-07-13 20:10:37 +020020 "github.com/docker/distribution/manifest/schema2"
21 "github.com/docker/distribution/reference"
22 "github.com/opencontainers/go-digest"
23 "google.golang.org/protobuf/encoding/prototext"
24
25 "source.monogon.dev/metropolis/pkg/localregistry/spec"
26)
27
28type Server struct {
29 manifests map[string][]byte
30 blobs map[digest.Digest]blobMeta
31}
32
33type blobMeta struct {
34 filePath string
35 mediaType string
36 contentLength int64
37}
38
Tim Windelschmidt0974b222024-01-16 14:04:15 +010039func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) {
40 indexPath := filepath.Join(image.Path, "index.json")
41
42 manifestListRaw, err := os.ReadFile(indexPath)
Lorenz Brun901c7322023-07-13 20:10:37 +020043 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +010044 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err)
Lorenz Brun901c7322023-07-13 20:10:37 +020045 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010046
47 var imageManifestList manifestlist.ManifestList
48 if err := json.Unmarshal(manifestListRaw, &imageManifestList); err != nil {
49 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while unmarshaling manifest list for %q: %w", image.Name, err)
Lorenz Brun901c7322023-07-13 20:10:37 +020050 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010051
52 if len(imageManifestList.Manifests) != 1 {
53 return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1")
54 }
55
56 return imageManifestList.Manifests[0], nil
Lorenz Brun901c7322023-07-13 20:10:37 +020057}
58
Tim Windelschmidt0974b222024-01-16 14:04:15 +010059func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) {
60 manifestPath := filepath.Join(image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex())
61 manifestRaw, err := os.ReadFile(manifestPath)
62 if err != nil {
63 return ocischema.Manifest{}, fmt.Errorf("while opening manifest file: %w", err)
64 }
65
66 var imageManifest ocischema.Manifest
67 if err := json.Unmarshal(manifestRaw, &imageManifest); err != nil {
68 return ocischema.Manifest{}, fmt.Errorf("while unmarshaling manifest for %q: %w", image.Name, err)
69 }
70
71 // For Digest lookups
72 s.manifests[image.Name] = manifestRaw
73 s.manifests[md.Digest.String()] = manifestRaw
74
75 return imageManifest, nil
76}
77
78func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) {
79 path := filepath.Join(image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex())
80 s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
81}
82
83func FromBazelManifest(mb []byte) (*Server, error) {
84 var bazelManifest spec.Manifest
85 if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
Lorenz Brun901c7322023-07-13 20:10:37 +020086 log.Fatalf("failed to parse manifest: %v", err)
87 }
88 s := Server{
89 manifests: make(map[string][]byte),
90 blobs: make(map[digest.Digest]blobMeta),
91 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010092 for _, i := range bazelManifest.Images {
93 md, err := manifestDescriptorFromBazel(i)
Lorenz Brun901c7322023-07-13 20:10:37 +020094 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +010095 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +020096 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010097
98 addBazelBlobFromDescriptor(&s, i, md.Descriptor)
99
100 m, err := manifestFromBazel(&s, i, md)
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, m.Config)
106 for _, l := range m.Layers {
107 addBazelBlobFromDescriptor(&s, i, l)
108 }
Lorenz Brun901c7322023-07-13 20:10:37 +0200109 }
110 return &s, nil
111}
112
113var (
114 versionCheckEp = regexp.MustCompile(`^/v2/$`)
115 manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
116 blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
117)
118
119func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
120 if req.Method != http.MethodGet && req.Method != http.MethodHead {
121 w.WriteHeader(http.StatusMethodNotAllowed)
122 fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
123 return
124 }
125 w.Header().Set("Content-Type", "application/json")
126 if versionCheckEp.MatchString(req.URL.Path) {
127 w.WriteHeader(http.StatusOK)
128 fmt.Fprintf(w, "{}")
129 return
130 } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
131 name := matches[1]
132 manifest, ok := s.manifests[name]
133 if !ok {
134 w.WriteHeader(http.StatusNotFound)
135 fmt.Fprintf(w, "Image not found")
136 return
137 }
138 w.Header().Set("Content-Type", schema2.MediaTypeManifest)
139 w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
140 w.WriteHeader(http.StatusOK)
141 io.Copy(w, bytes.NewReader(manifest))
142 } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
143 bm, ok := s.blobs[digest.Digest(matches[2])]
144 if !ok {
145 w.WriteHeader(http.StatusNotFound)
146 fmt.Fprintf(w, "Blob not found")
147 return
148 }
149 w.Header().Set("Content-Type", bm.mediaType)
150 w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
151 http.ServeFile(w, req, bm.filePath)
152 } else {
153 w.WriteHeader(http.StatusNotFound)
154 }
155}