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, ©Options) 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 }