From 3829fbd35a59ecdcda73cfbf814f75a5b2e032e6 Mon Sep 17 00:00:00 2001 From: Aditya R Date: Tue, 13 Jun 2023 11:11:42 +0530 Subject: [PATCH] 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()