package manifests import ( "bytes" "context" "encoding/json" "fmt" "github.com/sirupsen/logrus" "io" "net/http" "nf-repo/internal/interfaces" "nf-repo/internal/model" "nf-repo/internal/model/types" "nf-repo/internal/util/rerr" "sort" "strings" "sync" ) type memManifest struct { sync.RWMutex m map[string]map[string]*model.Manifest } func (m *memManifest) Referrers(ctx context.Context, repo string, target string) (*model.IndexManifest, *rerr.RepositoryError) { m.RLock() defer m.RUnlock() digestToManifestMap, repoExists := m.m[repo] if !repoExists { return nil, &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "NAME_UNKNOWN", Message: "Unknown name", } } im := &model.IndexManifest{ SchemaVersion: 2, MediaType: types.OCIImageIndex, Manifests: []model.Descriptor{}, } for digest, manifest := range digestToManifestMap { h, err := model.NewHash(digest) if err != nil { continue } var refPointer struct { Subject *model.Descriptor `json:"subject"` } json.Unmarshal(manifest.Blob, &refPointer) if refPointer.Subject == nil { continue } referenceDigest := refPointer.Subject.Digest if referenceDigest.String() != target { continue } // At this point, we know the current digest references the target var imageAsArtifact struct { Config struct { MediaType string `json:"mediaType"` } `json:"config"` } json.Unmarshal(manifest.Blob, &imageAsArtifact) im.Manifests = append(im.Manifests, model.Descriptor{ MediaType: types.MediaType(manifest.ContentType), Size: int64(len(manifest.Blob)), Digest: h, ArtifactType: imageAsArtifact.Config.MediaType, }) } return im, nil } func (m *memManifest) Tags(ctx context.Context, repo string, limit int, last int) (*model.Tag, *rerr.RepositoryError) { m.RLock() defer m.RUnlock() c, ok := m.m[repo] if !ok { return nil, &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "NAME_UNKNOWN", Message: "Unknown name", } } var tags []string for tag := range c { if !strings.Contains(tag, "sha256:") { tags = append(tags, tag) } } sort.Strings(tags) // https://github.com/opencontainers/distribution-spec/blob/b505e9cc53ec499edbd9c1be32298388921bb705/detail.md#tags-paginated // Offset using last query parameter. //if last := ctx.Query("last"); last != "" { // for i, t := range tags { // if t > last { // tags = tags[i:] // break // } // } //} // //// Limit using n query parameter. //if ns := ctx.Query("n"); ns != "" { // if n, err := strconv.Atoi(ns); err != nil { // return rerr.Error(ctx, &rerr.RepositoryError{ // Status: http.StatusBadRequest, // Code: "BAD_REQUEST", // Message: fmt.Sprintf("parsing n: %v", err), // }) // } else if n < len(tags) { // tags = tags[:n] // } //} tagsToList := &model.Tag{ Name: repo, Tags: tags, } return tagsToList, nil } func (m *memManifest) Catelog(ctx context.Context, limit, last int) (*model.Catalog, *rerr.RepositoryError) { m.RLock() defer m.RUnlock() var repos []string countRepos := 0 // TODO: implement pagination for key := range m.m { if countRepos >= limit { break } countRepos++ repos = append(repos, key) } repositoriesToList := &model.Catalog{ Repos: repos, } return repositoriesToList, nil } func (m *memManifest) Put(ctx context.Context, repo string, target string, digest string, mf *model.Manifest) *rerr.RepositoryError { // If the manifest // list's constituent manifests are already uploaded. // This isn't strictly required by the registry API, but some // registries require this. if types.MediaType(mf.ContentType).IsIndex() { if err := func() *rerr.RepositoryError { m.RLock() defer m.RUnlock() im, err := model.ParseIndexManifest(bytes.NewReader(mf.Blob)) if err != nil { return &rerr.RepositoryError{ Status: http.StatusBadRequest, Code: "MANIFEST_INVALID", Message: err.Error(), } } for _, desc := range im.Manifests { if !desc.MediaType.IsDistributable() { continue } if desc.MediaType.IsIndex() || desc.MediaType.IsImage() { if _, found := m.m[repo][desc.Digest.String()]; !found { return &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "MANIFEST_UNKNOWN", Message: fmt.Sprintf("Sub-manifest %q not found", desc.Digest), } } } else { // TODO: Probably want to do an existence check for blobs. logrus.Warnf("TODO: Check blobs for %q", desc.Digest) } } return nil }(); err != nil { return err } } m.Lock() defer m.Unlock() if _, ok := m.m[repo]; !ok { m.m[repo] = make(map[string]*model.Manifest, 2) } // Allow future references by target (tag) and immutable digest. // See https://docs.docker.com/engine/reference/commandline/pull/#pull-an-image-by-digest-immutable-identifier. m.m[repo][digest] = mf m.m[repo][target] = mf return nil } func (m *memManifest) Delete(ctx context.Context, repo string, target string) *rerr.RepositoryError { m.Lock() defer m.Unlock() if _, ok := m.m[repo]; !ok { return &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "NAME_UNKNOWN", Message: "Unknown name", } } _, ok := m.m[repo][target] if !ok { return &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "MANIFEST_UNKNOWN", Message: "Unknown manifest", } } delete(m.m[repo], target) if len(m.m[repo]) == 0 { delete(m.m, repo) } return nil } func (m *memManifest) Get(ctx context.Context, repo string, target string) (io.ReadCloser, string, *rerr.RepositoryError) { m.RLock() defer m.RUnlock() c, ok := m.m[repo] if !ok { return nil, "", &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "NAME_UNKNOWN", Message: "Unknown name", } } f, ok := c[target] if !ok { return nil, "", &rerr.RepositoryError{ Status: http.StatusNotFound, Code: "MANIFEST_UNKNOWN", Message: "Unknown manifest", } } reader := io.NopCloser(bytes.NewReader(f.Blob)) return reader, f.ContentType, nil } func NewManifestMemHandler() interfaces.ManifestHandler { return &memManifest{m: make(map[string]map[string]*model.Manifest)} }