From 3829fbd35a59ecdcda73cfbf814f75a5b2e032e6 Mon Sep 17 00:00:00 2001 From: Aditya R Date: Tue, 13 Jun 2023 11:11:42 +0530 Subject: [PATCH 1/2] podman: add support for splitting imagestore Add support for `--imagestore` in podman which allows users to split the filesystem of containers vs image store, imagestore if configured will pull images in image storage instead of the graphRoot while keeping the other parts still in the originally configured graphRoot. This is an implementation of https://github.com/containers/storage/pull/1549 in podman. Signed-off-by: Aditya R --- cmd/podman/root.go | 4 ++ docs/source/markdown/podman.1.md | 6 +++ libpod/options.go | 12 ++++++ pkg/domain/entities/engine.go | 1 + pkg/domain/infra/runtime_libpod.go | 4 ++ pkg/specgenutil/util.go | 3 ++ test/e2e/pull_test.go | 67 ++++++++++++++++++++++++++++++ 7 files changed, 97 insertions(+) diff --git a/cmd/podman/root.go b/cmd/podman/root.go index 3b8b2352b3..8d51d7240f 100644 --- a/cmd/podman/root.go +++ b/cmd/podman/root.go @@ -507,6 +507,10 @@ func rootFlags(cmd *cobra.Command, podmanConfig *entities.PodmanConfig) { pFlags.StringVar(&podmanConfig.Runroot, runrootFlagName, "", "Path to the 'run directory' where all state information is stored") _ = cmd.RegisterFlagCompletionFunc(runrootFlagName, completion.AutocompleteDefault) + imageStoreFlagName := "imagestore" + pFlags.StringVar(&podmanConfig.ImageStore, imageStoreFlagName, "", "Path to the `image store`, different from `graph root`, use this to split storing the image into a separate `image store`, see `man containers-storage.conf` for details") + _ = cmd.RegisterFlagCompletionFunc(imageStoreFlagName, completion.AutocompleteDefault) + pFlags.BoolVar(&podmanConfig.TransientStore, "transient-store", false, "Enable transient container storage") runtimeFlagName := "runtime" diff --git a/docs/source/markdown/podman.1.md b/docs/source/markdown/podman.1.md index 2bda9fa6c9..8f8390df3f 100644 --- a/docs/source/markdown/podman.1.md +++ b/docs/source/markdown/podman.1.md @@ -78,6 +78,12 @@ Identity value resolution precedence: - `containers.conf` Remote connections use local containers.conf for default. +#### **--imagestore**=*path* + +Path of the imagestore where images are stored. By default, the storage library stores all the images in the graphroot but if an imagestore is provided, then the storage library will store newly pulled images in the provided imagestore and keep using the graphroot for everything else. If the user is using the overlay driver, then the images which were already part of the graphroot will still be accessible. + +This will override *imagestore* option in `containers-storage.conf(5)`, refer to `containers-storage.conf(5)` for more details. + #### **--log-level**=*level* Log messages at and above specified level: debug, info, warn, error, fatal or panic (default: "warn") diff --git a/libpod/options.go b/libpod/options.go index d0e71a350f..8a5aea9397 100644 --- a/libpod/options.go +++ b/libpod/options.go @@ -126,6 +126,18 @@ func WithTransientStore(transientStore bool) RuntimeOption { } } +func WithImageStore(imageStore string) RuntimeOption { + return func(rt *Runtime) error { + if rt.valid { + return define.ErrRuntimeFinalized + } + + rt.storageConfig.ImageStore = imageStore + + return nil + } +} + // WithSignaturePolicy specifies the path of a file which decides how trust is // managed for images we've pulled. // If this is not specified, the system default configuration will be used diff --git a/pkg/domain/entities/engine.go b/pkg/domain/entities/engine.go index 984c69f5c1..7c6c0dab81 100644 --- a/pkg/domain/entities/engine.go +++ b/pkg/domain/entities/engine.go @@ -52,6 +52,7 @@ type PodmanConfig struct { URI string // URI to RESTful API Service Runroot string + ImageStore string StorageDriver string StorageOpts []string SSHMode string diff --git a/pkg/domain/infra/runtime_libpod.go b/pkg/domain/infra/runtime_libpod.go index a2cf003bcf..a6022c846c 100644 --- a/pkg/domain/infra/runtime_libpod.go +++ b/pkg/domain/infra/runtime_libpod.go @@ -153,6 +153,10 @@ func getRuntime(ctx context.Context, fs *flag.FlagSet, opts *engineOpts) (*libpo storageSet = true storageOpts.RunRoot = cfg.Runroot } + if fs.Changed("imagestore") { + storageOpts.ImageStore = cfg.ImageStore + options = append(options, libpod.WithImageStore(cfg.ImageStore)) + } if len(storageOpts.RunRoot) > 50 { return nil, errors.New("the specified runroot is longer than 50 characters") } diff --git a/pkg/specgenutil/util.go b/pkg/specgenutil/util.go index 1d7b3dee39..4f2766c8ba 100644 --- a/pkg/specgenutil/util.go +++ b/pkg/specgenutil/util.go @@ -285,6 +285,9 @@ func CreateExitCommandArgs(storageConfig storageTypes.StoreOptions, config *conf "--db-backend", config.Engine.DBBackend, fmt.Sprintf("--transient-store=%t", storageConfig.TransientStore), } + if storageConfig.ImageStore != "" { + command = append(command, []string{"--imagestore", storageConfig.ImageStore}...) + } if config.Engine.OCIRuntime != "" { command = append(command, []string{"--runtime", config.Engine.OCIRuntime}...) } diff --git a/test/e2e/pull_test.go b/test/e2e/pull_test.go index ffa9d33197..399c6e3a6a 100644 --- a/test/e2e/pull_test.go +++ b/test/e2e/pull_test.go @@ -62,6 +62,73 @@ var _ = Describe("Podman pull", func() { Expect(session).Should(Exit(0)) }) + It("podman pull and run on split imagestore", func() { + SkipIfRemote("podman-remote does not support setting external imagestore") + imgName := "splitstoretest" + + // Make alpine write-able + session := podmanTest.Podman([]string{"build", "--pull=never", "--tag", imgName, "build/basicalpine"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + tmpDir := filepath.Join(podmanTest.TempDir, "splitstore") + outfile := filepath.Join(podmanTest.TempDir, "image.tar") + + save := podmanTest.Podman([]string{"save", "-o", outfile, "--format", "oci-archive", imgName}) + save.WaitWithDefaultTimeout() + Expect(save).Should(Exit(0)) + + rmi := podmanTest.Podman([]string{"rmi", imgName}) + rmi.WaitWithDefaultTimeout() + Expect(rmi).Should(Exit(0)) + + // load to splitstore + result := podmanTest.Podman([]string{"load", "--imagestore", tmpDir, "-q", "-i", outfile}) + result.WaitWithDefaultTimeout() + Expect(result).Should(Exit(0)) + + // tag busybox to busybox-test in graphroot since we can delete readonly busybox + session = podmanTest.Podman([]string{"tag", "quay.io/libpod/busybox:latest", "busybox-test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + session = podmanTest.Podman([]string{"images", "--imagestore", tmpDir}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring(imgName)) + Expect(session.OutputToString()).To(ContainSubstring("busybox-test")) + + // Test deleting image in graphroot even when `--imagestore` is set + session = podmanTest.Podman([]string{"rmi", "--imagestore", tmpDir, "busybox-test"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + + // Images without --imagestore should not contain alpine + session = podmanTest.Podman([]string{"images"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(Not(ContainSubstring(imgName))) + + // Set `imagestore` in `storage.conf` and container should run. + configPath := filepath.Join(podmanTest.TempDir, ".config", "containers", "storage.conf") + os.Setenv("CONTAINERS_STORAGE_CONF", configPath) + defer func() { + os.Unsetenv("CONTAINERS_STORAGE_CONF") + }() + + err = os.MkdirAll(filepath.Dir(configPath), os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + storageConf := []byte(fmt.Sprintf("[storage]\nimagestore=\"%s\"", tmpDir)) + err = os.WriteFile(configPath, storageConf, os.ModePerm) + Expect(err).ToNot(HaveOccurred()) + + session = podmanTest.Podman([]string{"run", "--name", "test", "--rm", + imgName, "echo", "helloworld"}) + session.WaitWithDefaultTimeout() + Expect(session).Should(Exit(0)) + Expect(session.OutputToString()).To(ContainSubstring("helloworld")) + }) + It("podman pull by digest", func() { session := podmanTest.Podman([]string{"pull", "quay.io/libpod/testdigest_v2s2@sha256:755f4d90b3716e2bf57060d249e2cd61c9ac089b1233465c5c2cb2d7ee550fdb"}) session.WaitWithDefaultTimeout() From e5399aaf3c10b03124e4b6855bb3dd5b9a1135cc Mon Sep 17 00:00:00 2001 From: Aditya R Date: Fri, 16 Jun 2023 14:27:08 +0530 Subject: [PATCH 2/2] vendor: bump c/storage to v1.46.2-0.20230616083707-cc0d208e5e1c Signed-off-by: Aditya R --- go.mod | 2 +- go.sum | 4 +- .../github.com/containers/storage/.cirrus.yml | 4 +- vendor/github.com/containers/storage/check.go | 143 +++++++++++++----- .../storage/drivers/overlay/overlay.go | 2 +- .../github.com/containers/storage/layers.go | 7 - vendor/github.com/containers/storage/store.go | 73 +++++++-- .../containers/storage/types/options.go | 1 + vendor/modules.txt | 2 +- 9 files changed, 176 insertions(+), 62 deletions(-) diff --git a/go.mod b/go.mod index e5e58071fb..238753a4da 100644 --- a/go.mod +++ b/go.mod @@ -19,7 +19,7 @@ require ( github.com/containers/libhvee v0.0.5 github.com/containers/ocicrypt v1.1.7 github.com/containers/psgo v1.8.0 - github.com/containers/storage v1.46.2-0.20230613134951-e424b6649be3 + github.com/containers/storage v1.46.2-0.20230616083707-cc0d208e5e1c github.com/coreos/go-systemd/v22 v22.5.0 github.com/coreos/stream-metadata-go v0.4.2 github.com/crc-org/vfkit v0.0.5-0.20230602131541-3d57f09010c9 diff --git a/go.sum b/go.sum index d0f406910e..33eb75f192 100644 --- a/go.sum +++ b/go.sum @@ -257,8 +257,8 @@ github.com/containers/ocicrypt v1.1.7/go.mod h1:7CAhjcj2H8AYp5YvEie7oVSK2AhBY8Ns github.com/containers/psgo v1.8.0 h1:2loGekmGAxM9ir5OsXWEfGwFxorMPYnc6gEDsGFQvhY= github.com/containers/psgo v1.8.0/go.mod h1:T8ZxnX3Ur4RvnhxFJ7t8xJ1F48RhiZB4rSrOaR/qGHc= github.com/containers/storage v1.43.0/go.mod h1:uZ147thiIFGdVTjMmIw19knttQnUCl3y9zjreHrg11s= -github.com/containers/storage v1.46.2-0.20230613134951-e424b6649be3 h1:nSCnnrCMocJDsNUU4EDPT8GkW7ToU43/QbXGRC+ciEs= -github.com/containers/storage v1.46.2-0.20230613134951-e424b6649be3/go.mod h1:pRp3lkRo2qodb/ltpnudoXggrviRmaCmU5a5GhTBae0= +github.com/containers/storage v1.46.2-0.20230616083707-cc0d208e5e1c h1:hJP+UF9OzDaThxavD5isFbAFxbvb25TdFtjohAhH/dc= +github.com/containers/storage v1.46.2-0.20230616083707-cc0d208e5e1c/go.mod h1:pRp3lkRo2qodb/ltpnudoXggrviRmaCmU5a5GhTBae0= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= diff --git a/vendor/github.com/containers/storage/.cirrus.yml b/vendor/github.com/containers/storage/.cirrus.yml index 5567bfbc9c..8ef38e2cd2 100644 --- a/vendor/github.com/containers/storage/.cirrus.yml +++ b/vendor/github.com/containers/storage/.cirrus.yml @@ -18,12 +18,12 @@ env: #### Cache-image names to test with (double-quotes around names are critical) ### FEDORA_NAME: "fedora-38" - DEBIAN_NAME: "debian-12" + DEBIAN_NAME: "debian-13" # GCE project where images live IMAGE_PROJECT: "libpod-218412" # VM Image built in containers/automation_images - IMAGE_SUFFIX: "c20230601t145439z-f38f37d12" + IMAGE_SUFFIX: "c20230614t132754z-f38f37d13" FEDORA_CACHE_IMAGE_NAME: "fedora-${IMAGE_SUFFIX}" DEBIAN_CACHE_IMAGE_NAME: "debian-${IMAGE_SUFFIX}" diff --git a/vendor/github.com/containers/storage/check.go b/vendor/github.com/containers/storage/check.go index 81b5c3ab81..e58084fc7c 100644 --- a/vendor/github.com/containers/storage/check.go +++ b/vendor/github.com/containers/storage/check.go @@ -15,6 +15,7 @@ import ( drivers "github.com/containers/storage/drivers" "github.com/containers/storage/pkg/archive" + "github.com/containers/storage/pkg/idtools" "github.com/containers/storage/pkg/ioutils" "github.com/containers/storage/types" "github.com/sirupsen/logrus" @@ -74,6 +75,13 @@ type CheckOptions struct { ContainerData bool // check that associated "big" data items are present and can be read } +// checkIgnore is used to tell functions that compare the contents of a mounted +// layer to the contents that we'd expect it to have to ignore certain +// discrepancies +type checkIgnore struct { + ownership, timestamps, permissions bool +} + // CheckMost returns a CheckOptions with mostly just "quick" checks enabled. func CheckMost() *CheckOptions { return &CheckOptions{ @@ -125,10 +133,20 @@ func RepairEverything() *RepairOptions { // Check returns a list of problems with what's in the store, as a whole. It can be very expensive // to call. func (s *store) Check(options *CheckOptions) (CheckReport, error) { - var ignoreChownErrors bool + var ignore checkIgnore for _, o := range s.graphOptions { - if strings.Contains(o, "ignore_chown_errors") { - ignoreChownErrors = true + if strings.Contains(o, "ignore_chown_errors=true") { + ignore.ownership = true + } + if strings.HasPrefix(o, "force_mask=") { + ignore.permissions = true + } + } + for o := range s.pullOptions { + if strings.Contains(o, "use_hard_links") { + if s.pullOptions[o] == "true" { + ignore.timestamps = true + } } } @@ -160,7 +178,7 @@ func (s *store) Check(options *CheckOptions) (CheckReport, error) { // Walk the list of layer stores, looking at each layer that we didn't see in a // previously-visited store. - if _, _, err := readAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) { + if _, _, err := readOrWriteAllLayerStores(s, func(store roLayerStore) (struct{}, bool, error) { layers, err := store.Layers() if err != nil { return struct{}{}, true, err @@ -315,7 +333,6 @@ func (s *store) Check(options *CheckOptions) (CheckReport, error) { } else { report.ROLayers[id] = append(report.ROLayers[id], err) } - return } if layer.UncompressedSize != -1 && counter.Count != layer.UncompressedSize { // We expected the diff to have a specific size, and @@ -329,7 +346,6 @@ func (s *store) Check(options *CheckOptions) (CheckReport, error) { } else { report.ROLayers[id] = append(report.ROLayers[id], err) } - return } }() } @@ -398,6 +414,11 @@ func (s *store) Check(options *CheckOptions) (CheckReport, error) { expectedCheckDirectory.headers(diffHeaders) } // Scan the directory tree under the mount point. + var idmap *idtools.IDMappings + if !s.canUseShifting(layer.UIDMap, layer.GIDMap) { + // we would have had to chown() layer contents to match ID maps + idmap = idtools.NewIDMappingsFromMaps(layer.UIDMap, layer.GIDMap) + } actualCheckDirectory, err := newCheckDirectoryFromDirectory(mountPoint) if err != nil { err := fmt.Errorf("scanning contents of %slayer %s: %w", readWriteDesc, id, err) @@ -409,7 +430,7 @@ func (s *store) Check(options *CheckOptions) (CheckReport, error) { return } // Every departure from our expectations is an error. - diffs := compareCheckDirectory(expectedCheckDirectory, actualCheckDirectory, ignoreChownErrors) + diffs := compareCheckDirectory(expectedCheckDirectory, actualCheckDirectory, idmap, ignore) for _, diff := range diffs { err := fmt.Errorf("%slayer %s: %s, %w", readWriteDesc, id, diff, ErrLayerContentModified) if isReadWrite { @@ -805,27 +826,33 @@ func (s *store) Repair(report CheckReport, options *RepairOptions) []error { } // compareFileInfo returns a string summarizing what's different between the two checkFileInfos -func compareFileInfo(a, b checkFileInfo, ignoreChownErrors bool) string { - if a.typeflag == b.typeflag && a.uid != b.uid && a.gid != b.gid && a.size != b.size && - (os.ModeType|os.ModePerm)&a.mode != (os.ModeType|os.ModePerm)&b.mode { - return "" - } +func compareFileInfo(a, b checkFileInfo, idmap *idtools.IDMappings, ignore checkIgnore) string { var comparison []string if a.typeflag != b.typeflag { comparison = append(comparison, fmt.Sprintf("filetype:%v→%v", a.typeflag, b.typeflag)) } - if a.uid != b.uid && ignoreChownErrors { + if idmap != nil && !idmap.Empty() { + mappedUID, mappedGID, err := idmap.ToContainer(idtools.IDPair{UID: b.uid, GID: b.gid}) + if err != nil { + return err.Error() + } + b.uid, b.gid = mappedUID, mappedGID + } + if a.uid != b.uid && !ignore.ownership { comparison = append(comparison, fmt.Sprintf("uid:%d→%d", a.uid, b.uid)) } - if a.gid != b.gid && ignoreChownErrors { + if a.gid != b.gid && !ignore.ownership { comparison = append(comparison, fmt.Sprintf("gid:%d→%d", a.gid, b.gid)) } if a.size != b.size { comparison = append(comparison, fmt.Sprintf("size:%d→%d", a.size, b.size)) } - if (os.ModeType|os.ModePerm)&a.mode != (os.ModeType|os.ModePerm)&b.mode { + if (os.ModeType|os.ModePerm)&a.mode != (os.ModeType|os.ModePerm)&b.mode && !ignore.permissions { comparison = append(comparison, fmt.Sprintf("mode:%04o→%04o", a.mode, b.mode)) } + if a.mtime != b.mtime && !ignore.timestamps { + comparison = append(comparison, fmt.Sprintf("mtime:0x%x→0x%x", a.mtime, b.mtime)) + } return strings.Join(comparison, ",") } @@ -835,6 +862,7 @@ type checkFileInfo struct { uid, gid int size int64 mode os.FileMode + mtime int64 // unix-style whole seconds } // checkDirectory is a node in a filesystem record, possibly the top @@ -845,7 +873,7 @@ type checkDirectory struct { } // newCheckDirectory creates an empty checkDirectory -func newCheckDirectory(uid, gid int, size int64, mode os.FileMode) *checkDirectory { +func newCheckDirectory(uid, gid int, size int64, mode os.FileMode, mtime int64) *checkDirectory { return &checkDirectory{ directory: make(map[string]*checkDirectory), file: make(map[string]checkFileInfo), @@ -855,6 +883,7 @@ func newCheckDirectory(uid, gid int, size int64, mode os.FileMode) *checkDirecto gid: gid, size: size, mode: mode, + mtime: mtime, }, } } @@ -862,7 +891,7 @@ func newCheckDirectory(uid, gid int, size int64, mode os.FileMode) *checkDirecto // newCheckDirectoryDefaults creates an empty checkDirectory with hardwired defaults for the UID // (0), GID (0), size (0) and permissions (0o555) func newCheckDirectoryDefaults() *checkDirectory { - return newCheckDirectory(0, 0, 0, 0o555) + return newCheckDirectory(0, 0, 0, 0o555, time.Now().Unix()) } // newCheckDirectoryFromDirectory creates a checkDirectory for an on-disk directory tree @@ -880,9 +909,6 @@ func newCheckDirectoryFromDirectory(dir string) (*checkDirectory, error) { if err != nil { return err } - if hdr.Typeflag == tar.TypeLink || hdr.Typeflag == tar.TypeRegA { - hdr.Typeflag = tar.TypeReg - } hdr.Name = filepath.ToSlash(rel) cd.header(hdr) return nil @@ -894,16 +920,23 @@ func newCheckDirectoryFromDirectory(dir string) (*checkDirectory, error) { } // add adds an item to a checkDirectory -func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int64, mode os.FileMode) { +func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int64, mode os.FileMode, mtime int64) { components := strings.Split(path, "/") if components[len(components)-1] == "" { components = components[:len(components)-1] } - if typeflag == tar.TypeLink || typeflag == tar.TypeRegA { - typeflag = tar.TypeReg + if components[0] == "." { + components = components[1:] + } + if typeflag != tar.TypeReg { + size = 0 } switch len(components) { case 0: + c.uid = uid + c.gid = gid + c.mode = mode + c.mtime = mtime return case 1: switch typeflag { @@ -911,7 +944,7 @@ func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int6 delete(c.file, components[0]) // directory entries are mergers, not replacements if _, present := c.directory[components[0]]; !present { - c.directory[components[0]] = newCheckDirectory(uid, gid, size, mode) + c.directory[components[0]] = newCheckDirectory(uid, gid, size, mode, mtime) } else { c.directory[components[0]].checkFileInfo = checkFileInfo{ typeflag: tar.TypeDir, @@ -919,6 +952,7 @@ func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int6 gid: gid, size: size, mode: mode, + mtime: mtime, } } default: @@ -930,6 +964,7 @@ func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int6 gid: gid, size: size, mode: mode, + mtime: mtime, } case tar.TypeXGlobalHeader: // ignore, since even though it looks like a valid pathname, it doesn't end @@ -939,10 +974,10 @@ func (c *checkDirectory) add(path string, typeflag byte, uid, gid int, size int6 } subdirectory := c.directory[components[0]] if subdirectory == nil { - subdirectory = newCheckDirectory(uid, gid, size, mode) + subdirectory = newCheckDirectory(uid, gid, size, mode, mtime) c.directory[components[0]] = subdirectory } - subdirectory.add(strings.Join(components[1:], "/"), typeflag, uid, gid, size, mode) + subdirectory.add(strings.Join(components[1:], "/"), typeflag, uid, gid, size, mode, mtime) } // remove removes an item from a checkDirectory @@ -966,12 +1001,37 @@ func (c *checkDirectory) header(hdr *tar.Header) { if strings.HasPrefix(base, archive.WhiteoutPrefix) { if base == archive.WhiteoutOpaqueDir { c.remove(path.Clean(dir)) - c.add(path.Clean(dir), tar.TypeDir, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode)) + c.add(path.Clean(dir), tar.TypeDir, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix()) } else { c.remove(path.Join(dir, base[len(archive.WhiteoutPrefix):])) } } else { - c.add(name, hdr.Typeflag, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode)) + if hdr.Typeflag == tar.TypeLink { + // look up the attributes of the target of the hard link + // n.b. by convention, Linkname is always relative to the + // root directory of the archive, which is not always the + // same as being relative to hdr.Name + directory := c + for _, component := range strings.Split(path.Clean(hdr.Linkname), "/") { + if component == "." || component == ".." { + continue + } + if subdir, ok := directory.directory[component]; ok { + directory = subdir + continue + } + if file, ok := directory.file[component]; ok { + hdr.Typeflag = file.typeflag + hdr.Uid = file.uid + hdr.Gid = file.gid + hdr.Size = file.size + hdr.Mode = int64(file.mode) + hdr.ModTime = time.Unix(file.mtime, 0) + } + break + } + } + c.add(name, hdr.Typeflag, hdr.Uid, hdr.Gid, hdr.Size, os.FileMode(hdr.Mode), hdr.ModTime.Unix()) } } @@ -981,7 +1041,14 @@ func (c *checkDirectory) headers(hdrs []*tar.Header) { // sort the headers from the diff to ensure that whiteouts appear // before content when they both appear in the same directory, per // https://github.com/opencontainers/image-spec/blob/main/layer.md#whiteouts - sort.Slice(hdrs, func(i, j int) bool { + // and that hard links appear after other types of entries + sort.SliceStable(hdrs, func(i, j int) bool { + if hdrs[i].Typeflag != tar.TypeLink && hdrs[j].Typeflag == tar.TypeLink { + return true + } + if hdrs[i].Typeflag == tar.TypeLink && hdrs[j].Typeflag != tar.TypeLink { + return false + } idir, ifile := path.Split(hdrs[i].Name) jdir, jfile := path.Split(hdrs[j].Name) if idir != jdir { @@ -1016,7 +1083,7 @@ func (c *checkDirectory) names() []string { } // compareCheckSubdirectory walks two subdirectory trees and returns a list of differences -func compareCheckSubdirectory(path string, a, b *checkDirectory, ignoreChownErrors bool) []string { +func compareCheckSubdirectory(path string, a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string { var diff []string if a == nil { a = newCheckDirectoryDefaults() @@ -1028,20 +1095,20 @@ func compareCheckSubdirectory(path string, a, b *checkDirectory, ignoreChownErro if bdir, present := b.directory[aname]; !present { // directory was removed diff = append(diff, "-"+path+"/"+aname+"/") - diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, nil, ignoreChownErrors)...) + diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, nil, idmap, ignore)...) } else { // directory is in both trees; descend - if attributes := compareFileInfo(adir.checkFileInfo, bdir.checkFileInfo, ignoreChownErrors); attributes != "" { + if attributes := compareFileInfo(adir.checkFileInfo, bdir.checkFileInfo, idmap, ignore); attributes != "" { diff = append(diff, path+"/"+aname+"("+attributes+")") } - diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, bdir, ignoreChownErrors)...) + diff = append(diff, compareCheckSubdirectory(path+"/"+aname, adir, bdir, idmap, ignore)...) } } for bname, bdir := range b.directory { if _, present := a.directory[bname]; !present { // directory added diff = append(diff, "+"+path+"/"+bname+"/") - diff = append(diff, compareCheckSubdirectory(path+"/"+bname, nil, bdir, ignoreChownErrors)...) + diff = append(diff, compareCheckSubdirectory(path+"/"+bname, nil, bdir, idmap, ignore)...) } } for aname, afile := range a.file { @@ -1050,7 +1117,7 @@ func compareCheckSubdirectory(path string, a, b *checkDirectory, ignoreChownErro diff = append(diff, "-"+path+"/"+aname) } else { // item is in both trees; compare - if attributes := compareFileInfo(afile, bfile, ignoreChownErrors); attributes != "" { + if attributes := compareFileInfo(afile, bfile, idmap, ignore); attributes != "" { diff = append(diff, path+"/"+aname+"("+attributes+")") } } @@ -1062,7 +1129,7 @@ func compareCheckSubdirectory(path string, a, b *checkDirectory, ignoreChownErro diff = append(diff, "+"+path+"/"+bname) continue } - if attributes := compareFileInfo(filetype, b.file[bname], ignoreChownErrors); attributes != "" { + if attributes := compareFileInfo(filetype, b.file[bname], idmap, ignore); attributes != "" { // non-directory replaced with non-directory diff = append(diff, "+"+path+"/"+bname+"("+attributes+")") } @@ -1071,8 +1138,8 @@ func compareCheckSubdirectory(path string, a, b *checkDirectory, ignoreChownErro } // compareCheckDirectory walks two directory trees and returns a sorted list of differences -func compareCheckDirectory(a, b *checkDirectory, ignoreChownErrors bool) []string { - diff := compareCheckSubdirectory("", a, b, ignoreChownErrors) +func compareCheckDirectory(a, b *checkDirectory, idmap *idtools.IDMappings, ignore checkIgnore) []string { + diff := compareCheckSubdirectory("", a, b, idmap, ignore) sort.Slice(diff, func(i, j int) bool { if strings.Compare(diff[i][1:], diff[j][1:]) < 0 { return true diff --git a/vendor/github.com/containers/storage/drivers/overlay/overlay.go b/vendor/github.com/containers/storage/drivers/overlay/overlay.go index 5431da4e4e..6be5f663f4 100644 --- a/vendor/github.com/containers/storage/drivers/overlay/overlay.go +++ b/vendor/github.com/containers/storage/drivers/overlay/overlay.go @@ -1029,7 +1029,7 @@ func (d *Driver) create(id, parent string, opts *graphdriver.CreateOpts, disable } if imageStore != "" { if err2 := os.RemoveAll(workDirBase); err2 != nil { - logrus.Errorf("While recovering from a failure creating a layer, error deleting %#v: %v", dir, err2) + logrus.Errorf("While recovering from a failure creating a layer, error deleting %#v: %v", workDirBase, err2) } } } diff --git a/vendor/github.com/containers/storage/layers.go b/vendor/github.com/containers/storage/layers.go index b0ee67b2e9..0e6cb782b5 100644 --- a/vendor/github.com/containers/storage/layers.go +++ b/vendor/github.com/containers/storage/layers.go @@ -314,9 +314,6 @@ type rwLayerStore interface { // Clean up unreferenced layers GarbageCollect() error - - // supportsShifting() returns true if the driver.Driver.SupportsShifting(). - supportsShifting() bool } type layerStore struct { @@ -2472,10 +2469,6 @@ func (r *layerStore) LayersByUncompressedDigest(d digest.Digest) ([]Layer, error return r.layersByDigestMap(r.byuncompressedsum, d) } -func (r *layerStore) supportsShifting() bool { - return r.driver.SupportsShifting() -} - func closeAll(closes ...func() error) (rErr error) { for _, f := range closes { if err := f(); err != nil { diff --git a/vendor/github.com/containers/storage/store.go b/vendor/github.com/containers/storage/store.go index e754200896..14c1edd7f8 100644 --- a/vendor/github.com/containers/storage/store.go +++ b/vendor/github.com/containers/storage/store.go @@ -1216,6 +1216,54 @@ func writeToLayerStore[T any](s *store, fn func(store rwLayerStore) (T, error)) return fn(store) } +// readOrWriteAllLayerStores processes allLayerStores() in order: +// It locks the writeable store for writing and all others for reading, checks +// for updates, and calls +// +// (data, done, err) := fn(store) +// +// until the callback returns done == true, and returns the data from the callback. +// +// If reading or writing any layer store fails, it immediately returns ({}, true, err). +// +// If all layer stores are processed without setting done == true, it returns ({}, false, nil). +// +// Typical usage: +// +// if res, done, err := s.readOrWriteAllLayerStores(store, func(…) { +// … +// }; done { +// return res, err +// } +func readOrWriteAllLayerStores[T any](s *store, fn func(store roLayerStore) (T, bool, error)) (T, bool, error) { + var zeroRes T // A zero value of T + + rwLayerStore, roLayerStores, err := s.bothLayerStoreKinds() + if err != nil { + return zeroRes, true, err + } + + if err := rwLayerStore.startWriting(); err != nil { + return zeroRes, true, err + } + defer rwLayerStore.stopWriting() + if res, done, err := fn(rwLayerStore); done { + return res, true, err + } + + for _, s := range roLayerStores { + store := s + if err := store.startReading(); err != nil { + return zeroRes, true, err + } + defer store.stopReading() + if res, done, err := fn(store); done { + return res, true, err + } + } + return zeroRes, false, nil +} + // allImageStores returns a list of all image store objects used by the Store. // This is a convenience method for read-only users of the Store. func (s *store) allImageStores() []roImageStore { @@ -1319,10 +1367,13 @@ func (s *store) writeToAllStores(fn func(rlstore rwLayerStore) error) error { return fn(rlstore) } -// canUseShifting returns ??? -// store must be locked for writing. -func canUseShifting(store rwLayerStore, uidmap, gidmap []idtools.IDMap) bool { - if !store.supportsShifting() { +// canUseShifting returns true if we can use mount-time arguments (shifting) to +// avoid having to create a mapped top layer for a base image when we want to +// use it to create a container using ID mappings. +// On entry: +// - rlstore must be locked for writing +func (s *store) canUseShifting(uidmap, gidmap []idtools.IDMap) bool { + if !s.graphDriver.SupportsShifting() { return false } if uidmap != nil && !idtools.IsContiguous(uidmap) { @@ -1409,7 +1460,7 @@ func (s *store) PutLayer(id, parent string, names []string, mountLabel string, w OriginalDigest: options.OriginalDigest, UncompressedDigest: options.UncompressedDigest, } - if canUseShifting(rlstore, uidMap, gidMap) { + if s.canUseShifting(uidMap, gidMap) { layerOptions.IDMappingOptions = types.IDMappingOptions{HostUIDMapping: true, HostGIDMapping: true, UIDMap: nil, GIDMap: nil} } else { layerOptions.IDMappingOptions = types.IDMappingOptions{ @@ -1535,7 +1586,9 @@ func (s *store) CreateImage(id string, names []string, layer, metadata string, i }) } -// imageTopLayerForMapping does ??? +// imageTopLayerForMapping locates the layer that can take the place of the +// image's top layer as the shared parent layer for a one or more containers +// which are using ID mappings. // On entry: // - ristore must be locked EITHER for reading or writing // - s.imageStore must be locked for writing; it might be identical to ristore. @@ -1544,7 +1597,7 @@ func (s *store) CreateImage(id string, names []string, layer, metadata string, i func (s *store) imageTopLayerForMapping(image *Image, ristore roImageStore, rlstore rwLayerStore, lstores []roLayerStore, options types.IDMappingOptions) (*Layer, error) { layerMatchesMappingOptions := func(layer *Layer, options types.IDMappingOptions) bool { // If the driver supports shifting and the layer has no mappings, we can use it. - if canUseShifting(rlstore, options.UIDMap, options.GIDMap) && len(layer.UIDMap) == 0 && len(layer.GIDMap) == 0 { + if s.canUseShifting(options.UIDMap, options.GIDMap) && len(layer.UIDMap) == 0 && len(layer.GIDMap) == 0 { return true } // If we want host mapping, and the layer uses mappings, it's not the best match. @@ -1611,7 +1664,7 @@ func (s *store) imageTopLayerForMapping(image *Image, ristore roImageStore, rlst // that lets us edit image metadata, so create a duplicate of the layer with the desired // mappings, and register it as an alternate top layer in the image. var layerOptions LayerOptions - if canUseShifting(rlstore, options.UIDMap, options.GIDMap) { + if s.canUseShifting(options.UIDMap, options.GIDMap) { layerOptions.IDMappingOptions = types.IDMappingOptions{ HostUIDMapping: true, HostGIDMapping: true, @@ -1764,7 +1817,7 @@ func (s *store) CreateContainer(id string, names []string, image, layer, metadat // But in transient store mode, all container layers are volatile. Volatile: options.Volatile || s.transientStore, } - if canUseShifting(rlstore, uidMap, gidMap) { + if s.canUseShifting(uidMap, gidMap) { layerOptions.IDMappingOptions = types.IDMappingOptions{ HostUIDMapping: true, HostGIDMapping: true, @@ -2715,7 +2768,7 @@ func (s *store) mount(id string, options drivers.MountOpts) (string, error) { defer rlstore.stopWriting() if options.UidMaps != nil || options.GidMaps != nil { - options.DisableShifting = !canUseShifting(rlstore, options.UidMaps, options.GidMaps) + options.DisableShifting = !s.canUseShifting(options.UidMaps, options.GidMaps) } if rlstore.Exists(id) { diff --git a/vendor/github.com/containers/storage/types/options.go b/vendor/github.com/containers/storage/types/options.go index b6faf7e302..3ff00ac647 100644 --- a/vendor/github.com/containers/storage/types/options.go +++ b/vendor/github.com/containers/storage/types/options.go @@ -300,6 +300,7 @@ func getRootlessStorageOpts(rootlessUID int, systemOpts StoreOptions) (StoreOpti // present. if defaultConfigFileSet { opts.GraphDriverOptions = systemOpts.GraphDriverOptions + opts.ImageStore = systemOpts.ImageStore } else if opts.GraphDriverName == overlayDriver { for _, o := range systemOpts.GraphDriverOptions { if strings.Contains(o, "ignore_chown_errors") { diff --git a/vendor/modules.txt b/vendor/modules.txt index a64e2e8cef..4f3e1782b3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -287,7 +287,7 @@ github.com/containers/psgo/internal/dev github.com/containers/psgo/internal/host github.com/containers/psgo/internal/proc github.com/containers/psgo/internal/process -# github.com/containers/storage v1.46.2-0.20230613134951-e424b6649be3 +# github.com/containers/storage v1.46.2-0.20230616083707-cc0d208e5e1c ## explicit; go 1.19 github.com/containers/storage github.com/containers/storage/drivers