blob: 1f648e010967c793c5fd371e42b287cb51221c54 [file] [log] [blame]
Tim Windelschmidt6d33a432025-02-04 14:34:25 +01001// Copyright The Monogon Project Authors.
2// SPDX-License-Identifier: Apache-2.0
3
Lorenz Brun901c7322023-07-13 20:10:37 +02004// Package localregistry implements a read-only OCI Distribution / Docker
5// V2 container image registry backed by local layers.
6package localregistry
7
8import (
9 "bytes"
10 "encoding/json"
11 "fmt"
12 "io"
13 "log"
14 "net/http"
15 "os"
Tim Windelschmidt0974b222024-01-16 14:04:15 +010016 "path/filepath"
Lorenz Brun901c7322023-07-13 20:10:37 +020017 "regexp"
18 "strconv"
19
Serge Bazanskif779b8f2024-04-17 16:30:55 +020020 "github.com/bazelbuild/rules_go/go/runfiles"
Lorenz Brun901c7322023-07-13 20:10:37 +020021 "github.com/docker/distribution"
Tim Windelschmidt0974b222024-01-16 14:04:15 +010022 "github.com/docker/distribution/manifest/manifestlist"
23 "github.com/docker/distribution/manifest/ocischema"
Lorenz Brun901c7322023-07-13 20:10:37 +020024 "github.com/docker/distribution/manifest/schema2"
25 "github.com/docker/distribution/reference"
26 "github.com/opencontainers/go-digest"
27 "google.golang.org/protobuf/encoding/prototext"
28
Tim Windelschmidt9f21f532024-05-07 15:14:20 +020029 "source.monogon.dev/metropolis/test/localregistry/spec"
Lorenz Brun901c7322023-07-13 20:10:37 +020030)
31
32type Server struct {
33 manifests map[string][]byte
34 blobs map[digest.Digest]blobMeta
35}
36
37type blobMeta struct {
38 filePath string
39 mediaType string
40 contentLength int64
41}
42
Tim Windelschmidt0974b222024-01-16 14:04:15 +010043func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) {
Serge Bazanskif779b8f2024-04-17 16:30:55 +020044 indexPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "index.json"))
45 if err != nil {
46 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while locating manifest list file: %w", err)
47 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010048
49 manifestListRaw, err := os.ReadFile(indexPath)
Lorenz Brun901c7322023-07-13 20:10:37 +020050 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +010051 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err)
Lorenz Brun901c7322023-07-13 20:10:37 +020052 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010053
54 var imageManifestList manifestlist.ManifestList
55 if err := json.Unmarshal(manifestListRaw, &imageManifestList); err != nil {
56 return manifestlist.ManifestDescriptor{}, fmt.Errorf("while unmarshaling manifest list for %q: %w", image.Name, err)
Lorenz Brun901c7322023-07-13 20:10:37 +020057 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010058
59 if len(imageManifestList.Manifests) != 1 {
60 return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1")
61 }
62
63 return imageManifestList.Manifests[0], nil
Lorenz Brun901c7322023-07-13 20:10:37 +020064}
65
Tim Windelschmidt0974b222024-01-16 14:04:15 +010066func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) {
Serge Bazanskif779b8f2024-04-17 16:30:55 +020067 manifestPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex()))
68 if err != nil {
69 return ocischema.Manifest{}, fmt.Errorf("while locating manifest file: %w", err)
70 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010071 manifestRaw, err := os.ReadFile(manifestPath)
72 if err != nil {
73 return ocischema.Manifest{}, fmt.Errorf("while opening manifest file: %w", err)
74 }
75
76 var imageManifest ocischema.Manifest
77 if err := json.Unmarshal(manifestRaw, &imageManifest); err != nil {
78 return ocischema.Manifest{}, fmt.Errorf("while unmarshaling manifest for %q: %w", image.Name, err)
79 }
80
81 // For Digest lookups
82 s.manifests[image.Name] = manifestRaw
83 s.manifests[md.Digest.String()] = manifestRaw
84
85 return imageManifest, nil
86}
87
Serge Bazanski6ea57622024-04-17 21:02:32 +020088func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) error {
89 path, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex()))
90 if err != nil {
91 return fmt.Errorf("while locating blob: %w", err)
92 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +010093 s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size}
Serge Bazanski6ea57622024-04-17 21:02:32 +020094 return nil
Tim Windelschmidt0974b222024-01-16 14:04:15 +010095}
96
97func FromBazelManifest(mb []byte) (*Server, error) {
98 var bazelManifest spec.Manifest
99 if err := prototext.Unmarshal(mb, &bazelManifest); err != nil {
Lorenz Brun901c7322023-07-13 20:10:37 +0200100 log.Fatalf("failed to parse manifest: %v", err)
101 }
102 s := Server{
103 manifests: make(map[string][]byte),
104 blobs: make(map[digest.Digest]blobMeta),
105 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100106 for _, i := range bazelManifest.Images {
107 md, err := manifestDescriptorFromBazel(i)
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
Serge Bazanski6ea57622024-04-17 21:02:32 +0200112 if err := addBazelBlobFromDescriptor(&s, i, md.Descriptor); err != nil {
113 return nil, err
114 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100115
116 m, err := manifestFromBazel(&s, i, md)
Lorenz Brun901c7322023-07-13 20:10:37 +0200117 if err != nil {
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100118 return nil, err
Lorenz Brun901c7322023-07-13 20:10:37 +0200119 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100120
Serge Bazanski6ea57622024-04-17 21:02:32 +0200121 if err := addBazelBlobFromDescriptor(&s, i, m.Config); err != nil {
122 return nil, err
123 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100124 for _, l := range m.Layers {
Serge Bazanski6ea57622024-04-17 21:02:32 +0200125 if err := addBazelBlobFromDescriptor(&s, i, l); err != nil {
126 return nil, err
127 }
Tim Windelschmidt0974b222024-01-16 14:04:15 +0100128 }
Lorenz Brun901c7322023-07-13 20:10:37 +0200129 }
130 return &s, nil
131}
132
133var (
134 versionCheckEp = regexp.MustCompile(`^/v2/$`)
135 manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
136 blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
137)
138
139func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
140 if req.Method != http.MethodGet && req.Method != http.MethodHead {
141 w.WriteHeader(http.StatusMethodNotAllowed)
142 fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
143 return
144 }
145 w.Header().Set("Content-Type", "application/json")
146 if versionCheckEp.MatchString(req.URL.Path) {
147 w.WriteHeader(http.StatusOK)
148 fmt.Fprintf(w, "{}")
149 return
150 } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
151 name := matches[1]
152 manifest, ok := s.manifests[name]
153 if !ok {
154 w.WriteHeader(http.StatusNotFound)
155 fmt.Fprintf(w, "Image not found")
156 return
157 }
158 w.Header().Set("Content-Type", schema2.MediaTypeManifest)
159 w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
160 w.WriteHeader(http.StatusOK)
161 io.Copy(w, bytes.NewReader(manifest))
162 } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
163 bm, ok := s.blobs[digest.Digest(matches[2])]
164 if !ok {
165 w.WriteHeader(http.StatusNotFound)
166 fmt.Fprintf(w, "Blob not found")
167 return
168 }
169 w.Header().Set("Content-Type", bm.mediaType)
170 w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
171 http.ServeFile(w, req, bm.filePath)
172 } else {
173 w.WriteHeader(http.StatusNotFound)
174 }
175}