From 557c03f4af02532ac5107b9fd860e05960982609 Mon Sep 17 00:00:00 2001 From: Joe Lanford Date: Sun, 2 Feb 2025 14:50:33 -0500 Subject: [PATCH] consolidate image layer handling; move fs utils Signed-off-by: Joe Lanford --- .golangci.yaml | 2 + catalogd/cmd/catalogd/main.go | 2 +- catalogd/internal/source/containers_image.go | 77 ++---------- cmd/operator-controller/main.go | 2 +- internal/rukpak/source/containers_image.go | 66 ++-------- .../rukpak/source/containers_image_test.go | 2 +- internal/rukpak/source/helpers.go | 24 ---- internal/rukpak/source/helpers_test.go | 47 ------- internal/{fsutil/helpers.go => util/fs/fs.go} | 27 +++- .../helpers_test.go => util/fs/fs_test.go} | 45 ++++++- internal/util/image/layers.go | 118 ++++++++++++++++++ .../util/image/layers_test.go | 6 +- 12 files changed, 213 insertions(+), 205 deletions(-) delete mode 100644 internal/rukpak/source/helpers.go delete mode 100644 internal/rukpak/source/helpers_test.go rename internal/{fsutil/helpers.go => util/fs/fs.go} (75%) rename internal/{fsutil/helpers_test.go => util/fs/fs_test.go} (76%) create mode 100644 internal/util/image/layers.go rename catalogd/internal/source/containers_image_internal_test.go => internal/util/image/layers_test.go (95%) diff --git a/.golangci.yaml b/.golangci.yaml index 1ecb40994..2be54a329 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -66,6 +66,8 @@ linters-settings: alias: ctrl - pkg: github.com/blang/semver/v4 alias: bsemver + - pkg: "^github.com/operator-framework/operator-controller/internal/util/([^/]+)$" + alias: "${1}util" output: formats: diff --git a/catalogd/cmd/catalogd/main.go b/catalogd/cmd/catalogd/main.go index ff86c9b05..9eba85510 100644 --- a/catalogd/cmd/catalogd/main.go +++ b/catalogd/cmd/catalogd/main.go @@ -63,7 +63,7 @@ import ( "github.com/operator-framework/operator-controller/catalogd/internal/storage" "github.com/operator-framework/operator-controller/catalogd/internal/version" "github.com/operator-framework/operator-controller/catalogd/internal/webhook" - "github.com/operator-framework/operator-controller/internal/fsutil" + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" ) var ( diff --git a/catalogd/internal/source/containers_image.go b/catalogd/internal/source/containers_image.go index 7da305d69..362cc649f 100644 --- a/catalogd/internal/source/containers_image.go +++ b/catalogd/internal/source/containers_image.go @@ -1,25 +1,18 @@ package source import ( - "archive/tar" "context" "errors" "fmt" - "io" "os" - "path" "path/filepath" - "strings" "time" - "github.com/containerd/containerd/archive" "github.com/containers/image/v5/copy" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/oci/layout" - "github.com/containers/image/v5/pkg/blobinfocache/none" - "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/signature" "github.com/containers/image/v5/types" @@ -30,8 +23,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" catalogdv1 "github.com/operator-framework/operator-controller/catalogd/api/v1" - "github.com/operator-framework/operator-controller/internal/fsutil" - "github.com/operator-framework/operator-controller/internal/rukpak/source" + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" + imageutil "github.com/operator-framework/operator-controller/internal/util/image" ) const ConfigDirLabel = "operators.operatorframework.io.index.configs.v1" @@ -78,10 +71,14 @@ func (i *ContainersImageRegistry) Unpack(ctx context.Context, catalog *catalogdv // ////////////////////////////////////////////////////// unpackPath := i.unpackPath(catalog.Name, canonicalRef.Digest()) - if isUnpacked, unpackTime, err := source.IsImageUnpacked(unpackPath); isUnpacked && err == nil { + if unpackTime, err := fsutil.GetDirectoryModTime(unpackPath); err == nil { l.Info("image already unpacked", "ref", imgRef.String(), "digest", canonicalRef.Digest().String()) return successResult(unpackPath, canonicalRef, unpackTime), nil - } else if err != nil { + } else if errors.Is(err, fsutil.ErrNotDirectory) { + if err := fsutil.DeleteReadOnlyRecursive(unpackPath); err != nil { + return nil, err + } + } else if err != nil && !os.IsNotExist(err) { return nil, fmt.Errorf("error checking image already unpacked: %w", err) } @@ -297,59 +294,11 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st return wrapTerminal(fmt.Errorf("catalog image is missing the required label %q", ConfigDirLabel), specIsCanonical) } - if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil { - return fmt.Errorf("error ensuring empty unpack directory: %w", err) - } - l := log.FromContext(ctx) - l.Info("unpacking image", "path", unpackPath) - for i, layerInfo := range img.LayerInfos() { - if err := func() error { - layerReader, _, err := layoutSrc.GetBlob(ctx, layerInfo, none.NoCache) - if err != nil { - return fmt.Errorf("error getting blob for layer[%d]: %w", i, err) - } - defer layerReader.Close() - - if err := applyLayer(ctx, unpackPath, dirToUnpack, layerReader); err != nil { - return fmt.Errorf("error applying layer[%d]: %w", i, err) - } - l.Info("applied layer", "layer", i) - return nil - }(); err != nil { - return errors.Join(err, fsutil.DeleteReadOnlyRecursive(unpackPath)) - } - } - if err := fsutil.SetReadOnlyRecursive(unpackPath); err != nil { - return fmt.Errorf("error making unpack directory read-only: %w", err) - } - return nil -} - -func applyLayer(ctx context.Context, destPath string, srcPath string, layer io.ReadCloser) error { - decompressed, _, err := compression.AutoDecompress(layer) - if err != nil { - return fmt.Errorf("auto-decompress failed: %w", err) - } - defer decompressed.Close() - - _, err = archive.Apply(ctx, destPath, decompressed, archive.WithFilter(applyLayerFilter(srcPath))) - return err -} - -func applyLayerFilter(srcPath string) archive.Filter { - cleanSrcPath := path.Clean(strings.TrimPrefix(srcPath, "/")) - return func(h *tar.Header) (bool, error) { - h.Uid = os.Getuid() - h.Gid = os.Getgid() - h.Mode |= 0700 - - cleanName := path.Clean(strings.TrimPrefix(h.Name, "/")) - relPath, err := filepath.Rel(cleanSrcPath, cleanName) - if err != nil { - return false, fmt.Errorf("error getting relative path: %w", err) - } - return relPath != ".." && !strings.HasPrefix(relPath, "../"), nil - } + applyFilter := imageutil.AllFilters( + imageutil.OnlyPath(dirToUnpack), + imageutil.ForceOwnershipRWX(), + ) + return imageutil.ApplyLayersToDisk(ctx, unpackPath, img, layoutSrc, applyFilter) } func (i *ContainersImageRegistry) deleteOtherImages(catalogName string, digestToKeep digest.Digest) error { diff --git a/cmd/operator-controller/main.go b/cmd/operator-controller/main.go index 21fb05628..d9a544371 100644 --- a/cmd/operator-controller/main.go +++ b/cmd/operator-controller/main.go @@ -63,12 +63,12 @@ import ( "github.com/operator-framework/operator-controller/internal/controllers" "github.com/operator-framework/operator-controller/internal/features" "github.com/operator-framework/operator-controller/internal/finalizers" - "github.com/operator-framework/operator-controller/internal/fsutil" "github.com/operator-framework/operator-controller/internal/httputil" "github.com/operator-framework/operator-controller/internal/resolve" "github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety" "github.com/operator-framework/operator-controller/internal/rukpak/source" "github.com/operator-framework/operator-controller/internal/scheme" + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" "github.com/operator-framework/operator-controller/internal/version" ) diff --git a/internal/rukpak/source/containers_image.go b/internal/rukpak/source/containers_image.go index 50eade4f1..01eb55a8b 100644 --- a/internal/rukpak/source/containers_image.go +++ b/internal/rukpak/source/containers_image.go @@ -1,22 +1,17 @@ package source import ( - "archive/tar" "context" "errors" "fmt" - "io" "os" "path/filepath" - "github.com/containerd/containerd/archive" "github.com/containers/image/v5/copy" "github.com/containers/image/v5/docker" "github.com/containers/image/v5/docker/reference" "github.com/containers/image/v5/manifest" "github.com/containers/image/v5/oci/layout" - "github.com/containers/image/v5/pkg/blobinfocache/none" - "github.com/containers/image/v5/pkg/compression" "github.com/containers/image/v5/pkg/sysregistriesv2" "github.com/containers/image/v5/signature" "github.com/containers/image/v5/types" @@ -25,7 +20,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/operator-framework/operator-controller/internal/fsutil" + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" + imageutil "github.com/operator-framework/operator-controller/internal/util/image" ) var insecurePolicy = []byte(`{"default":[{"type":"insecureAcceptAnything"}]}`) @@ -71,11 +67,15 @@ func (i *ContainersImageRegistry) Unpack(ctx context.Context, bundle *BundleSour // ////////////////////////////////////////////////////// unpackPath := i.unpackPath(bundle.Name, canonicalRef.Digest()) - if isUnpacked, _, err := IsImageUnpacked(unpackPath); isUnpacked && err == nil { + if _, err := fsutil.GetDirectoryModTime(unpackPath); err == nil { l.Info("image already unpacked", "ref", imgRef.String(), "digest", canonicalRef.Digest().String()) return successResult(bundle.Name, unpackPath, canonicalRef), nil - } else if err != nil { - return nil, fmt.Errorf("error checking bundle already unpacked: %w", err) + } else if errors.Is(err, fsutil.ErrNotDirectory) { + if err := fsutil.DeleteReadOnlyRecursive(unpackPath); err != nil { + return nil, err + } + } else if err != nil && !os.IsNotExist(err) { + return nil, fmt.Errorf("error checking image already unpacked: %w", err) } ////////////////////////////////////////////////////// @@ -265,53 +265,7 @@ func (i *ContainersImageRegistry) unpackImage(ctx context.Context, unpackPath st panic(err) } }() - - if err := fsutil.EnsureEmptyDirectory(unpackPath, 0700); err != nil { - return fmt.Errorf("error ensuring empty unpack directory: %w", err) - } - l := log.FromContext(ctx) - l.Info("unpacking image", "path", unpackPath) - for i, layerInfo := range img.LayerInfos() { - if err := func() error { - layerReader, _, err := layoutSrc.GetBlob(ctx, layerInfo, none.NoCache) - if err != nil { - return fmt.Errorf("error getting blob for layer[%d]: %w", i, err) - } - defer layerReader.Close() - - if err := applyLayer(ctx, unpackPath, layerReader); err != nil { - return fmt.Errorf("error applying layer[%d]: %w", i, err) - } - l.Info("applied layer", "layer", i) - return nil - }(); err != nil { - return errors.Join(err, fsutil.DeleteReadOnlyRecursive(unpackPath)) - } - } - if err := fsutil.SetReadOnlyRecursive(unpackPath); err != nil { - return fmt.Errorf("error making unpack directory read-only: %w", err) - } - return nil -} - -func applyLayer(ctx context.Context, unpackPath string, layer io.ReadCloser) error { - decompressed, _, err := compression.AutoDecompress(layer) - if err != nil { - return fmt.Errorf("auto-decompress failed: %w", err) - } - defer decompressed.Close() - - _, err = archive.Apply(ctx, unpackPath, decompressed, archive.WithFilter(applyLayerFilter())) - return err -} - -func applyLayerFilter() archive.Filter { - return func(h *tar.Header) (bool, error) { - h.Uid = os.Getuid() - h.Gid = os.Getgid() - h.Mode |= 0700 - return true, nil - } + return imageutil.ApplyLayersToDisk(ctx, unpackPath, img, layoutSrc, imageutil.ForceOwnershipRWX()) } func (i *ContainersImageRegistry) deleteOtherImages(bundleName string, digestToKeep digest.Digest) error { diff --git a/internal/rukpak/source/containers_image_test.go b/internal/rukpak/source/containers_image_test.go index 772053f1b..c8639dd78 100644 --- a/internal/rukpak/source/containers_image_test.go +++ b/internal/rukpak/source/containers_image_test.go @@ -22,8 +22,8 @@ import ( "github.com/stretchr/testify/require" "sigs.k8s.io/controller-runtime/pkg/reconcile" - "github.com/operator-framework/operator-controller/internal/fsutil" "github.com/operator-framework/operator-controller/internal/rukpak/source" + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" ) const ( diff --git a/internal/rukpak/source/helpers.go b/internal/rukpak/source/helpers.go deleted file mode 100644 index 32c8d42d4..000000000 --- a/internal/rukpak/source/helpers.go +++ /dev/null @@ -1,24 +0,0 @@ -package source - -import ( - "errors" - "os" - "time" -) - -// IsImageUnpacked checks whether an image has been unpacked in `unpackPath`. -// If true, time of unpack will also be returned. If false unpack time is gibberish (zero/epoch time). -// If `unpackPath` is a file, it will be deleted and false will be returned without an error. -func IsImageUnpacked(unpackPath string) (bool, time.Time, error) { - unpackStat, err := os.Stat(unpackPath) - if errors.Is(err, os.ErrNotExist) { - return false, time.Time{}, nil - } - if err != nil { - return false, time.Time{}, err - } - if !unpackStat.IsDir() { - return false, time.Time{}, os.Remove(unpackPath) - } - return true, unpackStat.ModTime(), nil -} diff --git a/internal/rukpak/source/helpers_test.go b/internal/rukpak/source/helpers_test.go deleted file mode 100644 index de0106091..000000000 --- a/internal/rukpak/source/helpers_test.go +++ /dev/null @@ -1,47 +0,0 @@ -package source_test - -import ( - "os" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/operator-framework/operator-controller/internal/rukpak/source" -) - -func TestIsImageUnpacked(t *testing.T) { - tempDir := t.TempDir() - unpackPath := filepath.Join(tempDir, "myimage") - - t.Log("Test case: unpack path does not exist") - unpacked, modTime, err := source.IsImageUnpacked(unpackPath) - require.NoError(t, err) - require.False(t, unpacked) - require.True(t, modTime.IsZero()) - - t.Log("Test case: unpack path points to file") - require.NoError(t, os.WriteFile(unpackPath, []byte("test"), 0600)) - - unpacked, modTime, err = source.IsImageUnpacked(filepath.Join(tempDir, "myimage")) - require.NoError(t, err) - require.False(t, unpacked) - require.True(t, modTime.IsZero()) - - t.Log("Expect file to be deleted") - _, err = os.Stat(unpackPath) - require.ErrorIs(t, err, os.ErrNotExist) - - t.Log("Test case: unpack path points to directory (happy path)") - require.NoError(t, os.Mkdir(unpackPath, 0700)) - - unpacked, modTime, err = source.IsImageUnpacked(unpackPath) - require.NoError(t, err) - require.True(t, unpacked) - require.False(t, modTime.IsZero()) - - t.Log("Expect unpack time to match directory mod time") - stat, err := os.Stat(unpackPath) - require.NoError(t, err) - require.Equal(t, stat.ModTime(), modTime) -} diff --git a/internal/fsutil/helpers.go b/internal/util/fs/fs.go similarity index 75% rename from internal/fsutil/helpers.go rename to internal/util/fs/fs.go index 6f11ce1c2..f6ba9ec57 100644 --- a/internal/fsutil/helpers.go +++ b/internal/util/fs/fs.go @@ -1,10 +1,12 @@ -package fsutil +package fs import ( + "errors" "fmt" "io/fs" "os" "path/filepath" + "time" ) // EnsureEmptyDirectory ensures the directory given by `path` is empty. @@ -42,7 +44,10 @@ func SetWritableRecursive(root string) error { return setModeRecursive(root, ownerWritableFileMode, ownerWritableDirMode) } -// DeleteReadOnlyRecursive deletes read-only directory with path given by `root` +// DeleteReadOnlyRecursive deletes the directory with path given by `root`. +// Prior to deleting the directory, the directory and all descendant files +// and directories are set as writable. If any chmod or deletion error occurs +// it is immediately returned. func DeleteReadOnlyRecursive(root string) error { if err := SetWritableRecursive(root); err != nil { return fmt.Errorf("error making directory writable for deletion: %w", err) @@ -78,3 +83,21 @@ func setModeRecursive(path string, fileMode os.FileMode, dirMode os.FileMode) er } }) } + +var ( + ErrNotDirectory = errors.New("not a directory") +) + +// GetDirectoryModTime returns the modification time of the directory at dirPath. +// If stat(dirPath) fails, an error is returned with a zero-value time.Time. +// If dirPath is not a directory, an ErrNotDirectory error is returned. +func GetDirectoryModTime(dirPath string) (time.Time, error) { + dirStat, err := os.Stat(dirPath) + if err != nil { + return time.Time{}, err + } + if !dirStat.IsDir() { + return time.Time{}, ErrNotDirectory + } + return dirStat.ModTime(), nil +} diff --git a/internal/fsutil/helpers_test.go b/internal/util/fs/fs_test.go similarity index 76% rename from internal/fsutil/helpers_test.go rename to internal/util/fs/fs_test.go index 4f397f2aa..00579a51f 100644 --- a/internal/fsutil/helpers_test.go +++ b/internal/util/fs/fs_test.go @@ -1,4 +1,4 @@ -package fsutil +package fs import ( "io/fs" @@ -22,15 +22,15 @@ func TestEnsureEmptyDirectory(t *testing.T) { require.True(t, stat.IsDir()) require.Equal(t, dirPerms, stat.Mode().Perm()) - t.Log("Create a file inside directory") + t.Log("Create a read-only file inside directory") file := filepath.Join(dirPath, "file1") // write file as read-only to verify EnsureEmptyDirectory can still delete it. - require.NoError(t, os.WriteFile(file, []byte("test"), 0400)) + require.NoError(t, os.WriteFile(file, []byte("test"), ownerReadOnlyDirMode)) - t.Log("Create a sub-directory inside directory") + t.Log("Create a read-only sub-directory inside directory") subDir := filepath.Join(dirPath, "subdir") // write subDir as read-execute-only to verify EnsureEmptyDirectory can still delete it. - require.NoError(t, os.Mkdir(subDir, 0500)) + require.NoError(t, os.Mkdir(subDir, ownerReadOnlyDirMode)) t.Log("Call EnsureEmptyDirectory against directory with different permissions") require.NoError(t, EnsureEmptyDirectory(dirPath, 0640)) @@ -62,7 +62,7 @@ func TestSetReadOnlyRecursive(t *testing.T) { require.NoError(t, os.WriteFile(filePath, []byte("test"), ownerWritableFileMode)) require.NoError(t, os.Symlink(targetFilePath, symlinkPath)) - t.Log("Set directory structure as read-only") + t.Log("Set directory structure as read-only via DeleteReadOnlyRecursive") require.NoError(t, SetReadOnlyRecursive(nestedDir)) t.Log("Check file permissions") @@ -139,3 +139,36 @@ func TestDeleteReadOnlyRecursive(t *testing.T) { _, err := os.Stat(nestedDir) require.ErrorIs(t, err, os.ErrNotExist) } + +func TestGetDirectoryModTime(t *testing.T) { + tempDir := t.TempDir() + unpackPath := filepath.Join(tempDir, "myimage") + + t.Log("Test case: unpack path does not exist") + modTime, err := GetDirectoryModTime(unpackPath) + require.ErrorIs(t, err, os.ErrNotExist) + require.True(t, modTime.IsZero()) + + t.Log("Test case: unpack path points to file") + require.NoError(t, os.WriteFile(unpackPath, []byte("test"), ownerWritableFileMode)) + + modTime, err = GetDirectoryModTime(filepath.Join(tempDir, "myimage")) + require.ErrorIs(t, err, ErrNotDirectory) + require.True(t, modTime.IsZero()) + + t.Log("Expect file still exists and then clean it up") + require.FileExists(t, unpackPath) + require.NoError(t, os.Remove(unpackPath)) + + t.Log("Test case: unpack path points to directory (happy path)") + require.NoError(t, os.Mkdir(unpackPath, ownerWritableDirMode)) + + modTime, err = GetDirectoryModTime(unpackPath) + require.NoError(t, err) + require.False(t, modTime.IsZero()) + + t.Log("Expect unpack time to match directory mod time") + stat, err := os.Stat(unpackPath) + require.NoError(t, err) + require.Equal(t, stat.ModTime(), modTime) +} diff --git a/internal/util/image/layers.go b/internal/util/image/layers.go new file mode 100644 index 000000000..1038fdb81 --- /dev/null +++ b/internal/util/image/layers.go @@ -0,0 +1,118 @@ +package image + +import ( + "archive/tar" + "context" + "errors" + "fmt" + "os" + "path" + "path/filepath" + "strings" + + "github.com/containerd/containerd/archive" + "github.com/containers/image/v5/pkg/blobinfocache/none" + "github.com/containers/image/v5/pkg/compression" + "github.com/containers/image/v5/types" + "sigs.k8s.io/controller-runtime/pkg/log" + + fsutil "github.com/operator-framework/operator-controller/internal/util/fs" +) + +// ForceOwnershipRWX is a passthrough archive.Filter that sets a tar header's +// Uid and Gid to the current process's Uid and Gid and ensures its permissions +// give the owner full read/write/execute permission. The process Uid and Gid +// are determined when ForceOwnershipRWX is called, not when the filter function +// is called. +func ForceOwnershipRWX() archive.Filter { + uid := os.Getuid() + gid := os.Getgid() + return func(h *tar.Header) (bool, error) { + h.Uid = uid + h.Gid = gid + h.Mode |= 0700 + return true, nil + } +} + +// OnlyPath is an archive.Filter that keeps only files and directories that match p, or +// (if p is a directory) are present under p. OnlyPath does not remap files to a new location. +// If an error occurs while comparing the desired path prefix with the tar header's name, the +// filter will return false with that error. +func OnlyPath(p string) archive.Filter { + wantPath := path.Clean(strings.TrimPrefix(p, "/")) + return func(h *tar.Header) (bool, error) { + headerPath := path.Clean(strings.TrimPrefix(h.Name, "/")) + relPath, err := filepath.Rel(wantPath, headerPath) + if err != nil { + return false, fmt.Errorf("error getting relative path: %w", err) + } + if relPath == ".." || strings.HasPrefix(relPath, "../") { + return false, nil + } + return true, nil + } +} + +// AllFilters is a composite archive.Filter that executes each filter in the order +// they are given. If any filter returns false or an error, the composite filter will immediately +// return that result to the caller, and no further filters are executed. +func AllFilters(filters ...archive.Filter) archive.Filter { + return func(h *tar.Header) (bool, error) { + for _, filter := range filters { + keep, err := filter(h) + if err != nil { + return false, err + } + if !keep { + return false, nil + } + } + return true, nil + } +} + +// ApplyLayersToDisk writes the layers from img and imgSrc to disk using the provided filter. +// The destination directory will be created, if necessary. If dest is already present, its +// contents will be deleted. If img and imgSrc do not represent the same image, an error will +// be returned due to a mismatch in the expected layers. Once complete, the dest and its contents +// are marked as read-only to provide a safeguard against unintended changes. +func ApplyLayersToDisk(ctx context.Context, dest string, img types.Image, imgSrc types.ImageSource, filter archive.Filter) error { + var applyOpts []archive.ApplyOpt + if filter != nil { + applyOpts = append(applyOpts, archive.WithFilter(filter)) + } + + if err := fsutil.EnsureEmptyDirectory(dest, 0700); err != nil { + return fmt.Errorf("error ensuring empty unpack directory: %w", err) + } + l := log.FromContext(ctx) + l.Info("unpacking image", "path", dest) + for i, layerInfo := range img.LayerInfos() { + if err := func() error { + layerReader, _, err := imgSrc.GetBlob(ctx, layerInfo, none.NoCache) + if err != nil { + return fmt.Errorf("error getting blob for layer[%d]: %w", i, err) + } + defer layerReader.Close() + + decompressed, _, err := compression.AutoDecompress(layerReader) + if err != nil { + return fmt.Errorf("auto-decompress failed: %w", err) + } + defer decompressed.Close() + + if _, err := archive.Apply(ctx, dest, decompressed, applyOpts...); err != nil { + return fmt.Errorf("error applying layer[%d]: %w", i, err) + } + l.Info("applied layer", "layer", i) + return nil + }(); err != nil { + return errors.Join(err, fsutil.DeleteReadOnlyRecursive(dest)) + } + } + if err := fsutil.SetReadOnlyRecursive(dest); err != nil { + return fmt.Errorf("error making unpack directory read-only: %w", err) + } + return nil +} diff --git a/catalogd/internal/source/containers_image_internal_test.go b/internal/util/image/layers_test.go similarity index 95% rename from catalogd/internal/source/containers_image_internal_test.go rename to internal/util/image/layers_test.go index 0c3ba1286..0369eb364 100644 --- a/catalogd/internal/source/containers_image_internal_test.go +++ b/internal/util/image/layers_test.go @@ -1,4 +1,4 @@ -package source +package image import ( "archive/tar" @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestContainersImage_applyLayerFilter(t *testing.T) { +func TestOnlyPath(t *testing.T) { type testCase struct { name string srcPaths []string @@ -119,7 +119,7 @@ func TestContainersImage_applyLayerFilter(t *testing.T) { } { t.Run(tc.name, func(t *testing.T) { for _, srcPath := range tc.srcPaths { - f := applyLayerFilter(srcPath) + f := OnlyPath(srcPath) for _, tarHeader := range tc.tarHeaders { keep, err := f(&tarHeader) tc.assertion(&tarHeader, keep, err)