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