zotregistry.io/zot@v1.4.4-0.20231124084042-02a8ed785457/pkg/extensions/sync/service.go (about)

     1  //go:build sync
     2  // +build sync
     3  
     4  package sync
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"github.com/containers/common/pkg/retry"
    12  	"github.com/containers/image/v5/copy"
    13  	"github.com/opencontainers/go-digest"
    14  
    15  	zerr "zotregistry.io/zot/errors"
    16  	"zotregistry.io/zot/pkg/common"
    17  	syncconf "zotregistry.io/zot/pkg/extensions/config/sync"
    18  	client "zotregistry.io/zot/pkg/extensions/sync/httpclient"
    19  	"zotregistry.io/zot/pkg/extensions/sync/references"
    20  	"zotregistry.io/zot/pkg/log"
    21  	mTypes "zotregistry.io/zot/pkg/meta/types"
    22  	"zotregistry.io/zot/pkg/storage"
    23  )
    24  
    25  type BaseService struct {
    26  	config          syncconf.RegistryConfig
    27  	credentials     syncconf.CredentialsFile
    28  	remote          Remote
    29  	local           Local
    30  	retryOptions    *retry.RetryOptions
    31  	contentManager  ContentManager
    32  	storeController storage.StoreController
    33  	metaDB          mTypes.MetaDB
    34  	repositories    []string
    35  	references      references.References
    36  	client          *client.Client
    37  	log             log.Logger
    38  }
    39  
    40  func New(opts syncconf.RegistryConfig, credentialsFilepath string,
    41  	storeController storage.StoreController, metadb mTypes.MetaDB, log log.Logger,
    42  ) (Service, error) {
    43  	service := &BaseService{}
    44  
    45  	service.config = opts
    46  	service.log = log
    47  	service.metaDB = metadb
    48  
    49  	var err error
    50  
    51  	var credentialsFile syncconf.CredentialsFile
    52  	if credentialsFilepath != "" {
    53  		credentialsFile, err = getFileCredentials(credentialsFilepath)
    54  		if err != nil {
    55  			log.Error().Str("errortype", common.TypeOf(err)).Str("path", credentialsFilepath).
    56  				Err(err).Msg("couldn't get registry credentials from configured path")
    57  		}
    58  	}
    59  
    60  	service.credentials = credentialsFile
    61  
    62  	service.contentManager = NewContentManager(opts.Content, log)
    63  	service.local = NewLocalRegistry(storeController, metadb, log)
    64  
    65  	retryOptions := &retry.RetryOptions{}
    66  
    67  	if opts.MaxRetries != nil {
    68  		retryOptions.MaxRetry = *opts.MaxRetries
    69  		if opts.RetryDelay != nil {
    70  			retryOptions.Delay = *opts.RetryDelay
    71  		}
    72  	}
    73  
    74  	service.retryOptions = retryOptions
    75  	service.storeController = storeController
    76  
    77  	err = service.SetNextAvailableClient()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	service.references = references.NewReferences(
    83  		service.client,
    84  		service.storeController,
    85  		service.metaDB,
    86  		service.log,
    87  	)
    88  
    89  	service.remote = NewRemoteRegistry(
    90  		service.client,
    91  		service.log,
    92  	)
    93  
    94  	return service, nil
    95  }
    96  
    97  func (service *BaseService) SetNextAvailableClient() error {
    98  	if service.client != nil && service.client.Ping() {
    99  		return nil
   100  	}
   101  
   102  	for _, url := range service.config.URLs {
   103  		remoteAddress := StripRegistryTransport(url)
   104  		credentials := service.credentials[remoteAddress]
   105  
   106  		tlsVerify := true
   107  		if service.config.TLSVerify != nil {
   108  			tlsVerify = *service.config.TLSVerify
   109  		}
   110  
   111  		options := client.Config{
   112  			URL:       url,
   113  			Username:  credentials.Username,
   114  			Password:  credentials.Password,
   115  			TLSVerify: tlsVerify,
   116  			CertDir:   service.config.CertDir,
   117  		}
   118  
   119  		var err error
   120  
   121  		if service.client != nil {
   122  			err = service.client.SetConfig(options)
   123  		} else {
   124  			service.client, err = client.New(options, service.log)
   125  		}
   126  
   127  		if err != nil {
   128  			service.log.Error().Err(err).Str("url", url).Msg("sync: failed to initialize http client")
   129  
   130  			continue
   131  		}
   132  
   133  		if !service.client.Ping() {
   134  			continue
   135  		}
   136  	}
   137  
   138  	if service.client == nil {
   139  		return zerr.ErrSyncPingRegistry
   140  	}
   141  
   142  	return nil
   143  }
   144  
   145  func (service *BaseService) GetRetryOptions() *retry.Options {
   146  	return service.retryOptions
   147  }
   148  
   149  func (service *BaseService) getNextRepoFromCatalog(lastRepo string) string {
   150  	var found bool
   151  
   152  	var nextRepo string
   153  
   154  	for _, repo := range service.repositories {
   155  		if lastRepo == "" {
   156  			nextRepo = repo
   157  
   158  			break
   159  		}
   160  
   161  		if repo == lastRepo {
   162  			found = true
   163  
   164  			continue
   165  		}
   166  
   167  		if found {
   168  			nextRepo = repo
   169  
   170  			break
   171  		}
   172  	}
   173  
   174  	return nextRepo
   175  }
   176  
   177  func (service *BaseService) GetNextRepo(lastRepo string) (string, error) {
   178  	var err error
   179  
   180  	if len(service.repositories) == 0 {
   181  		if err = retry.RetryIfNecessary(context.Background(), func() error {
   182  			service.repositories, err = service.remote.GetRepositories(context.Background())
   183  
   184  			return err
   185  		}, service.retryOptions); err != nil {
   186  			service.log.Error().Str("errorType", common.TypeOf(err)).Str("remote registry", service.client.GetConfig().URL).
   187  				Err(err).Msg("error while getting repositories from remote registry")
   188  
   189  			return "", err
   190  		}
   191  	}
   192  
   193  	var matches bool
   194  
   195  	for !matches {
   196  		lastRepo = service.getNextRepoFromCatalog(lastRepo)
   197  		if lastRepo == "" {
   198  			break
   199  		}
   200  
   201  		matches = service.contentManager.MatchesContent(lastRepo)
   202  	}
   203  
   204  	return lastRepo, nil
   205  }
   206  
   207  // SyncReference on demand.
   208  func (service *BaseService) SyncReference(ctx context.Context, repo string,
   209  	subjectDigestStr string, referenceType string,
   210  ) error {
   211  	remoteRepo := repo
   212  
   213  	remoteURL := service.client.GetConfig().URL
   214  
   215  	if len(service.config.Content) > 0 {
   216  		remoteRepo = service.contentManager.GetRepoSource(repo)
   217  		if remoteRepo == "" {
   218  			service.log.Info().Str("remote", remoteURL).Str("repo", repo).Str("subject", subjectDigestStr).
   219  				Str("reference type", referenceType).Msg("will not sync reference for image, filtered out by content")
   220  
   221  			return zerr.ErrSyncImageFilteredOut
   222  		}
   223  	}
   224  
   225  	service.log.Info().Str("remote", remoteURL).Str("repo", repo).Str("subject", subjectDigestStr).
   226  		Str("reference type", referenceType).Msg("sync: syncing reference for image")
   227  
   228  	return service.references.SyncReference(ctx, repo, remoteRepo, subjectDigestStr, referenceType)
   229  }
   230  
   231  // SyncImage on demand.
   232  func (service *BaseService) SyncImage(ctx context.Context, repo, reference string) error {
   233  	remoteRepo := repo
   234  
   235  	remoteURL := service.client.GetConfig().URL
   236  
   237  	if len(service.config.Content) > 0 {
   238  		remoteRepo = service.contentManager.GetRepoSource(repo)
   239  		if remoteRepo == "" {
   240  			service.log.Info().Str("remote", remoteURL).Str("repo", repo).Str("reference", reference).
   241  				Msg("will not sync image, filtered out by content")
   242  
   243  			return zerr.ErrSyncImageFilteredOut
   244  		}
   245  	}
   246  
   247  	service.log.Info().Str("remote", remoteURL).Str("repo", repo).Str("reference", reference).
   248  		Msg("sync: syncing image")
   249  
   250  	manifestDigest, err := service.syncTag(ctx, repo, remoteRepo, reference)
   251  	if err != nil {
   252  		return err
   253  	}
   254  
   255  	err = service.references.SyncAll(ctx, repo, remoteRepo, manifestDigest.String())
   256  	if err != nil && !errors.Is(err, zerr.ErrSyncReferrerNotFound) {
   257  		return err
   258  	}
   259  
   260  	return nil
   261  }
   262  
   263  // sync repo periodically.
   264  func (service *BaseService) SyncRepo(ctx context.Context, repo string) error {
   265  	service.log.Info().Str("repo", repo).Str("registry", service.client.GetConfig().URL).
   266  		Msg("sync: syncing repo")
   267  
   268  	var err error
   269  
   270  	var tags []string
   271  
   272  	if err = retry.RetryIfNecessary(ctx, func() error {
   273  		tags, err = service.remote.GetRepoTags(repo)
   274  
   275  		return err
   276  	}, service.retryOptions); err != nil {
   277  		service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).
   278  			Err(err).Msg("error while getting tags for repo")
   279  
   280  		return err
   281  	}
   282  
   283  	// filter tags
   284  	tags, err = service.contentManager.FilterTags(repo, tags)
   285  	if err != nil {
   286  		return err
   287  	}
   288  
   289  	service.log.Info().Str("repo", repo).Msgf("sync: syncing tags %v", tags)
   290  
   291  	// apply content.destination rule
   292  	localRepo := service.contentManager.GetRepoDestination(repo)
   293  
   294  	for _, tag := range tags {
   295  		if common.IsContextDone(ctx) {
   296  			return ctx.Err()
   297  		}
   298  
   299  		if references.IsCosignTag(tag) || common.IsReferrersTag(tag) {
   300  			continue
   301  		}
   302  
   303  		var manifestDigest digest.Digest
   304  
   305  		if err = retry.RetryIfNecessary(ctx, func() error {
   306  			manifestDigest, err = service.syncTag(ctx, localRepo, repo, tag)
   307  
   308  			return err
   309  		}, service.retryOptions); err != nil {
   310  			if errors.Is(err, zerr.ErrSyncImageNotSigned) || errors.Is(err, zerr.ErrMediaTypeNotSupported) {
   311  				// skip unsigned images or unsupported image mediatype
   312  				continue
   313  			}
   314  
   315  			service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).
   316  				Err(err).Msg("error while syncing tags for repo")
   317  
   318  			return err
   319  		}
   320  
   321  		if manifestDigest != "" {
   322  			if err = retry.RetryIfNecessary(ctx, func() error {
   323  				err = service.references.SyncAll(ctx, localRepo, repo, manifestDigest.String())
   324  				if errors.Is(err, zerr.ErrSyncReferrerNotFound) {
   325  					return nil
   326  				}
   327  
   328  				return err
   329  			}, service.retryOptions); err != nil {
   330  				service.log.Error().Str("errorType", common.TypeOf(err)).Str("repo", repo).
   331  					Err(err).Msg("error while syncing tags for repo")
   332  
   333  				return err
   334  			}
   335  		}
   336  	}
   337  
   338  	service.log.Info().Str("repo", repo).Msg("sync: finished syncing repo")
   339  
   340  	return nil
   341  }
   342  
   343  func (service *BaseService) syncTag(ctx context.Context, localRepo, remoteRepo, tag string) (digest.Digest, error) {
   344  	copyOptions := getCopyOptions(service.remote.GetContext(), service.local.GetContext())
   345  
   346  	policyContext, err := getPolicyContext(service.log)
   347  	if err != nil {
   348  		return "", err
   349  	}
   350  
   351  	defer func() {
   352  		_ = policyContext.Destroy()
   353  	}()
   354  
   355  	remoteImageRef, err := service.remote.GetImageReference(remoteRepo, tag)
   356  	if err != nil {
   357  		service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
   358  			Str("repo", remoteRepo).Str("reference", tag).Msg("couldn't get a remote image reference")
   359  
   360  		return "", err
   361  	}
   362  
   363  	_, mediaType, manifestDigest, err := service.remote.GetManifestContent(remoteImageRef)
   364  	if err != nil {
   365  		service.log.Error().Err(err).Str("repo", remoteRepo).Str("reference", tag).
   366  			Msg("couldn't get upstream image manifest details")
   367  
   368  		return "", err
   369  	}
   370  
   371  	if !isSupportedMediaType(mediaType) {
   372  		return "", zerr.ErrMediaTypeNotSupported
   373  	}
   374  
   375  	if service.config.OnlySigned != nil && *service.config.OnlySigned &&
   376  		!references.IsCosignTag(tag) && !common.IsReferrersTag(tag) {
   377  		signed := service.references.IsSigned(ctx, remoteRepo, manifestDigest.String())
   378  		if !signed {
   379  			// skip unsigned images
   380  			service.log.Info().Str("image", remoteImageRef.DockerReference().String()).
   381  				Msg("skipping image without mandatory signature")
   382  
   383  			return "", zerr.ErrSyncImageNotSigned
   384  		}
   385  	}
   386  
   387  	skipImage, err := service.local.CanSkipImage(localRepo, tag, manifestDigest)
   388  	if err != nil {
   389  		service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
   390  			Str("repo", localRepo).Str("reference", tag).
   391  			Msg("couldn't check if the local image can be skipped")
   392  	}
   393  
   394  	if !skipImage {
   395  		localImageRef, err := service.local.GetImageReference(localRepo, tag)
   396  		if err != nil {
   397  			service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
   398  				Str("repo", localRepo).Str("reference", tag).Msg("couldn't get a local image reference")
   399  
   400  			return "", err
   401  		}
   402  
   403  		service.log.Info().Str("remote image", remoteImageRef.DockerReference().String()).
   404  			Str("local image", fmt.Sprintf("%s:%s", localRepo, tag)).Msg("syncing image")
   405  
   406  		_, err = copy.Image(ctx, policyContext, localImageRef, remoteImageRef, &copyOptions)
   407  		if err != nil {
   408  			service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
   409  				Str("remote image", remoteImageRef.DockerReference().String()).
   410  				Str("local image", fmt.Sprintf("%s:%s", localRepo, tag)).Msg("coulnd't sync image")
   411  
   412  			return "", err
   413  		}
   414  
   415  		err = service.local.CommitImage(localImageRef, localRepo, tag)
   416  		if err != nil {
   417  			service.log.Error().Err(err).Str("errortype", common.TypeOf(err)).
   418  				Str("repo", localRepo).Str("reference", tag).Msg("couldn't commit image to local image store")
   419  
   420  			return "", err
   421  		}
   422  	} else {
   423  		service.log.Info().Str("image", remoteImageRef.DockerReference().String()).
   424  			Msg("skipping image because it's already synced")
   425  	}
   426  
   427  	service.log.Info().Str("image", remoteImageRef.DockerReference().String()).Msg("sync: finished syncing image")
   428  
   429  	return manifestDigest, nil
   430  }
   431  
   432  func (service *BaseService) ResetCatalog() {
   433  	service.log.Info().Msg("resetting catalog")
   434  
   435  	service.repositories = []string{}
   436  }
   437  
   438  func (service *BaseService) SetNextAvailableURL() error {
   439  	service.log.Info().Msg("getting available client")
   440  
   441  	return service.SetNextAvailableClient()
   442  }