diff --git a/src/internal/packager/images/pull.go b/src/internal/packager/images/pull.go index 61210d43c1..88be157c3c 100644 --- a/src/internal/packager/images/pull.go +++ b/src/internal/packager/images/pull.go @@ -9,16 +9,18 @@ import ( "encoding/json" "errors" "fmt" - "github.com/zarf-dev/zarf/src/pkg/logger" "io/fs" "maps" "os" "path/filepath" + "runtime" "strings" "sync" "sync/atomic" "time" + "github.com/zarf-dev/zarf/src/pkg/logger" + "github.com/avast/retry-go/v4" "github.com/defenseunicorns/pkg/helpers/v2" "github.com/google/go-containerregistry/pkg/crane" @@ -34,8 +36,6 @@ import ( "github.com/google/go-containerregistry/pkg/v1/types" "github.com/moby/moby/client" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - "github.com/zarf-dev/zarf/src/config" - "github.com/zarf-dev/zarf/src/pkg/layout" "github.com/zarf-dev/zarf/src/pkg/message" "github.com/zarf-dev/zarf/src/pkg/transform" "github.com/zarf-dev/zarf/src/pkg/utils" @@ -249,7 +249,7 @@ func Pull(ctx context.Context, cfg PullConfig) (map[transform.Image]v1.Image, er toPull := maps.Clone(fetched) err = retry.Do(func() error { - saved, err := SaveConcurrent(ctx, cranePath, toPull) + saved, err := SaveConcurrent(ctx, cranePath, toPull, cfg.CacheDirectory) // Done save, remove from download list. for k := range saved { delete(toPull, k) @@ -264,7 +264,7 @@ func Pull(ctx context.Context, cfg PullConfig) (map[transform.Image]v1.Image, er message.Warnf("Failed to save images in parallel, falling back to sequential save: %s", err.Error()) l.Warn("failed to save images in parallel, falling back to sequential save", "error", err.Error()) err = retry.Do(func() error { - saved, err := SaveSequential(ctx, cranePath, toPull) + saved, err := SaveSequential(ctx, cranePath, toPull, cfg.CacheDirectory) for k := range saved { delete(toPull, k) } @@ -313,8 +313,19 @@ func Pull(ctx context.Context, cfg PullConfig) (map[transform.Image]v1.Image, er return fetched, nil } +// from https://github.com/google/go-containerregistry/blob/6bce25ecf0297c1aa9072bc665b5cf58d53e1c54/pkg/v1/cache/fs.go#L143 +func layerCachePath(path string, h v1.Hash) string { + var file string + if runtime.GOOS == "windows" { + file = fmt.Sprintf("%s-%s", h.Algorithm, h.Hex) + } else { + file = h.String() + } + return filepath.Join(path, file) +} + // CleanupInProgressLayers removes incomplete layers from the cache. -func CleanupInProgressLayers(ctx context.Context, img v1.Image) error { +func CleanupInProgressLayers(ctx context.Context, img v1.Image, cacheDirectory string) error { layers, err := img.Layers() if err != nil { return err @@ -331,12 +342,7 @@ func CleanupInProgressLayers(ctx context.Context, img v1.Image) error { if err != nil { return err } - absPath, err := config.GetAbsCachePath() - if err != nil { - return err - } - cacheDir := filepath.Join(absPath, layout.ImagesDir) - location := filepath.Join(cacheDir, digest.String()) + location := layerCachePath(cacheDirectory, digest) info, err := os.Stat(location) if errors.Is(err, fs.ErrNotExist) { return nil @@ -356,7 +362,7 @@ func CleanupInProgressLayers(ctx context.Context, img v1.Image) error { } // SaveSequential saves images sequentially. -func SaveSequential(ctx context.Context, cl clayout.Path, m map[transform.Image]v1.Image) (map[transform.Image]v1.Image, error) { +func SaveSequential(ctx context.Context, cl clayout.Path, m map[transform.Image]v1.Image, cacheDirectory string) (map[transform.Image]v1.Image, error) { l := logger.From(ctx) saved := map[transform.Image]v1.Image{} for info, img := range m { @@ -370,7 +376,7 @@ func SaveSequential(ctx context.Context, cl clayout.Path, m map[transform.Image] } l.Info("saving image", "ref", info.Reference, "size", size, "method", "sequential") if err := cl.AppendImage(img, clayout.WithAnnotations(annotations)); err != nil { - if err := CleanupInProgressLayers(ctx, img); err != nil { + if err := CleanupInProgressLayers(ctx, img, cacheDirectory); err != nil { message.WarnErr(err, "failed to clean up in-progress layers, please run `zarf tools clear-cache`") l.Error("failed to clean up in-progress layers. please run `zarf tools clear-cache`") } @@ -388,7 +394,7 @@ func SaveSequential(ctx context.Context, cl clayout.Path, m map[transform.Image] } // SaveConcurrent saves images in a concurrent, bounded manner. -func SaveConcurrent(ctx context.Context, cl clayout.Path, m map[transform.Image]v1.Image) (map[transform.Image]v1.Image, error) { +func SaveConcurrent(ctx context.Context, cl clayout.Path, m map[transform.Image]v1.Image, cacheDirectory string) (map[transform.Image]v1.Image, error) { l := logger.From(ctx) saved := map[transform.Image]v1.Image{} @@ -416,7 +422,7 @@ func SaveConcurrent(ctx context.Context, cl clayout.Path, m map[transform.Image] wStart := time.Now() l.Info("saving image", "ref", info.Reference, "size", size, "method", "concurrent") if err := cl.WriteImage(img); err != nil { - if err := CleanupInProgressLayers(ectx, img); err != nil { + if err := CleanupInProgressLayers(ectx, img, cacheDirectory); err != nil { message.WarnErr(err, "failed to clean up in-progress layers, please run `zarf tools clear-cache`") l.Error("failed to clean up in-progress layers. please run `zarf tools clear-cache`") } diff --git a/src/internal/packager/images/pull_test.go b/src/internal/packager/images/pull_test.go index 0bc7903997..87b3a99b17 100644 --- a/src/internal/packager/images/pull_test.go +++ b/src/internal/packager/images/pull_test.go @@ -104,6 +104,7 @@ func TestPull(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() ref, err := transform.ParseImageRef(tc.ref) require.NoError(t, err) destDir := t.TempDir() @@ -133,6 +134,7 @@ func TestPull(t *testing.T) { } t.Run("pulling a cosign image is successful and doesn't add anything to the cache", func(t *testing.T) { + t.Parallel() ref, err := transform.ParseImageRef("ghcr.io/stefanprodan/podinfo:sha256-57a654ace69ec02ba8973093b6a786faa15640575fbf0dbb603db55aca2ccec8.sig") require.NoError(t, err) destDir := t.TempDir() @@ -153,4 +155,38 @@ func TestPull(t *testing.T) { require.NoError(t, err) require.Empty(t, dir) }) + + t.Run("pulling an image with an invalid layer in the cache should still pull the image", func(t *testing.T) { + t.Parallel() + ref, err := transform.ParseImageRef("ghcr.io/fluxcd/image-automation-controller@sha256:48a89734dc82c3a2d4138554b3ad4acf93230f770b3a582f7f48be38436d031c") + require.NoError(t, err) + destDir := t.TempDir() + cacheDir := t.TempDir() + require.NoError(t, err) + invalidContent := []byte("this text here is not the valid layer that the image is looking for") + hash, err := v1.NewHash("sha256:d94c8059c3cffb9278601bf9f8be070d50c84796401a4c5106eb8a4042445bbc") + require.NoError(t, err) + invalidLayerPath := layerCachePath(cacheDir, hash) + err = os.WriteFile(invalidLayerPath, invalidContent, 0777) + require.NoError(t, err) + pullConfig := PullConfig{ + DestinationDirectory: destDir, + CacheDirectory: cacheDir, + ImageList: []transform.Image{ + ref, + }, + } + + _, err = Pull(context.Background(), pullConfig) + + // Verify the cache is fixed and the new image layer was pulled + require.NoError(t, err) + nowValidContents, err := os.ReadFile(invalidLayerPath) + require.NoError(t, err) + pulledLayerPath := filepath.Join(destDir, "blobs", "sha256", hash.Hex) + pulledLayer, err := os.ReadFile(pulledLayerPath) + require.NoError(t, err) + require.Equal(t, nowValidContents, pulledLayer) + require.NotEqual(t, nowValidContents, invalidContent) + }) }