| // Package localregistry implements a read-only OCI Distribution / Docker |
| // V2 container image registry backed by local layers. |
| package localregistry |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "io" |
| "log" |
| "net/http" |
| "os" |
| "path/filepath" |
| "regexp" |
| "strconv" |
| |
| "github.com/bazelbuild/rules_go/go/runfiles" |
| "github.com/docker/distribution" |
| "github.com/docker/distribution/manifest/manifestlist" |
| "github.com/docker/distribution/manifest/ocischema" |
| "github.com/docker/distribution/manifest/schema2" |
| "github.com/docker/distribution/reference" |
| "github.com/opencontainers/go-digest" |
| "google.golang.org/protobuf/encoding/prototext" |
| |
| "source.monogon.dev/metropolis/pkg/localregistry/spec" |
| ) |
| |
| type Server struct { |
| manifests map[string][]byte |
| blobs map[digest.Digest]blobMeta |
| } |
| |
| type blobMeta struct { |
| filePath string |
| mediaType string |
| contentLength int64 |
| } |
| |
| func manifestDescriptorFromBazel(image *spec.Image) (manifestlist.ManifestDescriptor, error) { |
| indexPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "index.json")) |
| if err != nil { |
| return manifestlist.ManifestDescriptor{}, fmt.Errorf("while locating manifest list file: %w", err) |
| } |
| |
| manifestListRaw, err := os.ReadFile(indexPath) |
| if err != nil { |
| return manifestlist.ManifestDescriptor{}, fmt.Errorf("while opening manifest list file: %w", err) |
| } |
| |
| var imageManifestList manifestlist.ManifestList |
| if err := json.Unmarshal(manifestListRaw, &imageManifestList); err != nil { |
| return manifestlist.ManifestDescriptor{}, fmt.Errorf("while unmarshaling manifest list for %q: %w", image.Name, err) |
| } |
| |
| if len(imageManifestList.Manifests) != 1 { |
| return manifestlist.ManifestDescriptor{}, fmt.Errorf("unexpected manifest list length > 1") |
| } |
| |
| return imageManifestList.Manifests[0], nil |
| } |
| |
| func manifestFromBazel(s *Server, image *spec.Image, md manifestlist.ManifestDescriptor) (ocischema.Manifest, error) { |
| manifestPath, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", md.Digest.Algorithm().String(), md.Digest.Hex())) |
| if err != nil { |
| return ocischema.Manifest{}, fmt.Errorf("while locating manifest file: %w", err) |
| } |
| manifestRaw, err := os.ReadFile(manifestPath) |
| if err != nil { |
| return ocischema.Manifest{}, fmt.Errorf("while opening manifest file: %w", err) |
| } |
| |
| var imageManifest ocischema.Manifest |
| if err := json.Unmarshal(manifestRaw, &imageManifest); err != nil { |
| return ocischema.Manifest{}, fmt.Errorf("while unmarshaling manifest for %q: %w", image.Name, err) |
| } |
| |
| // For Digest lookups |
| s.manifests[image.Name] = manifestRaw |
| s.manifests[md.Digest.String()] = manifestRaw |
| |
| return imageManifest, nil |
| } |
| |
| func addBazelBlobFromDescriptor(s *Server, image *spec.Image, dd distribution.Descriptor) error { |
| path, err := runfiles.Rlocation(filepath.Join("_main", image.Path, "blobs", dd.Digest.Algorithm().String(), dd.Digest.Hex())) |
| if err != nil { |
| return fmt.Errorf("while locating blob: %w", err) |
| } |
| s.blobs[dd.Digest] = blobMeta{filePath: path, mediaType: dd.MediaType, contentLength: dd.Size} |
| return nil |
| } |
| |
| func FromBazelManifest(mb []byte) (*Server, error) { |
| var bazelManifest spec.Manifest |
| if err := prototext.Unmarshal(mb, &bazelManifest); err != nil { |
| log.Fatalf("failed to parse manifest: %v", err) |
| } |
| s := Server{ |
| manifests: make(map[string][]byte), |
| blobs: make(map[digest.Digest]blobMeta), |
| } |
| for _, i := range bazelManifest.Images { |
| md, err := manifestDescriptorFromBazel(i) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := addBazelBlobFromDescriptor(&s, i, md.Descriptor); err != nil { |
| return nil, err |
| } |
| |
| m, err := manifestFromBazel(&s, i, md) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err := addBazelBlobFromDescriptor(&s, i, m.Config); err != nil { |
| return nil, err |
| } |
| for _, l := range m.Layers { |
| if err := addBazelBlobFromDescriptor(&s, i, l); err != nil { |
| return nil, err |
| } |
| } |
| } |
| return &s, nil |
| } |
| |
| var ( |
| versionCheckEp = regexp.MustCompile(`^/v2/$`) |
| manifestEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/manifests/(" + reference.TagRegexp.String() + "|" + digest.DigestRegexp.String() + ")$") |
| blobEp = regexp.MustCompile("^/v2/(" + reference.NameRegexp.String() + ")/blobs/(" + digest.DigestRegexp.String() + ")$") |
| ) |
| |
| func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { |
| if req.Method != http.MethodGet && req.Method != http.MethodHead { |
| w.WriteHeader(http.StatusMethodNotAllowed) |
| fmt.Fprintf(w, "Registry is read-only, only GET and HEAD are allowed\n") |
| return |
| } |
| w.Header().Set("Content-Type", "application/json") |
| if versionCheckEp.MatchString(req.URL.Path) { |
| w.WriteHeader(http.StatusOK) |
| fmt.Fprintf(w, "{}") |
| return |
| } else if matches := manifestEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 { |
| name := matches[1] |
| manifest, ok := s.manifests[name] |
| if !ok { |
| w.WriteHeader(http.StatusNotFound) |
| fmt.Fprintf(w, "Image not found") |
| return |
| } |
| w.Header().Set("Content-Type", schema2.MediaTypeManifest) |
| w.Header().Set("Content-Length", strconv.FormatInt(int64(len(manifest)), 10)) |
| w.WriteHeader(http.StatusOK) |
| io.Copy(w, bytes.NewReader(manifest)) |
| } else if matches := blobEp.FindStringSubmatch(req.URL.Path); len(matches) > 0 { |
| bm, ok := s.blobs[digest.Digest(matches[2])] |
| if !ok { |
| w.WriteHeader(http.StatusNotFound) |
| fmt.Fprintf(w, "Blob not found") |
| return |
| } |
| w.Header().Set("Content-Type", bm.mediaType) |
| w.Header().Set("Content-Length", strconv.FormatInt(bm.contentLength, 10)) |
| http.ServeFile(w, req, bm.filePath) |
| } else { |
| w.WriteHeader(http.StatusNotFound) |
| } |
| } |