zotregistry.dev/zot@v1.4.4-0.20240314164342-eec277e14d20/pkg/extensions/sync/references/references.go (about)

     1  //go:build sync
     2  // +build sync
     3  
     4  package references
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"net/http"
    12  
    13  	godigest "github.com/opencontainers/go-digest"
    14  	ispec "github.com/opencontainers/image-spec/specs-go/v1"
    15  	"github.com/sigstore/cosign/v2/pkg/oci/static"
    16  
    17  	zerr "zotregistry.dev/zot/errors"
    18  	"zotregistry.dev/zot/pkg/common"
    19  	"zotregistry.dev/zot/pkg/extensions/sync/constants"
    20  	"zotregistry.dev/zot/pkg/extensions/sync/features"
    21  	client "zotregistry.dev/zot/pkg/extensions/sync/httpclient"
    22  	"zotregistry.dev/zot/pkg/log"
    23  	mTypes "zotregistry.dev/zot/pkg/meta/types"
    24  	"zotregistry.dev/zot/pkg/storage"
    25  	storageTypes "zotregistry.dev/zot/pkg/storage/types"
    26  )
    27  
    28  type Reference interface {
    29  	// Returns name of reference (OCIReference/CosignReference)
    30  	Name() string
    31  	// Returns whether or not image is signed
    32  	IsSigned(ctx context.Context, upstreamRepo, subjectDigestStr string) bool
    33  	// Sync recursively all references for a subject digest (can be image/artifacts/signatures)
    34  	SyncReferences(ctx context.Context, localRepo, upstreamRepo, subjectDigestStr string) ([]godigest.Digest, error)
    35  }
    36  
    37  type References struct {
    38  	referenceList []Reference
    39  	features      *features.Map
    40  	log           log.Logger
    41  }
    42  
    43  func NewReferences(httpClient *client.Client, storeController storage.StoreController,
    44  	metaDB mTypes.MetaDB, log log.Logger,
    45  ) References {
    46  	refs := References{features: features.New(), log: log}
    47  
    48  	refs.referenceList = append(refs.referenceList, NewCosignReference(httpClient, storeController, metaDB, log))
    49  	refs.referenceList = append(refs.referenceList, NewTagReferences(httpClient, storeController, metaDB, log))
    50  	refs.referenceList = append(refs.referenceList, NewOciReferences(httpClient, storeController, metaDB, log))
    51  
    52  	return refs
    53  }
    54  
    55  func (refs References) IsSigned(ctx context.Context, upstreamRepo, subjectDigestStr string) bool {
    56  	for _, ref := range refs.referenceList {
    57  		ok := ref.IsSigned(ctx, upstreamRepo, subjectDigestStr)
    58  		if ok {
    59  			return true
    60  		}
    61  	}
    62  
    63  	return false
    64  }
    65  
    66  func (refs References) SyncAll(ctx context.Context, localRepo, upstreamRepo, subjectDigestStr string) error {
    67  	seen := &[]godigest.Digest{}
    68  
    69  	return refs.syncAll(ctx, localRepo, upstreamRepo, subjectDigestStr, seen)
    70  }
    71  
    72  func (refs References) syncAll(ctx context.Context, localRepo, upstreamRepo,
    73  	subjectDigestStr string, seen *[]godigest.Digest,
    74  ) error {
    75  	var err error
    76  
    77  	var syncedRefsDigests []godigest.Digest
    78  
    79  	// mark subject digest as seen as soon as it comes in
    80  	*seen = append(*seen, godigest.Digest(subjectDigestStr))
    81  
    82  	// for each reference type(cosign/oci reference)
    83  	for _, ref := range refs.referenceList {
    84  		supported, ok := refs.features.Get(ref.Name(), upstreamRepo)
    85  		if !supported && ok {
    86  			continue
    87  		}
    88  
    89  		syncedRefsDigests, err = ref.SyncReferences(ctx, localRepo, upstreamRepo, subjectDigestStr)
    90  		if err != nil {
    91  			// for all referrers we can stop querying same repo (for ten minutes) if the errors are different than 404
    92  			if !errors.Is(err, zerr.ErrSyncReferrerNotFound) {
    93  				refs.features.Set(ref.Name(), upstreamRepo, false)
    94  			}
    95  
    96  			// in the case of oci referrers, it will return 404 only if the repo is not found or refferers API is not supported
    97  			// no need to continue to make requests to the same repo
    98  			if ref.Name() == constants.OCI && errors.Is(err, zerr.ErrSyncReferrerNotFound) {
    99  				refs.features.Set(ref.Name(), upstreamRepo, false)
   100  			}
   101  
   102  			refs.log.Debug().Err(err).
   103  				Str("reference type", ref.Name()).
   104  				Str("image", fmt.Sprintf("%s:%s", upstreamRepo, subjectDigestStr)).
   105  				Msg("couldn't sync image referrer")
   106  		} else {
   107  			refs.features.Set(ref.Name(), upstreamRepo, true)
   108  		}
   109  
   110  		// for each synced references
   111  		for _, refDigest := range syncedRefsDigests {
   112  			if !common.Contains(*seen, refDigest) {
   113  				// sync all references pointing to this one
   114  				err = refs.syncAll(ctx, localRepo, upstreamRepo, refDigest.String(), seen)
   115  			}
   116  		}
   117  	}
   118  
   119  	return err
   120  }
   121  
   122  func (refs References) SyncReference(ctx context.Context, localRepo, upstreamRepo,
   123  	subjectDigestStr, referenceType string,
   124  ) error {
   125  	var err error
   126  
   127  	var syncedRefsDigests []godigest.Digest
   128  
   129  	for _, ref := range refs.referenceList {
   130  		if ref.Name() == referenceType {
   131  			syncedRefsDigests, err = ref.SyncReferences(ctx, localRepo, upstreamRepo, subjectDigestStr)
   132  			if err != nil {
   133  				refs.log.Debug().Err(err).
   134  					Str("reference type", ref.Name()).
   135  					Str("image", fmt.Sprintf("%s:%s", upstreamRepo, subjectDigestStr)).
   136  					Msg("couldn't sync image referrer")
   137  
   138  				return err
   139  			}
   140  
   141  			for _, refDigest := range syncedRefsDigests {
   142  				err = refs.SyncAll(ctx, localRepo, upstreamRepo, refDigest.String())
   143  			}
   144  		}
   145  	}
   146  
   147  	return err
   148  }
   149  
   150  func syncBlob(ctx context.Context, client *client.Client, imageStore storageTypes.ImageStore,
   151  	localRepo, remoteRepo string, digest godigest.Digest, log log.Logger,
   152  ) error {
   153  	var resultPtr interface{}
   154  
   155  	body, _, statusCode, err := client.MakeGetRequest(ctx, resultPtr, "", "v2", remoteRepo, "blobs", digest.String())
   156  	if err != nil {
   157  		if statusCode != http.StatusOK {
   158  			log.Info().Str("repo", remoteRepo).Str("digest", digest.String()).Msg("couldn't get remote blob")
   159  
   160  			return err
   161  		}
   162  	}
   163  
   164  	_, _, err = imageStore.FullBlobUpload(localRepo, bytes.NewBuffer(body), digest)
   165  	if err != nil {
   166  		log.Error().Str("errorType", common.TypeOf(err)).Str("digest", digest.String()).Str("repo", localRepo).
   167  			Err(err).Msg("couldn't upload blob")
   168  
   169  		return err
   170  	}
   171  
   172  	return nil
   173  }
   174  
   175  func manifestsEqual(manifest1, manifest2 ispec.Manifest) bool {
   176  	if manifest1.Config.Digest == manifest2.Config.Digest &&
   177  		manifest1.Config.MediaType == manifest2.Config.MediaType &&
   178  		manifest1.Config.Size == manifest2.Config.Size {
   179  		if descriptorsEqual(manifest1.Layers, manifest2.Layers) {
   180  			return true
   181  		}
   182  	}
   183  
   184  	return false
   185  }
   186  
   187  func descriptorsEqual(desc1, desc2 []ispec.Descriptor) bool {
   188  	if len(desc1) != len(desc2) {
   189  		return false
   190  	}
   191  
   192  	for id, desc := range desc1 {
   193  		if !descriptorEqual(desc, desc2[id]) {
   194  			return false
   195  		}
   196  	}
   197  
   198  	return true
   199  }
   200  
   201  func descriptorEqual(desc1, desc2 ispec.Descriptor) bool {
   202  	if desc1.Size == desc2.Size &&
   203  		desc1.Digest == desc2.Digest &&
   204  		desc1.MediaType == desc2.MediaType &&
   205  		desc1.Annotations[static.SignatureAnnotationKey] == desc2.Annotations[static.SignatureAnnotationKey] {
   206  		return true
   207  	}
   208  
   209  	return false
   210  }
   211  
   212  func getNotationManifestsFromOCIRefs(ociRefs ispec.Index) []ispec.Descriptor {
   213  	notaryManifests := []ispec.Descriptor{}
   214  
   215  	for _, ref := range ociRefs.Manifests {
   216  		if ref.ArtifactType == common.ArtifactTypeNotation {
   217  			notaryManifests = append(notaryManifests, ref)
   218  		}
   219  	}
   220  
   221  	return notaryManifests
   222  }
   223  
   224  func getCosignManifestsFromOCIRefs(ociRefs ispec.Index) []ispec.Descriptor {
   225  	cosignManifests := []ispec.Descriptor{}
   226  
   227  	for _, ref := range ociRefs.Manifests {
   228  		if ref.ArtifactType == common.ArtifactTypeCosign {
   229  			cosignManifests = append(cosignManifests, ref)
   230  		}
   231  	}
   232  
   233  	return cosignManifests
   234  }