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