blob: e40fb1bb7bb3e94531fa426e53684b8de6ba38ef [file] [log] [blame]
// 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"
"regexp"
"strconv"
"github.com/docker/distribution"
"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 blobFromBazel(s *Server, bd *spec.BlobDescriptor, mediaType string) (distribution.Descriptor, error) {
digestRaw, err := os.ReadFile(bd.DigestPath)
if err != nil {
return distribution.Descriptor{}, fmt.Errorf("while opening digest file: %w", err)
}
stat, err := os.Stat(bd.FilePath)
if err != nil {
return distribution.Descriptor{}, fmt.Errorf("while stat'ing blob file: %w", err)
}
digest := digest.Digest("sha256:" + string(digestRaw))
s.blobs[digest] = blobMeta{filePath: bd.FilePath, mediaType: mediaType, contentLength: stat.Size()}
return distribution.Descriptor{
MediaType: mediaType,
Size: stat.Size(),
Digest: digest,
}, nil
}
func FromBazelManifest(m []byte) (*Server, error) {
var manifest spec.Manifest
if err := prototext.Unmarshal(m, &manifest); 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 manifest.Images {
imageManifest := schema2.Manifest{
Versioned: schema2.SchemaVersion,
}
var err error
imageManifest.Config, err = blobFromBazel(&s, i.Config, schema2.MediaTypeImageConfig)
if err != nil {
return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
}
for _, l := range i.Layers {
ml, err := blobFromBazel(&s, l, schema2.MediaTypeLayer)
if err != nil {
return nil, fmt.Errorf("while creating blob spec for %q: %w", i.Name, err)
}
imageManifest.Layers = append(imageManifest.Layers, ml)
}
s.manifests[i.Name], err = json.Marshal(imageManifest)
if err != nil {
return nil, fmt.Errorf("while marshaling image %q manifest: %w", i.Name, err)
}
// For Digest lookups
s.manifests[string(digest.Canonical.FromBytes(s.manifests[i.Name]))] = s.manifests[i.Name]
}
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)
}
}