From 1f9893817b32e147131c8f45dbc257e394aa4f30 Mon Sep 17 00:00:00 2001 From: Giuseppe Scrivano Date: Wed, 18 Jun 2025 11:46:21 +0200 Subject: [PATCH] volumes: add new --uid and --gid option they allow to override the owner of the volume. Differently from -o=uid= and -o=gid= they are not passed down to the mount operation. Closes: https://issues.redhat.com/browse/RHEL-76452 Signed-off-by: Giuseppe Scrivano --- cmd/podman/volumes/create.go | 19 ++++- .../source/markdown/podman-volume-create.1.md | 13 ++++ pkg/api/handlers/libpod/volumes.go | 7 ++ pkg/domain/entities/types/volumes.go | 4 + pkg/domain/infra/abi/volumes.go | 7 ++ test/e2e/volume_create_test.go | 76 +++++++++++++++++++ 6 files changed, 125 insertions(+), 1 deletion(-) diff --git a/cmd/podman/volumes/create.go b/cmd/podman/volumes/create.go index 8e557edd87..52817bb99f 100644 --- a/cmd/podman/volumes/create.go +++ b/cmd/podman/volumes/create.go @@ -23,7 +23,8 @@ var ( ValidArgsFunction: completion.AutocompleteNone, Example: `podman volume create myvol podman volume create - podman volume create --label foo=bar myvol`, + podman volume create --label foo=bar myvol + podman volume create --uid 4321 --gid 1234 myvol`, } ) @@ -33,6 +34,8 @@ var ( Label []string Opts []string Ignore bool + UID int + GID int }{} ) @@ -57,6 +60,14 @@ func init() { ignoreFlagName := "ignore" flags.BoolVar(&opts.Ignore, ignoreFlagName, false, "Don't fail if volume already exists") + + uidFlagName := "uid" + flags.IntVar(&opts.UID, uidFlagName, 0, "Set the UID of the volume owner") + _ = createCommand.RegisterFlagCompletionFunc(uidFlagName, completion.AutocompleteNone) + + gidFlagName := "gid" + flags.IntVar(&opts.GID, gidFlagName, 0, "Set the GID of the volume owner") + _ = createCommand.RegisterFlagCompletionFunc(gidFlagName, completion.AutocompleteNone) } func create(cmd *cobra.Command, args []string) error { @@ -77,6 +88,12 @@ func create(cmd *cobra.Command, args []string) error { if err != nil { return fmt.Errorf("unable to process options: %w", err) } + if cmd.Flags().Changed("uid") { + createOpts.UID = &opts.UID + } + if cmd.Flags().Changed("gid") { + createOpts.GID = &opts.GID + } response, err := registry.ContainerEngine().VolumeCreate(context.Background(), createOpts) if err != nil { return err diff --git a/docs/source/markdown/podman-volume-create.1.md b/docs/source/markdown/podman-volume-create.1.md index cf9bc8e298..0ad874ee41 100644 --- a/docs/source/markdown/podman-volume-create.1.md +++ b/docs/source/markdown/podman-volume-create.1.md @@ -28,6 +28,10 @@ An overlay filesystem is created, which allows changes to the volume to be commi Using a value other than **local** or **image**, Podman attempts to create the volume using a volume plugin with the given name. Such plugins must be defined in the **volume_plugins** section of the **[containers.conf(5)](https://github.com/containers/common/blob/main/docs/containers.conf.5.md)** configuration file. +#### **--gid**=*gid* + +Set the GID that the volume will be created as. Differently than `--opt o=gid=*gid*`, the specified value is not passed to the mount operation. The specified GID will own the volume's mount point directory and affects the volume chown operation. + #### **--help** Print usage statement @@ -69,6 +73,10 @@ This option is mandatory when using the **image** driver. When not using the **local** and **image** drivers, the given options are passed directly to the volume plugin. In this case, supported options are dictated by the plugin in question, not Podman. +#### **--uid**=*uid* + +Set the UID that the volume will be created as. Differently than `--opt o=uid=*uid*`, the specified value is not passed to the mount operation. The specified UID will own the volume's mount point directory and affects the volume chown operation. + ## QUOTAS `podman volume create` uses `XFS project quota controls` for controlling the size and the number of inodes of builtin volumes. The directory used to store the volumes must be an `XFS` file system and be mounted with the `pquota` option. @@ -124,6 +132,11 @@ Create tmpfs named volume testvol with specified options. # podman volume create --opt device=tmpfs --opt type=tmpfs --opt o=uid=1000,gid=1000 testvol ``` +Create volume overriding the owner UID and GID. +``` +# podman volume create --uid 1000 --gid 1000 myvol +``` + Create image named volume using the specified local image in containers/storage. ``` # podman volume create --driver image --opt image=fedora:latest fedoraVol diff --git a/pkg/api/handlers/libpod/volumes.go b/pkg/api/handlers/libpod/volumes.go index 545f3b3f55..44d8c4eb39 100644 --- a/pkg/api/handlers/libpod/volumes.go +++ b/pkg/api/handlers/libpod/volumes.go @@ -77,6 +77,13 @@ func CreateVolume(w http.ResponseWriter, r *http.Request) { volumeOptions = append(volumeOptions, libpod.WithVolumeIgnoreIfExist()) } + if input.UID != nil { + volumeOptions = append(volumeOptions, libpod.WithVolumeUID(*input.UID), libpod.WithVolumeNoChown()) + } + if input.GID != nil { + volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*input.GID), libpod.WithVolumeNoChown()) + } + vol, err := runtime.NewVolume(r.Context(), volumeOptions...) if err != nil { utils.InternalServerError(w, err) diff --git a/pkg/domain/entities/types/volumes.go b/pkg/domain/entities/types/volumes.go index a6e7f1659b..06e3727f10 100644 --- a/pkg/domain/entities/types/volumes.go +++ b/pkg/domain/entities/types/volumes.go @@ -18,6 +18,10 @@ type VolumeCreateOptions struct { Options map[string]string `schema:"opts"` // Ignore existing volumes IgnoreIfExists bool `schema:"ignoreIfExist"` + // UID that the volume will be created as + UID *int `schema:"uid"` + // GID that the volume will be created as + GID *int `schema:"gid"` } type VolumeRmReport struct { diff --git a/pkg/domain/infra/abi/volumes.go b/pkg/domain/infra/abi/volumes.go index 77c99559e5..2a6c480d72 100644 --- a/pkg/domain/infra/abi/volumes.go +++ b/pkg/domain/infra/abi/volumes.go @@ -40,6 +40,13 @@ func (ic *ContainerEngine) VolumeCreate(ctx context.Context, opts entities.Volum volumeOptions = append(volumeOptions, libpod.WithVolumeIgnoreIfExist()) } + if opts.UID != nil { + volumeOptions = append(volumeOptions, libpod.WithVolumeUID(*opts.UID), libpod.WithVolumeNoChown()) + } + if opts.GID != nil { + volumeOptions = append(volumeOptions, libpod.WithVolumeGID(*opts.GID), libpod.WithVolumeNoChown()) + } + vol, err := ic.Libpod.NewVolume(ctx, volumeOptions...) if err != nil { return nil, err diff --git a/test/e2e/volume_create_test.go b/test/e2e/volume_create_test.go index 7ea3e893bd..630ecfa710 100644 --- a/test/e2e/volume_create_test.go +++ b/test/e2e/volume_create_test.go @@ -179,6 +179,82 @@ var _ = Describe("Podman volume create", func() { Expect(inspectOpts.OutputToString()).To(Equal(optionStrFormatExpect)) }) + It("podman create volume with --uid and --gid flags", func() { + volName := "testVolFlags" + uid := "3001" + gid := "4001" + session := podmanTest.Podman([]string{"volume", "create", "--uid", uid, "--gid", gid, volName}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + inspectUID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .UID }}", volName}) + inspectUID.WaitWithDefaultTimeout() + Expect(inspectUID).Should(ExitCleanly()) + Expect(inspectUID.OutputToString()).To(Equal(uid)) + + inspectGID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .GID }}", volName}) + inspectGID.WaitWithDefaultTimeout() + Expect(inspectGID).Should(ExitCleanly()) + Expect(inspectGID.OutputToString()).To(Equal(gid)) + + // The specified values must not be passed down to the -o option. + fn := func(typ string) string { + return fmt.Sprintf("{{ if .Options.%s }}{{ .Options.%s }}{{ else }}EMPTY{{ end }}", typ, typ) + } + optionFormat := fmt.Sprintf("%s:%s:%s", fn("o"), fn("UID"), fn("GID")) + optionStrFormatExpected := "EMPTY:EMPTY:EMPTY" + inspectOpts := podmanTest.Podman([]string{"volume", "inspect", "--format", optionFormat, volName}) + inspectOpts.WaitWithDefaultTimeout() + Expect(inspectOpts).Should(ExitCleanly()) + Expect(inspectOpts.OutputToString()).To(Equal(optionStrFormatExpected)) + }) + + It("podman create volume with --uid flag only", func() { + volName := "testVolUidOnly" + uid := "3002" + session := podmanTest.Podman([]string{"volume", "create", "--uid", uid, volName}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + inspectUID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .UID }}", volName}) + inspectUID.WaitWithDefaultTimeout() + Expect(inspectUID).Should(ExitCleanly()) + Expect(inspectUID.OutputToString()).To(Equal(uid)) + + inspectGID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ if .GID }}{{ .GID }}{{ else }} EMPTY {{ end }}", volName}) + inspectGID.WaitWithDefaultTimeout() + Expect(inspectGID).Should(ExitCleanly()) + Expect(inspectGID.OutputToString()).To(Equal("EMPTY")) + }) + + It("podman create volume with --gid flag only", func() { + volName := "testVolGidOnly" + gid := "4002" + session := podmanTest.Podman([]string{"volume", "create", "--gid", gid, volName}) + session.WaitWithDefaultTimeout() + Expect(session).Should(ExitCleanly()) + + inspectGID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ .GID }}", volName}) + inspectGID.WaitWithDefaultTimeout() + Expect(inspectGID).Should(ExitCleanly()) + Expect(inspectGID.OutputToString()).To(Equal(gid)) + + inspectUID := podmanTest.Podman([]string{"volume", "inspect", "--format", "{{ if .UID }}{{ .UID }}{{ else }} EMPTY {{ end }}", volName}) + inspectUID.WaitWithDefaultTimeout() + Expect(inspectUID).Should(ExitCleanly()) + Expect(inspectUID.OutputToString()).To(Equal("EMPTY")) + }) + + It("podman create volume --uid and --gid flags with invalid values", func() { + session := podmanTest.Podman([]string{"volume", "create", "--uid", "invalid", "testVol"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "invalid argument \"invalid\" for \"--uid\" flag")) + + session = podmanTest.Podman([]string{"volume", "create", "--gid", "invalid", "testVol"}) + session.WaitWithDefaultTimeout() + Expect(session).To(ExitWithError(125, "invalid argument \"invalid\" for \"--gid\" flag")) + }) + It("image-backed volume basic functionality", func() { podmanTest.AddImageToRWStore(fedoraMinimal) volName := "testvol"