mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
008527b7bb
GC and scrub should not stop if a manifest or index is missing from storage. Other similar changes are also included. WRT metadb, the missing manifests cannot be added, and the results returned from metadb do not include the descriptors for these manifests. Signed-off-by: Andrei Aaron <andreifdaaron@gmail.com>
1134 lines
32 KiB
Go
1134 lines
32 KiB
Go
package storage
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math/rand"
|
|
"path"
|
|
"strings"
|
|
"time"
|
|
|
|
dockerList "github.com/distribution/distribution/v3/manifest/manifestlist"
|
|
docker "github.com/distribution/distribution/v3/manifest/schema2"
|
|
"github.com/distribution/distribution/v3/registry/storage/driver"
|
|
godigest "github.com/opencontainers/go-digest"
|
|
"github.com/opencontainers/image-spec/schema"
|
|
imeta "github.com/opencontainers/image-spec/specs-go"
|
|
ispec "github.com/opencontainers/image-spec/specs-go/v1"
|
|
"github.com/santhosh-tekuri/jsonschema/v5"
|
|
|
|
zerr "zotregistry.dev/zot/v2/errors"
|
|
zcommon "zotregistry.dev/zot/v2/pkg/common"
|
|
"zotregistry.dev/zot/v2/pkg/compat"
|
|
"zotregistry.dev/zot/v2/pkg/extensions/monitoring"
|
|
zlog "zotregistry.dev/zot/v2/pkg/log"
|
|
"zotregistry.dev/zot/v2/pkg/scheduler"
|
|
storageConstants "zotregistry.dev/zot/v2/pkg/storage/constants"
|
|
storageTypes "zotregistry.dev/zot/v2/pkg/storage/types"
|
|
)
|
|
|
|
const (
|
|
manifestWithEmptyLayersErrMsg = "layers/minItems: minimum 1 items required, but found 0 items"
|
|
cosignSignatureTagSuffix = "sig"
|
|
)
|
|
|
|
func GetTagsByIndex(index ispec.Index) []string {
|
|
tags := make([]string, 0)
|
|
|
|
for _, manifest := range index.Manifests {
|
|
v, ok := manifest.Annotations[ispec.AnnotationRefName]
|
|
if ok {
|
|
tags = append(tags, v)
|
|
}
|
|
}
|
|
|
|
return tags
|
|
}
|
|
|
|
func GetManifestDescByReference(index ispec.Index, reference string) (ispec.Descriptor, bool) {
|
|
var manifestDesc ispec.Descriptor
|
|
|
|
for _, manifest := range index.Manifests {
|
|
if reference == manifest.Digest.String() {
|
|
return manifest, true
|
|
}
|
|
|
|
v, ok := manifest.Annotations[ispec.AnnotationRefName]
|
|
if ok && v == reference {
|
|
return manifest, true
|
|
}
|
|
}
|
|
|
|
return manifestDesc, false
|
|
}
|
|
|
|
func ValidateManifest(imgStore storageTypes.ImageStore, repo, reference, mediaType string, body []byte,
|
|
compats []compat.MediaCompatibility, log zlog.Logger,
|
|
) error {
|
|
// validate the manifest
|
|
if !IsSupportedMediaType(compats, mediaType) {
|
|
log.Debug().Interface("actual", mediaType).
|
|
Msg("bad manifest media type")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
if len(body) == 0 {
|
|
log.Debug().Int("len", len(body)).Msg("invalid body length")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
switch mediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
var manifest ispec.Manifest
|
|
|
|
// validate manifest
|
|
if err := ValidateManifestSchema(body); err != nil {
|
|
log.Error().Err(err).Msg("failed to validate OCIv1 image manifest schema")
|
|
|
|
return zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error())
|
|
}
|
|
|
|
if err := json.Unmarshal(body, &manifest); err != nil {
|
|
log.Error().Err(err).Msg("failed to unmarshal JSON")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
// validate blobs only for known media types
|
|
if manifest.Config.MediaType == ispec.MediaTypeImageConfig ||
|
|
manifest.Config.MediaType == ispec.MediaTypeEmptyJSON {
|
|
// validate config blob - a lightweight check if the blob is present
|
|
ok, _, _, err := imgStore.StatBlob(repo, manifest.Config.Digest)
|
|
if !ok || err != nil {
|
|
log.Error().Err(err).Str("digest", manifest.Config.Digest.String()).
|
|
Msg("failed to stat blob due to missing config blob")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
// validate layers - a lightweight check if the blob is present
|
|
for _, layer := range manifest.Layers {
|
|
if IsNonDistributable(layer.MediaType) {
|
|
log.Debug().Str("digest", layer.Digest.String()).Str("mediaType", layer.MediaType).
|
|
Msg("skip checking non-distributable layer exists")
|
|
|
|
continue
|
|
}
|
|
|
|
ok, _, _, err := imgStore.StatBlob(repo, layer.Digest)
|
|
if !ok || err != nil {
|
|
log.Error().Err(err).Str("digest", layer.Digest.String()).
|
|
Msg("failed to validate manifest due to missing layer blob")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
}
|
|
}
|
|
case ispec.MediaTypeImageIndex:
|
|
// validate manifest
|
|
if err := ValidateImageIndexSchema(body); err != nil {
|
|
log.Error().Err(err).Msg("failed to validate OCIv1 image index manifest schema")
|
|
|
|
return zerr.NewError(zerr.ErrBadManifest).AddDetail("jsonSchemaValidation", err.Error())
|
|
}
|
|
|
|
var indexManifest ispec.Index
|
|
if err := json.Unmarshal(body, &indexManifest); err != nil {
|
|
log.Error().Err(err).Msg("failed to unmarshal JSON")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
for _, manifest := range indexManifest.Manifests {
|
|
if ok, _, _, err := imgStore.StatBlob(repo, manifest.Digest); !ok || err != nil {
|
|
log.Error().Err(err).Str("digest", manifest.Digest.String()).
|
|
Msg("failed to stat manifest due to missing manifest blob")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
}
|
|
default:
|
|
// non-OCI compatible
|
|
descriptors, err := compat.Validate(body, mediaType)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("failed to unmarshal JSON")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
|
|
for _, desc := range descriptors {
|
|
if ok, _, _, err := imgStore.StatBlob(repo, desc.Digest); !ok || err != nil {
|
|
log.Error().Err(err).Str("digest", desc.Digest.String()).
|
|
Msg("failed to stat non-OCI descriptor due to missing blob")
|
|
|
|
return zerr.ErrBadManifest
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Returns the canonical digest or the digest provided by the reference if any
|
|
// Per spec, the canonical digest would always be returned to the client in
|
|
// request headers, but that does not make sense if the client requested a different digest algorithm
|
|
// See https://github.com/opencontainers/distribution-spec/issues/494
|
|
func GetAndValidateRequestDigest(body []byte, reference string, log zlog.Logger) (
|
|
godigest.Digest, error,
|
|
) {
|
|
expectedDigest, err := godigest.Parse(reference)
|
|
if err != nil {
|
|
// This is a non-digest reference
|
|
return godigest.Canonical.FromBytes(body), err
|
|
}
|
|
|
|
actualDigest := expectedDigest.Algorithm().FromBytes(body)
|
|
|
|
if expectedDigest.String() != actualDigest.String() {
|
|
log.Error().Str("actual", actualDigest.String()).Str("expected", expectedDigest.String()).
|
|
Msg("failed to validate manifest digest")
|
|
|
|
return actualDigest, zerr.ErrBadManifest
|
|
}
|
|
|
|
return actualDigest, nil
|
|
}
|
|
|
|
/*
|
|
CheckIfIndexNeedsUpdate verifies if an index needs to be updated given a new manifest descriptor.
|
|
|
|
Returns whether or not index needs update, in the latter case it will also return the previous digest.
|
|
*/
|
|
func CheckIfIndexNeedsUpdate(index *ispec.Index, desc *ispec.Descriptor,
|
|
log zlog.Logger,
|
|
) (bool, godigest.Digest, error) {
|
|
var oldDgst godigest.Digest
|
|
|
|
var reference string
|
|
|
|
tag, ok := desc.Annotations[ispec.AnnotationRefName]
|
|
if ok {
|
|
reference = tag
|
|
} else {
|
|
reference = desc.Digest.String()
|
|
}
|
|
|
|
updateIndex := true
|
|
|
|
for midx, manifest := range index.Manifests {
|
|
manifest := manifest
|
|
if reference == manifest.Digest.String() {
|
|
// nothing changed, so don't update
|
|
updateIndex = false
|
|
|
|
break
|
|
}
|
|
|
|
v, ok := manifest.Annotations[ispec.AnnotationRefName]
|
|
if ok && v == reference {
|
|
if manifest.Digest.String() == desc.Digest.String() {
|
|
// nothing changed, so don't update
|
|
updateIndex = false
|
|
|
|
break
|
|
}
|
|
|
|
// manifest contents have changed for the same tag,
|
|
// so update index.json descriptor
|
|
log.Info().
|
|
Int64("old size", manifest.Size).
|
|
Int64("new size", desc.Size).
|
|
Str("old digest", manifest.Digest.String()).
|
|
Str("new digest", desc.Digest.String()).
|
|
Str("old mediaType", manifest.MediaType).
|
|
Str("new mediaType", desc.MediaType).
|
|
Msg("updating existing tag with new manifest contents")
|
|
|
|
// changing media-type is disallowed!
|
|
if manifest.MediaType != desc.MediaType {
|
|
log.Info().
|
|
Str("old mediaType", manifest.MediaType).
|
|
Str("new mediaType", desc.MediaType).Msg("media-type changed")
|
|
}
|
|
|
|
oldDesc := *desc
|
|
|
|
desc = &manifest
|
|
oldDgst = manifest.Digest
|
|
desc.Size = oldDesc.Size
|
|
desc.Digest = oldDesc.Digest
|
|
|
|
index.Manifests = append(index.Manifests[:midx], index.Manifests[midx+1:]...)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
return updateIndex, oldDgst, nil
|
|
}
|
|
|
|
// GetIndex returns the contents of index.json.
|
|
func GetIndex(imgStore storageTypes.ImageStore, repo string, log zlog.Logger) (ispec.Index, error) {
|
|
var index ispec.Index
|
|
|
|
buf, err := imgStore.GetIndexContent(repo)
|
|
if err != nil {
|
|
if errors.As(err, &driver.PathNotFoundError{}) {
|
|
return index, zerr.ErrRepoNotFound
|
|
}
|
|
|
|
return index, err
|
|
}
|
|
|
|
if err := json.Unmarshal(buf, &index); err != nil {
|
|
log.Error().Err(err).Str("dir", path.Join(imgStore.RootDir(), repo)).Msg("invalid JSON")
|
|
|
|
return index, zerr.ErrRepoBadVersion
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
// GetImageIndex returns a multiarch type image.
|
|
func GetImageIndex(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, log zlog.Logger,
|
|
) (ispec.Index, error) {
|
|
var imageIndex ispec.Index
|
|
|
|
if err := digest.Validate(); err != nil {
|
|
return imageIndex, err
|
|
}
|
|
|
|
buf, err := imgStore.GetBlobContent(repo, digest)
|
|
if err != nil {
|
|
return imageIndex, err
|
|
}
|
|
|
|
indexPath := path.Join(imgStore.RootDir(), repo, "blobs",
|
|
digest.Algorithm().String(), digest.Encoded())
|
|
|
|
if err := json.Unmarshal(buf, &imageIndex); err != nil {
|
|
log.Error().Err(err).Str("path", indexPath).Msg("invalid JSON")
|
|
|
|
return imageIndex, err
|
|
}
|
|
|
|
return imageIndex, nil
|
|
}
|
|
|
|
func GetImageManifest(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, log zlog.Logger,
|
|
) (ispec.Manifest, error) {
|
|
var manifestContent ispec.Manifest
|
|
|
|
manifestBlob, err := imgStore.GetBlobContent(repo, digest)
|
|
if err != nil {
|
|
return manifestContent, err
|
|
}
|
|
|
|
manifestPath := path.Join(imgStore.RootDir(), repo, "blobs",
|
|
digest.Algorithm().String(), digest.Encoded())
|
|
|
|
if err := json.Unmarshal(manifestBlob, &manifestContent); err != nil {
|
|
log.Error().Err(err).Str("path", manifestPath).Msg("invalid JSON")
|
|
|
|
return manifestContent, err
|
|
}
|
|
|
|
return manifestContent, nil
|
|
}
|
|
|
|
func RemoveManifestDescByReference(index *ispec.Index, reference string, detectCollisions bool,
|
|
) (ispec.Descriptor, error) {
|
|
var removedManifest ispec.Descriptor
|
|
|
|
var found, foundAsTag bool // keep track if the manifest was found by digest or by tag
|
|
manifestDigestCounts := map[godigest.Digest]int{} // Keep track of the number of references for a specific digest
|
|
|
|
foundCount := 0
|
|
|
|
var outIndex ispec.Index
|
|
|
|
for _, manifest := range index.Manifests {
|
|
manifestDigestCounts[manifest.Digest]++
|
|
|
|
tag, ok := manifest.Annotations[ispec.AnnotationRefName]
|
|
if ok && tag == reference {
|
|
removedManifest = manifest
|
|
found = true
|
|
foundAsTag = true
|
|
foundCount++
|
|
|
|
continue
|
|
} else if reference == manifest.Digest.String() {
|
|
removedManifest = manifest
|
|
found = true
|
|
foundCount++
|
|
|
|
continue
|
|
}
|
|
|
|
outIndex.Manifests = append(outIndex.Manifests, manifest)
|
|
}
|
|
|
|
if foundCount > 1 && detectCollisions {
|
|
return ispec.Descriptor{}, zerr.ErrManifestConflict
|
|
} else if !found {
|
|
return ispec.Descriptor{}, zerr.ErrManifestNotFound
|
|
}
|
|
|
|
// In case of delete by digest we remove the manifest right away from storage
|
|
// but in case of delete by tag we want to only remove the tag
|
|
// and handle the manifest based on retention rules later.
|
|
// If there are more than one tags with the same digest we want to keep the others.
|
|
// If and only if there are no other tags except the one we want to remove
|
|
// we need to add a new descriptor without a tag name to keep track of
|
|
// the manifest in index.json so it remains accessible by digest.
|
|
if foundAsTag && manifestDigestCounts[removedManifest.Digest] == 1 {
|
|
newManifest := removedManifest
|
|
delete(newManifest.Annotations, ispec.AnnotationRefName)
|
|
outIndex.Manifests = append(outIndex.Manifests, newManifest)
|
|
}
|
|
|
|
index.Manifests = outIndex.Manifests
|
|
|
|
return removedManifest, nil
|
|
}
|
|
|
|
/*
|
|
Unmarshal an image index and for all manifests in that
|
|
index, ensure that they do not have a name or they are not in other
|
|
manifest indexes else GC can never clean them.
|
|
*/
|
|
func UpdateIndexWithPrunedImageManifests(imgStore storageTypes.ImageStore, index *ispec.Index, repo string,
|
|
desc ispec.Descriptor, oldDgst godigest.Digest, log zlog.Logger,
|
|
) error {
|
|
if (desc.MediaType == ispec.MediaTypeImageIndex) && (oldDgst != "") {
|
|
otherImgIndexes := []ispec.Descriptor{}
|
|
|
|
for _, manifest := range index.Manifests {
|
|
if manifest.MediaType == ispec.MediaTypeImageIndex {
|
|
otherImgIndexes = append(otherImgIndexes, manifest)
|
|
}
|
|
}
|
|
|
|
otherImgIndexes = append(otherImgIndexes, desc)
|
|
|
|
prunedManifests, err := PruneImageManifestsFromIndex(imgStore, repo, oldDgst, *index, otherImgIndexes, log)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
index.Manifests = prunedManifests
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/*
|
|
Before an image index manifest is pushed to a repo, its constituent manifests
|
|
are pushed first, so when updating/removing this image index manifest, we also
|
|
need to determine if there are other image index manifests which refer to the
|
|
same constitutent manifests so that they can be garbage-collected correctly
|
|
|
|
PruneImageManifestsFromIndex is a helper routine to achieve this.
|
|
*/
|
|
func PruneImageManifestsFromIndex(imgStore storageTypes.ImageStore, repo string, digest godigest.Digest, //nolint:gocyclo,lll
|
|
outIndex ispec.Index, otherImgIndexes []ispec.Descriptor, log zlog.Logger,
|
|
) ([]ispec.Descriptor, error) {
|
|
dir := path.Join(imgStore.RootDir(), repo)
|
|
|
|
indexPath := path.Join(dir, "blobs", digest.Algorithm().String(), digest.Encoded())
|
|
|
|
buf, err := imgStore.GetBlobContent(repo, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var imgIndex ispec.Index
|
|
if err := json.Unmarshal(buf, &imgIndex); err != nil {
|
|
log.Error().Err(err).Str("path", indexPath).Msg("invalid JSON")
|
|
|
|
return nil, err
|
|
}
|
|
|
|
inUse := map[string]uint{}
|
|
|
|
for _, manifest := range imgIndex.Manifests {
|
|
inUse[manifest.Digest.Encoded()]++
|
|
}
|
|
|
|
for _, otherIndex := range otherImgIndexes {
|
|
oindex, err := GetImageIndex(imgStore, repo, otherIndex.Digest, log)
|
|
if err != nil {
|
|
// Handle missing blobs gracefully - log warning and continue with other indexes
|
|
var pathNotFoundErr driver.PathNotFoundError
|
|
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
|
|
log.Warn().Err(err).Str("repository", repo).Str("digest", otherIndex.Digest.String()).
|
|
Msg("skipping missing image index blob, continuing with other indexes")
|
|
|
|
continue
|
|
}
|
|
|
|
return nil, err
|
|
}
|
|
|
|
for _, omanifest := range oindex.Manifests {
|
|
_, ok := inUse[omanifest.Digest.Encoded()]
|
|
if ok {
|
|
inUse[omanifest.Digest.Encoded()]++
|
|
}
|
|
}
|
|
}
|
|
|
|
prunedManifests := []ispec.Descriptor{}
|
|
|
|
// for all manifests in the index, skip those that either have a tag or
|
|
// are used in other imgIndexes
|
|
for _, outManifest := range outIndex.Manifests {
|
|
if outManifest.MediaType != ispec.MediaTypeImageManifest {
|
|
prunedManifests = append(prunedManifests, outManifest)
|
|
|
|
continue
|
|
}
|
|
|
|
_, ok := outManifest.Annotations[ispec.AnnotationRefName]
|
|
if ok {
|
|
prunedManifests = append(prunedManifests, outManifest)
|
|
|
|
continue
|
|
}
|
|
|
|
count, ok := inUse[outManifest.Digest.Encoded()]
|
|
if !ok {
|
|
prunedManifests = append(prunedManifests, outManifest)
|
|
|
|
continue
|
|
}
|
|
|
|
if count != 1 {
|
|
// this manifest is in use in other image indexes
|
|
prunedManifests = append(prunedManifests, outManifest)
|
|
|
|
continue
|
|
}
|
|
}
|
|
|
|
return prunedManifests, nil
|
|
}
|
|
|
|
func isBlobReferencedInImageManifest(imgStore storageTypes.ImageStore, repo string,
|
|
bdigest, mdigest godigest.Digest, log zlog.Logger,
|
|
) (bool, error) {
|
|
if bdigest == mdigest {
|
|
return true, nil
|
|
}
|
|
|
|
manifestContent, err := GetImageManifest(imgStore, repo, mdigest, log)
|
|
if err != nil {
|
|
// Handle missing blobs gracefully - treat as not referenced and continue
|
|
var pathNotFoundErr driver.PathNotFoundError
|
|
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
|
|
log.Warn().Err(err).Str("repo", repo).Str("digest", mdigest.String()).Str("component", "gc").
|
|
Msg("skipping missing manifest blob, treating as not referenced")
|
|
|
|
return false, nil
|
|
}
|
|
|
|
log.Error().Err(err).Str("repo", repo).Str("digest", mdigest.String()).Str("component", "gc").
|
|
Msg("failed to read manifest image")
|
|
|
|
return false, err
|
|
}
|
|
|
|
if bdigest == manifestContent.Config.Digest {
|
|
return true, nil
|
|
}
|
|
|
|
for _, layer := range manifestContent.Layers {
|
|
if bdigest == layer.Digest {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func IsBlobReferencedInImageIndex(imgStore storageTypes.ImageStore, repo string,
|
|
digest godigest.Digest, index ispec.Index, log zlog.Logger,
|
|
) (bool, error) {
|
|
for _, desc := range index.Manifests {
|
|
var found bool
|
|
|
|
switch desc.MediaType {
|
|
case ispec.MediaTypeImageIndex:
|
|
if digest == desc.Digest {
|
|
// no need to look further if we have a match
|
|
return true, nil
|
|
}
|
|
|
|
indexImage, err := GetImageIndex(imgStore, repo, desc.Digest, log)
|
|
if err != nil {
|
|
// Handle missing blobs gracefully - treat as not referenced and continue
|
|
var pathNotFoundErr driver.PathNotFoundError
|
|
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
|
|
log.Warn().Err(err).Str("repository", repo).Str("digest", desc.Digest.String()).
|
|
Msg("skipping missing image index blob, treating as not referenced")
|
|
|
|
continue
|
|
}
|
|
|
|
log.Error().Err(err).Str("repository", repo).Str("digest", desc.Digest.String()).
|
|
Msg("failed to read multiarch(index) image")
|
|
|
|
return false, err
|
|
}
|
|
|
|
found, _ = IsBlobReferencedInImageIndex(imgStore, repo, digest, indexImage, log)
|
|
case ispec.MediaTypeImageManifest:
|
|
found, _ = isBlobReferencedInImageManifest(imgStore, repo, digest, desc.Digest, log)
|
|
default:
|
|
// should return true for digests found in index.json even if we don't know it's mediatype
|
|
if digest == desc.Digest {
|
|
log.Debug().Str("mediatype", desc.MediaType).Str("digest", digest.String()).
|
|
Msg("unexpected media-type found in image index manifest list")
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if found {
|
|
return true, nil
|
|
}
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
func IsBlobReferenced(imgStore storageTypes.ImageStore, repo string,
|
|
digest godigest.Digest, log zlog.Logger,
|
|
) (bool, error) {
|
|
dir := path.Join(imgStore.RootDir(), repo)
|
|
if !imgStore.DirExists(dir) {
|
|
return false, zerr.ErrRepoNotFound
|
|
}
|
|
|
|
index, err := GetIndex(imgStore, repo, log)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return IsBlobReferencedInImageIndex(imgStore, repo, digest, index, log)
|
|
}
|
|
|
|
func ApplyLinter(imgStore storageTypes.ImageStore, linter Lint, repo string, descriptor ispec.Descriptor,
|
|
) (bool, error) {
|
|
pass := true
|
|
|
|
// we'll skip anything that's not a image manifest
|
|
if descriptor.MediaType != ispec.MediaTypeImageManifest {
|
|
return pass, nil
|
|
}
|
|
|
|
if linter != nil && !IsSignature(descriptor) {
|
|
// lint new index with new manifest before writing to disk
|
|
pass, err := linter.Lint(repo, descriptor.Digest, imgStore)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if !pass {
|
|
return false, zerr.ErrImageLintAnnotations
|
|
}
|
|
}
|
|
|
|
return pass, nil
|
|
}
|
|
|
|
func IsSignature(descriptor ispec.Descriptor) bool {
|
|
tag := descriptor.Annotations[ispec.AnnotationRefName]
|
|
|
|
switch descriptor.MediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
// is cosgin signature
|
|
if strings.HasPrefix(tag, "sha256-") && strings.HasSuffix(tag, cosignSignatureTagSuffix) {
|
|
return true
|
|
}
|
|
|
|
// is cosign signature (OCI 1.1 support)
|
|
if descriptor.ArtifactType == zcommon.ArtifactTypeCosign {
|
|
return true
|
|
}
|
|
|
|
// is notation signature
|
|
if descriptor.ArtifactType == zcommon.ArtifactTypeNotation {
|
|
return true
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func GetReferrers(imgStore storageTypes.ImageStore, repo string, gdigest godigest.Digest, artifactTypes []string,
|
|
log zlog.Logger,
|
|
) (ispec.Index, error) {
|
|
nilIndex := ispec.Index{}
|
|
|
|
if err := gdigest.Validate(); err != nil {
|
|
return nilIndex, err
|
|
}
|
|
|
|
dir := path.Join(imgStore.RootDir(), repo)
|
|
if !imgStore.DirExists(dir) {
|
|
return nilIndex, zerr.ErrRepoNotFound
|
|
}
|
|
|
|
index, err := GetIndex(imgStore, repo, log)
|
|
if err != nil {
|
|
return nilIndex, err
|
|
}
|
|
|
|
result := []ispec.Descriptor{}
|
|
seenDigests := make(map[godigest.Digest]struct{})
|
|
|
|
for _, descriptor := range index.Manifests {
|
|
if descriptor.Digest == gdigest {
|
|
continue
|
|
}
|
|
|
|
// Skip if we've already processed this digest
|
|
if _, seen := seenDigests[descriptor.Digest]; seen {
|
|
continue
|
|
}
|
|
|
|
// Mark as seen early to avoid processing duplicates
|
|
seenDigests[descriptor.Digest] = struct{}{}
|
|
|
|
buf, err := imgStore.GetBlobContent(repo, descriptor.Digest)
|
|
if err != nil {
|
|
log.Error().Err(err).Str("blob", imgStore.BlobPath(repo, descriptor.Digest)).Msg("failed to read manifest")
|
|
|
|
if errors.Is(err, zerr.ErrBlobNotFound) {
|
|
return nilIndex, zerr.ErrManifestNotFound
|
|
}
|
|
|
|
return nilIndex, err
|
|
}
|
|
|
|
switch descriptor.MediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
var manifestContent ispec.Manifest
|
|
|
|
if err := json.Unmarshal(buf, &manifestContent); err != nil {
|
|
log.Error().Err(err).Str("manifest digest", descriptor.Digest.String()).Msg("invalid JSON")
|
|
|
|
return nilIndex, err
|
|
}
|
|
|
|
if manifestContent.Subject == nil || manifestContent.Subject.Digest != gdigest {
|
|
continue
|
|
}
|
|
|
|
// filter by artifact type
|
|
manifestArtifactType := zcommon.GetManifestArtifactType(manifestContent)
|
|
|
|
if len(artifactTypes) > 0 && !zcommon.Contains(artifactTypes, manifestArtifactType) {
|
|
continue
|
|
}
|
|
|
|
result = append(result, ispec.Descriptor{
|
|
MediaType: descriptor.MediaType,
|
|
ArtifactType: manifestArtifactType,
|
|
Size: descriptor.Size,
|
|
Digest: descriptor.Digest,
|
|
Annotations: manifestContent.Annotations,
|
|
})
|
|
case ispec.MediaTypeImageIndex:
|
|
var indexContent ispec.Index
|
|
|
|
if err := json.Unmarshal(buf, &indexContent); err != nil {
|
|
log.Error().Err(err).Str("manifest digest", descriptor.Digest.String()).Msg("invalid JSON")
|
|
|
|
return nilIndex, err
|
|
}
|
|
|
|
if indexContent.Subject == nil || indexContent.Subject.Digest != gdigest {
|
|
continue
|
|
}
|
|
|
|
indexArtifactType := zcommon.GetIndexArtifactType(indexContent)
|
|
|
|
if len(artifactTypes) > 0 && !zcommon.Contains(artifactTypes, indexArtifactType) {
|
|
continue
|
|
}
|
|
|
|
result = append(result, ispec.Descriptor{
|
|
MediaType: descriptor.MediaType,
|
|
ArtifactType: indexArtifactType,
|
|
Size: descriptor.Size,
|
|
Digest: descriptor.Digest,
|
|
Annotations: indexContent.Annotations,
|
|
})
|
|
}
|
|
}
|
|
|
|
index = ispec.Index{
|
|
Versioned: imeta.Versioned{SchemaVersion: storageConstants.SchemaVersion},
|
|
MediaType: ispec.MediaTypeImageIndex,
|
|
Manifests: result,
|
|
Annotations: map[string]string{},
|
|
}
|
|
|
|
return index, nil
|
|
}
|
|
|
|
// Get blob descriptor from it's manifest contents, if blob can not be found it will return error.
|
|
func GetBlobDescriptorFromRepo(imgStore storageTypes.ImageStore, repo string, blobDigest godigest.Digest,
|
|
log zlog.Logger,
|
|
) (ispec.Descriptor, error) {
|
|
index, err := GetIndex(imgStore, repo, log)
|
|
if err != nil {
|
|
return ispec.Descriptor{}, err
|
|
}
|
|
|
|
return GetBlobDescriptorFromIndex(imgStore, index, repo, blobDigest, log)
|
|
}
|
|
|
|
func GetBlobDescriptorFromIndex(imgStore storageTypes.ImageStore, index ispec.Index, repo string,
|
|
blobDigest godigest.Digest, log zlog.Logger,
|
|
) (ispec.Descriptor, error) {
|
|
for _, desc := range index.Manifests {
|
|
if desc.Digest == blobDigest {
|
|
return desc, nil
|
|
}
|
|
|
|
switch desc.MediaType {
|
|
case ispec.MediaTypeImageManifest:
|
|
if foundDescriptor, err := getBlobDescriptorFromManifest(imgStore, repo, blobDigest, desc, log); err == nil {
|
|
return foundDescriptor, nil
|
|
}
|
|
case ispec.MediaTypeImageIndex:
|
|
indexImage, err := GetImageIndex(imgStore, repo, desc.Digest, log)
|
|
if err != nil {
|
|
// Handle missing blobs gracefully - skip this index and continue searching
|
|
var pathNotFoundErr driver.PathNotFoundError
|
|
if errors.Is(err, zerr.ErrBlobNotFound) || errors.As(err, &pathNotFoundErr) {
|
|
log.Warn().Err(err).Str("repository", repo).Str("digest", desc.Digest.String()).
|
|
Msg("skipping missing image index blob, continuing search for blob descriptor")
|
|
|
|
continue
|
|
}
|
|
|
|
return ispec.Descriptor{}, err
|
|
}
|
|
|
|
if foundDescriptor, err := GetBlobDescriptorFromIndex(imgStore, indexImage, repo, blobDigest, log); err == nil {
|
|
return foundDescriptor, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
return ispec.Descriptor{}, zerr.ErrBlobNotFound
|
|
}
|
|
|
|
func getBlobDescriptorFromManifest(imgStore storageTypes.ImageStore, repo string, blobDigest godigest.Digest,
|
|
desc ispec.Descriptor, log zlog.Logger,
|
|
) (ispec.Descriptor, error) {
|
|
manifest, err := GetImageManifest(imgStore, repo, desc.Digest, log)
|
|
if err != nil {
|
|
return ispec.Descriptor{}, err
|
|
}
|
|
|
|
if manifest.Config.Digest == blobDigest {
|
|
return manifest.Config, nil
|
|
}
|
|
|
|
for _, layer := range manifest.Layers {
|
|
if layer.Digest == blobDigest {
|
|
return layer, nil
|
|
}
|
|
}
|
|
|
|
return ispec.Descriptor{}, zerr.ErrBlobNotFound
|
|
}
|
|
|
|
func IsSupportedMediaType(compats []compat.MediaCompatibility, mediaType string) bool {
|
|
// check for some supported legacy formats if configured
|
|
for _, comp := range compats {
|
|
if comp == compat.DockerManifestV2SchemaV2 &&
|
|
(mediaType == docker.MediaTypeManifest || mediaType == dockerList.MediaTypeManifestList) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return mediaType == ispec.MediaTypeImageIndex ||
|
|
mediaType == ispec.MediaTypeImageManifest
|
|
}
|
|
|
|
func IsNonDistributable(mediaType string) bool {
|
|
return mediaType == ispec.MediaTypeImageLayerNonDistributable || //nolint:staticcheck
|
|
mediaType == ispec.MediaTypeImageLayerNonDistributableGzip || //nolint:staticcheck
|
|
mediaType == ispec.MediaTypeImageLayerNonDistributableZstd //nolint:staticcheck
|
|
}
|
|
|
|
func ValidateManifestSchema(buf []byte) error {
|
|
if err := schema.ValidatorMediaTypeManifest.Validate(bytes.NewBuffer(buf)); err != nil {
|
|
if !IsEmptyLayersError(err) {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateImageIndexSchema(buf []byte) error {
|
|
if err := schema.ValidatorMediaTypeImageIndex.Validate(bytes.NewBuffer(buf)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func IsEmptyLayersError(err error) bool {
|
|
var validationErr *jsonschema.ValidationError
|
|
if errors.As(err, &validationErr) {
|
|
if len(validationErr.Causes) == 1 && strings.Contains(err.Error(), manifestWithEmptyLayersErrMsg) {
|
|
return true
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/*
|
|
DedupeTaskGenerator takes all blobs paths found in the storage.imagestore and groups them by digest
|
|
|
|
for each digest and based on the dedupe value it will dedupe or restore deduped blobs to the original state(undeduped)\
|
|
by creating a task for each digest and pushing it to the task scheduler.
|
|
*/
|
|
type DedupeTaskGenerator struct {
|
|
ImgStore storageTypes.ImageStore
|
|
// storage dedupe value
|
|
Dedupe bool
|
|
// store blobs paths grouped by digest
|
|
digest godigest.Digest
|
|
duplicateBlobs []string
|
|
/* store processed digest, used for iterating duplicateBlobs one by one
|
|
and generating a task for each unprocessed one*/
|
|
lastDigests []godigest.Digest
|
|
done bool
|
|
repos []string // list of repos on which we run dedupe
|
|
Log zlog.Logger
|
|
}
|
|
|
|
func (gen *DedupeTaskGenerator) Name() string {
|
|
return "DedupeTaskGenerator"
|
|
}
|
|
|
|
func (gen *DedupeTaskGenerator) Next() (scheduler.Task, error) {
|
|
var err error
|
|
|
|
/* at first run get from storage currently found repositories so that we skip the ones that gets synced/uploaded
|
|
while this generator runs, there are deduped/restored inline, no need to run dedupe/restore again */
|
|
if len(gen.repos) == 0 {
|
|
gen.repos, err = gen.ImgStore.GetRepositories()
|
|
if err != nil {
|
|
//nolint: dupword
|
|
gen.Log.Error().Err(err).Str("component", "dedupe").Msg("failed to get list of repositories")
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// if still no repos
|
|
if len(gen.repos) == 0 {
|
|
gen.Log.Info().Str("component", "dedupe").Msg("no repositories found in storage, finished.")
|
|
|
|
// no repositories in storage, no need to continue
|
|
gen.done = true
|
|
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
}
|
|
|
|
// get all blobs from storage.imageStore and group them by digest
|
|
gen.digest, gen.duplicateBlobs, err = gen.ImgStore.GetNextDigestWithBlobPaths(gen.repos, gen.lastDigests)
|
|
if err != nil {
|
|
gen.Log.Error().Err(err).Str("component", "dedupe").Msg("failed to get next digest")
|
|
|
|
return nil, err
|
|
}
|
|
|
|
// if no digests left, then mark the task generator as done
|
|
if gen.digest == "" {
|
|
gen.Log.Info().Str("component", "dedupe").Msg("no digests left, finished")
|
|
|
|
gen.done = true
|
|
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
// mark digest as processed before running its task
|
|
gen.lastDigests = append(gen.lastDigests, gen.digest)
|
|
|
|
// generate rebuild dedupe task for this digest
|
|
return newDedupeTask(gen.ImgStore, gen.digest, gen.Dedupe, gen.duplicateBlobs, gen.Log), nil
|
|
}
|
|
|
|
func (gen *DedupeTaskGenerator) IsDone() bool {
|
|
return gen.done
|
|
}
|
|
|
|
func (gen *DedupeTaskGenerator) IsReady() bool {
|
|
return true
|
|
}
|
|
|
|
func (gen *DedupeTaskGenerator) Reset() {
|
|
gen.lastDigests = []godigest.Digest{}
|
|
gen.duplicateBlobs = []string{}
|
|
gen.repos = []string{}
|
|
gen.digest = ""
|
|
gen.done = false
|
|
}
|
|
|
|
type dedupeTask struct {
|
|
imgStore storageTypes.ImageStore
|
|
// digest of duplicateBLobs
|
|
digest godigest.Digest
|
|
// blobs paths with the same digest ^
|
|
duplicateBlobs []string
|
|
dedupe bool
|
|
log zlog.Logger
|
|
}
|
|
|
|
func newDedupeTask(imgStore storageTypes.ImageStore, digest godigest.Digest, dedupe bool,
|
|
duplicateBlobs []string, log zlog.Logger,
|
|
) *dedupeTask {
|
|
return &dedupeTask{imgStore, digest, duplicateBlobs, dedupe, log}
|
|
}
|
|
|
|
func (dt *dedupeTask) DoWork(ctx context.Context) error {
|
|
// run task
|
|
err := dt.imgStore.RunDedupeForDigest(ctx, dt.digest, dt.dedupe, dt.duplicateBlobs) //nolint: contextcheck
|
|
if err != nil {
|
|
// log it
|
|
dt.log.Error().Err(err).Str("digest", dt.digest.String()).Str("component", "dedupe").
|
|
Msg("failed to rebuild digest")
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
func (dt *dedupeTask) String() string {
|
|
return fmt.Sprintf("{Name: %s, digest: %s, dedupe: %t}",
|
|
dt.Name(), dt.digest, dt.dedupe)
|
|
}
|
|
|
|
func (dt *dedupeTask) Name() string {
|
|
return "DedupeTask"
|
|
}
|
|
|
|
func NewStorageMetricsInitGenerator(imgStore storageTypes.ImageStore, metrics monitoring.MetricServer, log zlog.Logger,
|
|
) *StorageMetricsInitGenerator {
|
|
processedRepos := make(map[string]struct{})
|
|
|
|
return &StorageMetricsInitGenerator{
|
|
ImgStore: imgStore,
|
|
Metrics: metrics,
|
|
Log: log,
|
|
processedRepos: processedRepos,
|
|
MaxDelay: 15, //nolint:mnd
|
|
}
|
|
}
|
|
|
|
type StorageMetricsInitGenerator struct {
|
|
ImgStore storageTypes.ImageStore
|
|
done bool
|
|
Metrics monitoring.MetricServer
|
|
processedRepos map[string]struct{}
|
|
nextRun time.Time
|
|
rand *rand.Rand
|
|
Log zlog.Logger
|
|
MaxDelay int
|
|
}
|
|
|
|
func (gen *StorageMetricsInitGenerator) Name() string {
|
|
return "StorageMetricsInitGenerator"
|
|
}
|
|
|
|
func (gen *StorageMetricsInitGenerator) Next() (scheduler.Task, error) {
|
|
if len(gen.processedRepos) == 0 && gen.nextRun.IsZero() {
|
|
gen.rand = rand.New(rand.NewSource(time.Now().UTC().UnixNano())) //nolint: gosec
|
|
}
|
|
|
|
delay := gen.rand.Intn(gen.MaxDelay)
|
|
|
|
gen.nextRun = time.Now().Add(time.Duration(delay) * time.Second)
|
|
|
|
repo, err := gen.ImgStore.GetNextRepository(gen.processedRepos)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gen.Log.Debug().Str("repo", repo).Int("randomDelay", delay).Msg("generate task for storage metrics")
|
|
|
|
if repo == "" {
|
|
gen.done = true
|
|
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
|
|
gen.processedRepos[repo] = struct{}{}
|
|
|
|
return NewStorageMetricsTask(gen.ImgStore, gen.Metrics, repo, gen.Log), nil
|
|
}
|
|
|
|
func (gen *StorageMetricsInitGenerator) IsDone() bool {
|
|
return gen.done
|
|
}
|
|
|
|
func (gen *StorageMetricsInitGenerator) IsReady() bool {
|
|
return time.Now().After(gen.nextRun)
|
|
}
|
|
|
|
func (gen *StorageMetricsInitGenerator) Reset() {
|
|
gen.processedRepos = make(map[string]struct{})
|
|
gen.done = false
|
|
gen.nextRun = time.Time{}
|
|
}
|
|
|
|
type smTask struct {
|
|
imgStore storageTypes.ImageStore
|
|
metrics monitoring.MetricServer
|
|
repo string
|
|
log zlog.Logger
|
|
}
|
|
|
|
func NewStorageMetricsTask(imgStore storageTypes.ImageStore, metrics monitoring.MetricServer, repo string,
|
|
log zlog.Logger,
|
|
) *smTask {
|
|
return &smTask{imgStore, metrics, repo, log}
|
|
}
|
|
|
|
func (smt *smTask) DoWork(ctx context.Context) error {
|
|
// run task
|
|
monitoring.SetStorageUsage(smt.metrics, smt.imgStore.RootDir(), smt.repo)
|
|
smt.log.Debug().Str("component", "monitoring").Msg("computed storage usage for repo " + smt.repo)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (smt *smTask) String() string {
|
|
return fmt.Sprintf("{Name: \"%s\", repo: \"%s\"}",
|
|
smt.Name(), smt.repo)
|
|
}
|
|
|
|
func (smt *smTask) Name() string {
|
|
return "StorageMetricsTask"
|
|
}
|