From 20a60cbad465f6147b0814f1096b4f8483b693e4 Mon Sep 17 00:00:00 2001 From: Catalin Hofnar Date: Tue, 5 Apr 2022 18:18:31 +0300 Subject: [PATCH] Enhance sync logic - stop blob redownloads and re-pushes (#479 #480) Changed imagesToCopyFromUpstream to return a map[string][]types.ImageReference from just an array of refs Rewrote some logic in sync.go to use the new signature of imagesToCopyFromUpstream Split getLocalImageRef by adding function getLocalCachePath Adapted tests for new changes, added some tests Merged #481 Signed-off-by: Catalin Hofnar --- pkg/extensions/sync/on_demand.go | 7 +- pkg/extensions/sync/sync.go | 316 ++++++++++++---------- pkg/extensions/sync/sync_internal_test.go | 66 ++++- pkg/extensions/sync/sync_test.go | 4 +- pkg/extensions/sync/utils.go | 86 +++--- 5 files changed, 285 insertions(+), 194 deletions(-) diff --git a/pkg/extensions/sync/on_demand.go b/pkg/extensions/sync/on_demand.go index 4f223cef..a6e378d9 100644 --- a/pkg/extensions/sync/on_demand.go +++ b/pkg/extensions/sync/on_demand.go @@ -309,7 +309,12 @@ func syncRun(regCfg RegistryConfig, localRepo, remoteRepo, tag string, utils syn } } - localImageRef, localCachePath, err := getLocalImageRef(utils.imageStore, localRepo, tag) + localCachePath, err := getLocalCachePath(utils.imageStore, localRepo) + if err != nil { + log.Error().Err(err).Msgf("couldn't get localCachePath for %s", localRepo) + } + + localImageRef, err := getLocalImageRef(localCachePath, localRepo, tag) if err != nil { log.Error().Err(err).Msgf("couldn't obtain a valid image reference for reference %s/%s:%s", localCachePath, localRepo, tag) diff --git a/pkg/extensions/sync/sync.go b/pkg/extensions/sync/sync.go index 2401f138..ad64be74 100644 --- a/pkg/extensions/sync/sync.go +++ b/pkg/extensions/sync/sync.go @@ -190,10 +190,12 @@ func filterImagesBySemver(upstreamReferences *[]types.ImageReference, content Co // imagesToCopyFromRepos lists all images given a registry name and its repos. func imagesToCopyFromUpstream(ctx context.Context, registryName string, repos []string, upstreamCtx *types.SystemContext, content Content, log log.Logger, -) ([]types.ImageReference, error) { - var upstreamReferences []types.ImageReference +) (map[string][]types.ImageReference, error) { + upstreamReferences := make(map[string][]types.ImageReference) for _, repoName := range repos { + repoUpstreamReferences := make([]types.ImageReference, 0) + repoRef, err := parseRepositoryReference(fmt.Sprintf("%s/%s", registryName, repoName)) if err != nil { log.Error().Err(err).Msgf("couldn't parse repository reference: %s", repoRef) @@ -230,23 +232,27 @@ func imagesToCopyFromUpstream(ctx context.Context, registryName string, repos [] return nil, err } - upstreamReferences = append(upstreamReferences, ref) + repoUpstreamReferences = append(repoUpstreamReferences, ref) } + + upstreamReferences[repoName] = repoUpstreamReferences + + log.Debug().Msgf("repo: %s - upstream refs to be copied: %v", repoName, upstreamReferences) + + err = filterImagesByTagRegex(&repoUpstreamReferences, content, log) + if err != nil { + return map[string][]types.ImageReference{}, err + } + + log.Debug().Msgf("repo: %s - remaining upstream refs to be copied: %v", repoName, repoUpstreamReferences) + + filterImagesBySemver(&repoUpstreamReferences, content, log) + + log.Debug().Msgf("repo: %s - remaining upstream refs to be copied: %v", repoName, repoUpstreamReferences) + + upstreamReferences[repoName] = repoUpstreamReferences } - log.Debug().Msgf("upstream refs to be copied: %v", upstreamReferences) - - err := filterImagesByTagRegex(&upstreamReferences, content, log) - if err != nil { - return []types.ImageReference{}, err - } - - log.Debug().Msgf("remaining upstream refs to be copied: %v", upstreamReferences) - - filterImagesBySemver(&upstreamReferences, content, log) - - log.Debug().Msgf("remaining upstream refs to be copied: %v", upstreamReferences) - return upstreamReferences, nil } @@ -284,6 +290,7 @@ func getUpstreamContext(regCfg *RegistryConfig, credentials Credentials) *types. return upstreamCtx } +// nolint:gocyclo // offloading some of the functionalities from here would make the code harder to follow func syncRegistry(ctx context.Context, regCfg RegistryConfig, upstreamURL string, storeController storage.StoreController, localCtx *types.SystemContext, policyCtx *signature.PolicyContext, credentials Credentials, @@ -321,30 +328,36 @@ func syncRegistry(ctx context.Context, regCfg RegistryConfig, upstreamURL string log.Info().Msgf("got repos: %v", repos) - var images []struct { + upstreamAddr := StripRegistryTransport(upstreamURL) + + reposWithContentID := make(map[string][]struct { ref types.ImageReference content Content - } - - upstreamAddr := StripRegistryTransport(upstreamURL) + }) for contentID, repos := range repos { r := repos contentID := contentID if err = retry.RetryIfNecessary(ctx, func() error { - refs, err := imagesToCopyFromUpstream(ctx, upstreamAddr, r, upstreamCtx, regCfg.Content[contentID], log) - for _, ref := range refs { - images = append(images, struct { - ref types.ImageReference - content Content - }{ - ref: ref, - content: regCfg.Content[contentID], - }) + for _, repo := range r { + refs, err := imagesToCopyFromUpstream(ctx, upstreamAddr, r, upstreamCtx, regCfg.Content[contentID], log) + if err != nil { + return err + } + + for _, ref := range refs[repo] { + reposWithContentID[repo] = append(reposWithContentID[repo], struct { + ref types.ImageReference + content Content + }{ + ref: ref, + content: regCfg.Content[contentID], + }) + } } - return err + return nil }, retryOptions); err != nil { log.Error().Err(err).Msg("error while getting images references from upstream, retrying...") @@ -352,7 +365,7 @@ func syncRegistry(ctx context.Context, regCfg RegistryConfig, upstreamURL string } } - for _, image := range images { + for remoteRepo, imageList := range reposWithContentID { select { case <-ctx.Done(): return ctx.Err() @@ -360,147 +373,160 @@ func syncRegistry(ctx context.Context, regCfg RegistryConfig, upstreamURL string break } - upstreamImageRef := image.ref + remoteRepoCopy := remoteRepo + imageStore := storeController.GetImageStore(remoteRepoCopy) - remoteRepo := getRepoFromRef(upstreamImageRef, upstreamAddr) - localRepo := getRepoDestination(remoteRepo, image.content) - - upstreamImageDigest, err := docker.GetDigest(ctx, upstreamCtx, upstreamImageRef) + localCachePath, err := getLocalCachePath(imageStore, remoteRepoCopy) if err != nil { - log.Error().Err(err).Msgf("couldn't get upstream image %s manifest", upstreamImageRef.DockerReference()) + log.Error().Err(err).Msgf("couldn't get localCachePath for %s", remoteRepoCopy) return err } - tag := getTagFromRef(upstreamImageRef, log).Tag() - - imageStore := storeController.GetImageStore(localRepo) - - // get upstream signatures - cosignManifest, err := getCosignManifest(httpClient, *registryURL, remoteRepo, - upstreamImageDigest.String(), log) - if err != nil && !errors.Is(err, zerr.ErrSyncSignatureNotFound) { - log.Error().Err(err).Msgf("couldn't get upstream image %s cosign manifest", upstreamImageRef.DockerReference()) - - return err + if localCachePath != "" { + defer os.RemoveAll(localCachePath) } - refs, err := getNotaryRefs(httpClient, *registryURL, remoteRepo, upstreamImageDigest.String(), log) - if err != nil && !errors.Is(err, zerr.ErrSyncSignatureNotFound) { - log.Error().Err(err).Msgf("couldn't get upstream image %s notary references", upstreamImageRef.DockerReference()) + for _, image := range imageList { + localRepo := remoteRepoCopy + upstreamImageRef := image.ref - return err - } + upstreamImageDigest, err := docker.GetDigest(ctx, upstreamCtx, upstreamImageRef) + if err != nil { + log.Error().Err(err).Msgf("couldn't get upstream image %s manifest", upstreamImageRef.DockerReference()) - // check if upstream image is signed - if cosignManifest == nil && len(refs.References) == 0 { - // upstream image not signed - if regCfg.OnlySigned != nil && *regCfg.OnlySigned { - // skip unsigned images - log.Info().Msgf("skipping image without signature %s", upstreamImageRef.DockerReference()) + return err + } + + tag := getTagFromRef(upstreamImageRef, log).Tag() + // get upstream signatures + cosignManifest, err := getCosignManifest(httpClient, *registryURL, remoteRepoCopy, + upstreamImageDigest.String(), log) + if err != nil && !errors.Is(err, zerr.ErrSyncSignatureNotFound) { + log.Error().Err(err).Msgf("couldn't get upstream image %s cosign manifest", upstreamImageRef.DockerReference()) + + return err + } + + refs, err := getNotaryRefs(httpClient, *registryURL, remoteRepoCopy, upstreamImageDigest.String(), log) + if err != nil && !errors.Is(err, zerr.ErrSyncSignatureNotFound) { + log.Error().Err(err).Msgf("couldn't get upstream image %s notary references", upstreamImageRef.DockerReference()) + + return err + } + + // check if upstream image is signed + if cosignManifest == nil && len(refs.References) == 0 { + // upstream image not signed + if regCfg.OnlySigned != nil && *regCfg.OnlySigned { + // skip unsigned images + log.Info().Msgf("skipping image without signature %s", upstreamImageRef.DockerReference()) + + continue + } + } + + skipImage, err := canSkipImage(localRepo, tag, upstreamImageDigest.String(), imageStore, log) + if err != nil { + log.Error().Err(err).Msgf("couldn't check if the upstream image %s can be skipped", + upstreamImageRef.DockerReference()) + + return err + } + + // sync only differences + if skipImage { + log.Info().Msgf("already synced image %s, checking its signatures", upstreamImageRef.DockerReference()) + + skipNotarySig, err := canSkipNotarySignature(localRepo, tag, upstreamImageDigest.String(), + refs, imageStore, log) + if err != nil { + log.Error().Err(err).Msgf("couldn't check if the upstream image %s notary signature can be skipped", + upstreamImageRef.DockerReference()) + } + + if !skipNotarySig { + if err = retry.RetryIfNecessary(ctx, func() error { + err = syncNotarySignature(httpClient, imageStore, *registryURL, localRepo, remoteRepoCopy, + upstreamImageDigest.String(), refs, log) + + return err + }, retryOptions); err != nil { + log.Error().Err(err).Msgf("couldn't copy notary signature for %s", upstreamImageRef.DockerReference()) + } + } + + skipCosignSig, err := canSkipCosignSignature(localRepo, tag, upstreamImageDigest.String(), + cosignManifest, imageStore, log) + if err != nil { + log.Error().Err(err).Msgf("couldn't check if the upstream image %s cosign signature can be skipped", + upstreamImageRef.DockerReference()) + } + + if !skipCosignSig { + if err = retry.RetryIfNecessary(ctx, func() error { + err = syncCosignSignature(httpClient, imageStore, *registryURL, localRepo, remoteRepoCopy, + upstreamImageDigest.String(), cosignManifest, log) + + return err + }, retryOptions); err != nil { + log.Error().Err(err).Msgf("couldn't copy cosign signature for %s", upstreamImageRef.DockerReference()) + } + } continue } - } - skipImage, err := canSkipImage(localRepo, tag, upstreamImageDigest.String(), imageStore, log) - if err != nil { - log.Error().Err(err).Msgf("couldn't check if the upstream image %s can be skipped", - upstreamImageRef.DockerReference()) - - return err - } - - // sync only differences - if skipImage { - log.Info().Msgf("already synced image %s, checking its signatures", upstreamImageRef.DockerReference()) - - skipNotarySig, err := canSkipNotarySignature(localRepo, tag, upstreamImageDigest.String(), - refs, imageStore, log) + localImageRef, err := getLocalImageRef(localCachePath, localRepo, tag) if err != nil { - log.Error().Err(err).Msgf("couldn't check if the upstream image %s notary signature can be skipped", - upstreamImageRef.DockerReference()) + log.Error().Err(err).Msgf("couldn't obtain a valid image reference for reference %s/%s:%s", + localCachePath, localRepo, tag) + + return err } - if !skipNotarySig { - if err = retry.RetryIfNecessary(ctx, func() error { - err = syncNotarySignature(httpClient, imageStore, *registryURL, localRepo, remoteRepo, - upstreamImageDigest.String(), refs, log) + log.Info().Msgf("copying image %s to %s", upstreamImageRef.DockerReference(), localCachePath) - return err - }, retryOptions); err != nil { - log.Error().Err(err).Msgf("couldn't copy notary signature for %s", upstreamImageRef.DockerReference()) - } + if err = retry.RetryIfNecessary(ctx, func() error { + _, err = copy.Image(ctx, policyCtx, localImageRef, upstreamImageRef, &options) + + return err + }, retryOptions); err != nil { + log.Error().Err(err).Msgf("error while copying image %s to %s", + upstreamImageRef.DockerReference(), localCachePath) + + return err } + // push from cache to repo + err = pushSyncedLocalImage(localRepo, tag, localCachePath, imageStore, log) - skipCosignSig, err := canSkipCosignSignature(localRepo, tag, upstreamImageDigest.String(), - cosignManifest, imageStore, log) if err != nil { - log.Error().Err(err).Msgf("couldn't check if the upstream image %s cosign signature can be skipped", - upstreamImageRef.DockerReference()) + log.Error().Err(err).Msgf("error while pushing synced cached image %s", + fmt.Sprintf("%s/%s:%s", localCachePath, localRepo, tag)) + + return err } - if !skipCosignSig { - if err = retry.RetryIfNecessary(ctx, func() error { - err = syncCosignSignature(httpClient, imageStore, *registryURL, localRepo, remoteRepo, - upstreamImageDigest.String(), cosignManifest, log) + refs, err = getNotaryRefs(httpClient, *registryURL, remoteRepoCopy, upstreamImageDigest.String(), log) + if err = retry.RetryIfNecessary(ctx, func() error { + err = syncNotarySignature(httpClient, imageStore, *registryURL, localRepo, + remoteRepoCopy, upstreamImageDigest.String(), refs, log) - return err - }, retryOptions); err != nil { - log.Error().Err(err).Msgf("couldn't copy cosign signature for %s", upstreamImageRef.DockerReference()) - } + return err + }, retryOptions); err != nil { + log.Error().Err(err).Msgf("couldn't copy notary signature for %s", upstreamImageRef.DockerReference()) } - continue - } + cosignManifest, err = getCosignManifest(httpClient, *registryURL, remoteRepoCopy, + upstreamImageDigest.String(), log) + if err = retry.RetryIfNecessary(ctx, func() error { + err = syncCosignSignature(httpClient, imageStore, *registryURL, localRepo, + remoteRepoCopy, upstreamImageDigest.String(), cosignManifest, log) - localImageRef, localCachePath, err := getLocalImageRef(imageStore, localRepo, tag) - if err != nil { - log.Error().Err(err).Msgf("couldn't obtain a valid image reference for reference %s/%s:%s", - localCachePath, localRepo, tag) - - return err - } - - defer os.RemoveAll(localCachePath) - - log.Info().Msgf("copying image %s to %s", upstreamImageRef.DockerReference(), localCachePath) - - if err = retry.RetryIfNecessary(ctx, func() error { - _, err = copy.Image(ctx, policyCtx, localImageRef, upstreamImageRef, &options) - - return err - }, retryOptions); err != nil { - log.Error().Err(err).Msgf("error while copying image %s to %s", - upstreamImageRef.DockerReference(), localCachePath) - - return err - } - - err = pushSyncedLocalImage(localRepo, tag, localCachePath, imageStore, log) - if err != nil { - log.Error().Err(err).Msgf("error while pushing synced cached image %s", - fmt.Sprintf("%s/%s:%s", localCachePath, localRepo, tag)) - - return err - } - - if err = retry.RetryIfNecessary(ctx, func() error { - err = syncNotarySignature(httpClient, imageStore, *registryURL, localRepo, remoteRepo, upstreamImageDigest.String(), - refs, log) - - return err - }, retryOptions); err != nil { - log.Error().Err(err).Msgf("couldn't copy notary signature for %s", upstreamImageRef.DockerReference()) - } - - if err = retry.RetryIfNecessary(ctx, func() error { - err = syncCosignSignature(httpClient, imageStore, *registryURL, localRepo, remoteRepo, upstreamImageDigest.String(), - cosignManifest, log) - - return err - }, retryOptions); err != nil { - log.Error().Err(err).Msgf("couldn't copy cosign signature for %s", upstreamImageRef.DockerReference()) + return err + }, retryOptions); err != nil { + log.Error().Err(err).Msgf("couldn't copy cosign signature for %s", upstreamImageRef.DockerReference()) + } } } diff --git a/pkg/extensions/sync/sync_internal_test.go b/pkg/extensions/sync/sync_internal_test.go index 2227755f..ce2f39d6 100644 --- a/pkg/extensions/sync/sync_internal_test.go +++ b/pkg/extensions/sync/sync_internal_test.go @@ -61,11 +61,10 @@ func TestInjectSyncUtils(t *testing.T) { log := log.Logger{Logger: zerolog.New(os.Stdout)} metrics := monitoring.NewMetricsServer(false, log) - imageStore := storage.NewImageStore(t.TempDir(), false, storage.DefaultGCDelay, false, false, log, metrics) - injected = test.InjectFailure(0) - _, _, err = getLocalImageRef(imageStore, testImage, testImageTag) + + _, err = getLocalCachePath(imageStore, testImage) if injected { So(err, ShouldNotBeNil) } else { @@ -154,7 +153,7 @@ func TestSyncInternal(t *testing.T) { So(err, ShouldNotBeNil) }) - Convey("Verify getLocalImageRef()", t, func() { + Convey("Verify getLocalImageRef() and getLocalCachePath()", t, func() { log := log.Logger{Logger: zerolog.New(os.Stdout)} metrics := monitoring.NewMetricsServer(false, log) @@ -163,13 +162,36 @@ func TestSyncInternal(t *testing.T) { err := os.Chmod(imageStore.RootDir(), 0o000) So(err, ShouldBeNil) - _, _, err = getLocalImageRef(imageStore, testImage, testImageTag) + localCachePath, err := getLocalCachePath(imageStore, testImage) + So(err, ShouldNotBeNil) + + _, err = getLocalImageRef(localCachePath, testImage, testImageTag) + So(err, ShouldNotBeNil) + + err = os.Chmod(imageStore.RootDir(), 0o544) + So(err, ShouldBeNil) + + _, err = getLocalCachePath(imageStore, testImage) So(err, ShouldNotBeNil) err = os.Chmod(imageStore.RootDir(), 0o755) So(err, ShouldBeNil) - _, _, err = getLocalImageRef(imageStore, "zot][]321", "tag_tag][]") + localCachePath, err = getLocalCachePath(imageStore, testImage) + So(err, ShouldBeNil) + + testPath, _ := path.Split(localCachePath) + + err = os.Chmod(testPath, 0o544) + So(err, ShouldBeNil) + + _, err = getLocalCachePath(imageStore, testImage) + So(err, ShouldNotBeNil) + + err = os.Chmod(testPath, 0o755) + So(err, ShouldBeNil) + + _, err = getLocalImageRef(localCachePath, "zot][]321", "tag_tag][]") So(err, ShouldNotBeNil) }) @@ -471,15 +493,41 @@ func TestSyncInternal(t *testing.T) { panic(err) } - manifestConfigPath := path.Join(imageStore.RootDir(), testImage, "blobs", "sha256", manifest.Config.Digest.Hex()) - if err := os.MkdirAll(manifestConfigPath, 0o000); err != nil { + cachedManifestBackup, err := os.ReadFile(cachedManifestConfigPath) + if err != nil { + panic(err) + } + + configDigestBackup := manifest.Config.Digest + manifest.Config.Digest = "not what it needs to be" + manifestBuf, err := json.Marshal(manifest) + if err != nil { + panic(err) + } + + if err = os.WriteFile(cachedManifestConfigPath, manifestBuf, 0o600); err != nil { + panic(err) + } + + if err = os.Chmod(cachedManifestConfigPath, 0o755); err != nil { panic(err) } err = pushSyncedLocalImage(testImage, testImageTag, testRootDir, imageStore, log) So(err, ShouldNotBeNil) - if err := os.Remove(manifestConfigPath); err != nil { + manifest.Config.Digest = configDigestBackup + manifestBuf = cachedManifestBackup + + if err := os.Remove(cachedManifestConfigPath); err != nil { + panic(err) + } + + if err = os.WriteFile(cachedManifestConfigPath, manifestBuf, 0o600); err != nil { + panic(err) + } + + if err = os.Chmod(cachedManifestConfigPath, 0o755); err != nil { panic(err) } diff --git a/pkg/extensions/sync/sync_test.go b/pkg/extensions/sync/sync_test.go index fbd8de8c..a699b2e1 100644 --- a/pkg/extensions/sync/sync_test.go +++ b/pkg/extensions/sync/sync_test.go @@ -3295,8 +3295,8 @@ func TestSyncOnlyDiff(t *testing.T) { case <-done: return default: - _, err := os.ReadDir(path.Join(destDir, testImage, ".sync")) - if err == nil { + fileList, _ := os.ReadDir(path.Join(destDir, testImage, ".sync")) + if len(fileList) > 0 { isPopulated = true } time.Sleep(200 * time.Millisecond) diff --git a/pkg/extensions/sync/utils.go b/pkg/extensions/sync/utils.go index 1b90ca63..d33e6c62 100644 --- a/pkg/extensions/sync/utils.go +++ b/pkg/extensions/sync/utils.go @@ -44,14 +44,6 @@ func getTagFromRef(ref types.ImageReference, log log.Logger) reference.Tagged { return tagged } -// getRepoFromRef returns repo name from a registry ImageReference. -func getRepoFromRef(ref types.ImageReference, registryDomain string) string { - imageName := strings.Replace(ref.DockerReference().Name(), registryDomain, "", 1) - imageName = strings.TrimPrefix(imageName, "/") - - return imageName -} - // parseRepositoryReference parses input into a reference.Named, and verifies that it names a repository, not an image. func parseRepositoryReference(input string) (reference.Named, error) { ref, err := reference.ParseNormalizedNamed(input) @@ -296,11 +288,13 @@ func pushSyncedLocalImage(localRepo, tag, localCachePath string, return err } - _, _, err = imageStore.FullBlobUpload(localRepo, blobReader, blob.Digest.String()) - if err != nil { - log.Error().Err(err).Str("blob digest", blob.Digest.String()).Msg("couldn't upload blob") + if found, _, _ := imageStore.CheckBlob(localRepo, blob.Digest.String()); !found { + _, _, err = imageStore.FullBlobUpload(localRepo, blobReader, blob.Digest.String()) + if err != nil { + log.Error().Err(err).Str("blob digest", blob.Digest.String()).Msg("couldn't upload blob") - return err + return err + } } } @@ -312,11 +306,13 @@ func pushSyncedLocalImage(localRepo, tag, localCachePath string, return err } - _, _, err = imageStore.FullBlobUpload(localRepo, blobReader, manifest.Config.Digest.String()) - if err != nil { - log.Error().Err(err).Str("blob digest", manifest.Config.Digest.String()).Msg("couldn't upload config blob") + if found, _, _ := imageStore.CheckBlob(localRepo, manifest.Config.Digest.String()); !found { + _, _, err = imageStore.FullBlobUpload(localRepo, blobReader, manifest.Config.Digest.String()) + if err != nil { + log.Error().Err(err).Str("blob digest", manifest.Config.Digest.String()).Msg("couldn't upload config blob") - return err + return err + } } _, err = imageStore.PutImageManifest(localRepo, tag, ispec.MediaTypeImageManifest, manifestContent) @@ -326,14 +322,6 @@ func pushSyncedLocalImage(localRepo, tag, localCachePath string, return err } - log.Info().Msgf("removing temporary cached synced repo %s", path.Join(cacheImageStore.RootDir(), localRepo)) - - if err := os.RemoveAll(cacheImageStore.RootDir()); err != nil { - log.Error().Err(err).Msg("couldn't remove locally cached sync repo") - - return err - } - return nil } @@ -364,17 +352,9 @@ func getImageRef(registryDomain, repo, tag string) (types.ImageReference, error) } // get a local ImageReference used to temporary store one synced image. -func getLocalImageRef(imageStore storage.ImageStore, repo, tag string) (types.ImageReference, string, error) { - uuid, err := guuid.NewV4() - // hard to reach test case, injected error, see pkg/test/dev.go - if err := test.Error(err); err != nil { - return nil, "", err - } - - localCachePath := path.Join(imageStore.RootDir(), repo, SyncBlobUploadDir, uuid.String()) - - if err = os.MkdirAll(path.Join(localCachePath, repo), storage.DefaultDirPerms); err != nil { - return nil, "", err +func getLocalImageRef(localCachePath, repo, tag string) (types.ImageReference, error) { + if _, err := os.ReadDir(localCachePath); err != nil { + return nil, err } localRepo := path.Join(localCachePath, repo) @@ -382,10 +362,42 @@ func getLocalImageRef(imageStore storage.ImageStore, repo, tag string) (types.Im localImageRef, err := layout.ParseReference(localTaggedRepo) if err != nil { - return nil, "", err + return nil, err } - return localImageRef, localCachePath, nil + return localImageRef, nil +} + +// Returns the localCachePath with an UUID at the end. Only to be called once per repo. +func getLocalCachePath(imageStore storage.ImageStore, repo string) (string, error) { + localRepoPath := path.Join(imageStore.RootDir(), repo, SyncBlobUploadDir) + // check if SyncBlobUploadDir exists, create if not + var err error + if _, err = os.ReadDir(localRepoPath); os.IsNotExist(err) { + if err = os.MkdirAll(localRepoPath, storage.DefaultDirPerms); err != nil { + return "", err + } + } + + if err != nil { + return "", err + } + + // create uuid folder + uuid, err := guuid.NewV4() + // hard to reach test case, injected error, see pkg/test/dev.go + if err := test.Error(err); err != nil { + return "", err + } + + localCachePath := path.Join(localRepoPath, uuid.String()) + + cachedRepoPath := path.Join(localCachePath, repo) + if err = os.MkdirAll(cachedRepoPath, storage.DefaultDirPerms); err != nil { + return "", err + } + + return localCachePath, nil } // canSkipImage returns whether or not we already synced this image.