blob: e40fb1bb7bb3e94531fa426e53684b8de6ba38ef [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"
13 "regexp"
14 "strconv"
15
16 "github.com/docker/distribution"
17 "github.com/docker/distribution/manifest/schema2"
18 "github.com/docker/distribution/reference"
19 "github.com/opencontainers/go-digest"
20 "google.golang.org/protobuf/encoding/prototext"
21
22 "source.monogon.dev/metropolis/pkg/localregistry/spec"
23)
24
25type Server struct {
26 manifests map[string][]byte
27 blobs map[digest.Digest]blobMeta
28}
29
30type blobMeta struct {
31 filePath string
32 mediaType string
33 contentLength int64
34}
35
36func blobFromBazel(s *Server, bd *spec.BlobDescriptor, mediaType string) (distribution.Descriptor, error) {
37 digestRaw, err := os.ReadFile(bd.DigestPath)
38 if err != nil {
39 return distribution.Descriptor{}, fmt.Errorf("while opening digest file: %w", err)
40 }
41 stat, err := os.Stat(bd.FilePath)
42 if err != nil {
43 return distribution.Descriptor{}, fmt.Errorf("while stat'ing blob file: %w", err)
44 }
45 digest := digest.Digest("sha256:" + string(digestRaw))
46 s.blobs[digest] = blobMeta{filePath: bd.FilePath, mediaType: mediaType, contentLength: stat.Size()}
47 return distribution.Descriptor{
48 MediaType: mediaType,
49 Size: stat.Size(),
50 Digest: digest,
51 }, nil
52}
53
54func FromBazelManifest(m []byte) (*Server, error) {
55 var manifest spec.Manifest
56 if err := prototext.Unmarshal(m, &manifest); err != nil {
57 log.Fatalf("failed to parse manifest: %v", err)
58 }
59 s := Server{
60 manifests: make(map[string][]byte),
61 blobs: make(map[digest.Digest]blobMeta),
62 }
63 for _, i := range manifest.Images {
64 imageManifest := schema2.Manifest{
65 Versioned: schema2.SchemaVersion,
66 }
67 var err error
68 imageManifest.Config, err = blobFromBazel(&s, i.Config, schema2.MediaTypeImageConfig)
69 if err != nil {
70 return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
71 }
72 for _, l := range i.Layers {
73 ml, err := blobFromBazel(&s, l, schema2.MediaTypeLayer)
74 if err != nil {
75 return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
76 }
77 imageManifest.Layers = append(imageManifest.Layers, ml)
78 }
79 s.manifests[i.Name], err = json.Marshal(imageManifest)
80 if err != nil {
81 return nil, fmt.Errorf("while marshaling image %q manifest: %w", i.Name, err)
82 }
83 // For Digest lookups
84 s.manifests[string(digest.Canonical.FromBytes(s.manifests[i.Name]))] = s.manifests[i.Name]
85 }
86 return &s, nil
87}
88
89var (
90 versionCheckEp = regexp.MustCompile(`^/v2/$`)
91 manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$")
92 blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$")
93)
94
95func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) {
96 if req.Method != http.MethodGet && req.Method != http.MethodHead {
97 w.WriteHeader(http.StatusMethodNotAllowed)
98 fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n")
99 return
100 }
101 w.Header().Set("Content-Type", "application/json")
102 if versionCheckEp.MatchString(req.URL.Path) {
103 w.WriteHeader(http.StatusOK)
104 fmt.Fprintf(w, "{}")
105 return
106 } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
107 name := matches[1]
108 manifest, ok := s.manifests[name]
109 if !ok {
110 w.WriteHeader(http.StatusNotFound)
111 fmt.Fprintf(w, "Image not found")
112 return
113 }
114 w.Header().Set("Content-Type", schema2.MediaTypeManifest)
115 w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10))
116 w.WriteHeader(http.StatusOK)
117 io.Copy(w, bytes.NewReader(manifest))
118 } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 {
119 bm, ok := s.blobs[digest.Digest(matches[2])]
120 if !ok {
121 w.WriteHeader(http.StatusNotFound)
122 fmt.Fprintf(w, "Blob not found")
123 return
124 }
125 w.Header().Set("Content-Type", bm.mediaType)
126 w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10))
127 http.ServeFile(w, req, bm.filePath)
128 } else {
129 w.WriteHeader(http.StatusNotFound)
130 }
131}