libpod: add volume rename support

Add a podman volume rename command, REST API endpoint, and bindings for renaming volumes.

The rename updates both the VolumeConfig and VolumeState tables in a single transaction and moves the volume directory on disk, rolling back if the transaction fails. Renaming an anonymous volume converts it to a named volume. Volumes that are in use, mounted, or backed by a volume plugin or the image driver cannot be renamed.

Fixes: #28189
Signed-off-by: MayorFaj <mayorfaj@gmail.com>
This commit is contained in:
MayorFaj 2026-06-11 21:56:15 +01:00
parent e2a4453cbd
commit d01b7ae534
26 changed files with 607 additions and 18 deletions

View file

@ -1,8 +1,6 @@
package containers
import (
"errors"
"github.com/spf13/cobra"
"go.podman.io/podman/v6/cmd/podman/common"
"go.podman.io/podman/v6/cmd/podman/registry"
@ -26,9 +24,9 @@ var (
Use: renameCommand.Use,
Short: renameCommand.Short,
Long: renameCommand.Long,
RunE: renameCommand.RunE,
RunE: rename,
Args: renameCommand.Args,
ValidArgsFunction: renameCommand.ValidArgsFunction,
ValidArgsFunction: common.AutocompleteContainerOneArg,
Example: "podman container rename containerA newName",
}
)
@ -45,10 +43,11 @@ func init() {
}
func rename(_ *cobra.Command, args []string) error {
if len(args) > 2 {
return errors.New("must provide at least two arguments to rename")
}
args = utils.RemoveSlash(args)
return renameContainer(args)
}
func renameContainer(args []string) error {
renameOpts := entities.ContainerRenameOptions{
NewName: args[1],
}

View file

@ -0,0 +1,34 @@
package volumes
import (
"github.com/spf13/cobra"
"go.podman.io/podman/v6/cmd/podman/common"
"go.podman.io/podman/v6/cmd/podman/registry"
"go.podman.io/podman/v6/pkg/domain/entities"
)
var (
volumeRenameDescription = "Rename an existing volume. The volume must not be in use by any containers."
volumeRenameCommand = &cobra.Command{
Use: "rename VOLUME NEWNAME",
Short: "Rename a volume",
Long: volumeRenameDescription,
RunE: volumeRename,
Args: cobra.ExactArgs(2),
ValidArgsFunction: common.AutocompleteVolumes,
Example: "podman volume rename oldName newName",
}
)
func init() {
registry.Commands = append(registry.Commands, registry.CliCommand{
Command: volumeRenameCommand,
Parent: volumeCmd,
})
}
func volumeRename(_ *cobra.Command, args []string) error {
return registry.ContainerEngine().VolumeRename(registry.Context(), args[0], entities.VolumeRenameOptions{
NewName: args[1],
})
}

View file

@ -13,7 +13,8 @@ Rename changes the name of an existing container.
The old name is freed, and is available for use.
This command can be run on containers in any state.
However, running containers may not fully receive the effects until they are restarted - for example, a running container may still use the old name in its logs.
At present, only containers are supported; pods and volumes cannot be renamed.
Use **podman volume rename** to rename volumes.
At present, pods cannot be renamed.
## OPTIONS

View file

@ -0,0 +1,34 @@
% podman-volume-rename 1
## NAME
podman\-volume\-rename - Rename a volume
## SYNOPSIS
**podman volume rename** *volume* *new_name*
## DESCRIPTION
Renames an existing volume. The following restrictions apply:
- The volume must not be in use by any containers (running or stopped).
- The volume must not be currently mounted (via **podman volume mount**).
- Only volumes using the **local** driver can be renamed; volumes backed by
a volume plugin or the **image** driver cannot be renamed.
Renaming an anonymous volume converts it to a named volume.
## OPTIONS
None.
## EXAMPLES
Rename volume `mydata` to `data_backup`:
```
$ podman volume rename mydata data_backup
```
## SEE ALSO
**[podman(1)](podman.1.md)**, **[podman-volume(1)](podman-volume.1.md)**, **[podman-volume-inspect(1)](podman-volume-inspect.1.md)**
## HISTORY
June 2026, Originally compiled by Podman Developers

View file

@ -22,6 +22,7 @@ podman volume is a set of subcommands that manage volumes.
| mount | [podman-volume-mount(1)](podman-volume-mount.1.md) | Mount a volume filesystem. |
| prune | [podman-volume-prune(1)](podman-volume-prune.1.md) | Remove unused volumes. |
| reload | [podman-volume-reload(1)](podman-volume-reload.1.md) | Reload all volumes from volumes plugins. |
| rename | [podman-volume-rename(1)](podman-volume-rename.1.md) | Rename a volume. |
| rm | [podman-volume-rm(1)](podman-volume-rm.1.md) | Remove one or more volumes. |
| unmount | [podman-volume-unmount(1)](podman-volume-unmount.1.md) | Unmount a volume. |

View file

@ -195,7 +195,7 @@ const (
Refresh Status = "refresh"
// Remove ...
Remove Status = "remove"
// Rename indicates that a container was renamed
// Rename indicates that the target was renamed
Rename Status = "rename"
// Renumber indicates that lock numbers were reallocated at user
// request.

View file

@ -90,7 +90,7 @@ func (e *Event) ToHumanReadable(truncate bool) string {
} else {
humanFormat = fmt.Sprintf("%s %s %s", e.Time, e.Type, e.Status)
}
case Volume, Machine:
case Machine, Volume:
humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, e.Name)
case Secret:
humanFormat = fmt.Sprintf("%s %s %s %s", e.Time, e.Type, e.Status, id)

View file

@ -5,6 +5,8 @@ package libpod
import (
"context"
"errors"
"fmt"
"strings"
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/podman/v6/libpod/events"
@ -110,6 +112,64 @@ func (r *Runtime) GetAllVolumes() ([]*Volume, error) {
return r.state.AllVolumes()
}
// RenameVolume renames the given volume to a new name.
// The volume must not be in use by any containers, and must use the local
// driver.
func (r *Runtime) RenameVolume(_ context.Context, vol *Volume, newName string) (*Volume, error) {
if !r.valid {
return nil, define.ErrRuntimeStopped
}
vol.lock.Lock()
defer vol.lock.Unlock()
if err := vol.update(); err != nil {
return nil, err
}
if newName == "" || !define.NameRegex.MatchString(newName) {
return nil, define.RegexError
}
if vol.Name() == newName {
return vol, nil
}
// Only local-driver volumes can be renamed
driver := vol.Driver()
if driver != "" && driver != define.VolumeDriverLocal {
return nil, fmt.Errorf("renaming volume %s: rename is not supported for volumes using driver %q: %w", vol.Name(), driver, define.ErrInvalidArg)
}
// Refuse rename if the volume is currently mounted
if vol.state.MountCount > 0 {
return nil, fmt.Errorf("renaming volume %s: volume is currently mounted: %w", vol.Name(), define.ErrVolumeBeingUsed)
}
// Refuse rename if the volume is in use by any container
ctrs, err := r.state.VolumeInUse(vol)
if err != nil {
return nil, fmt.Errorf("checking if volume %s is in use: %w", vol.Name(), err)
}
if len(ctrs) > 0 {
return nil, fmt.Errorf("volume %s is being used by the following container(s): %s: %w", vol.Name(), strings.Join(ctrs, ", "), define.ErrVolumeBeingUsed)
}
oldName := vol.config.Name
config := *vol.config
config.Name = newName
config.MountPoint = r.volumeDataPath(newName)
config.IsAnon = false
if err := r.state.RenameVolume(vol, &config); err != nil {
return nil, fmt.Errorf("renaming volume %s: %w", oldName, err)
}
vol.config = &config
vol.newVolumeEvent(events.Rename)
return vol, nil
}
// PruneVolumes removes unused volumes from the system
func (r *Runtime) PruneVolumes(ctx context.Context, filterFuncs []VolumeFilter, dryRun bool) ([]*reports.PruneReport, error) {
preports := make([]*reports.PruneReport, 0)

View file

@ -26,6 +26,14 @@ import (
const volumeSuffix = "+volume"
func (r *Runtime) volumePath(name string) string {
return filepath.Join(r.config.Engine.VolumePath, name)
}
func (r *Runtime) volumeDataPath(name string) string {
return filepath.Join(r.volumePath(name), "_data")
}
// NewVolume creates a new empty volume
func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
if !r.valid {
@ -161,7 +169,7 @@ func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, opti
}
} else {
// Create the mountpoint of this volume
volPathRoot := filepath.Join(r.config.Engine.VolumePath, volume.config.Name)
volPathRoot := r.volumePath(volume.config.Name)
if err := os.MkdirAll(volPathRoot, 0o700); err != nil {
return nil, fmt.Errorf("creating volume directory %q: %w", volPathRoot, err)
}
@ -205,7 +213,7 @@ func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, opti
}
}
fullVolPath := filepath.Join(volPathRoot, "_data")
fullVolPath := r.volumeDataPath(volume.config.Name)
if err := os.MkdirAll(fullVolPath, 0o755); err != nil {
return nil, fmt.Errorf("creating volume directory %q: %w", fullVolPath, err)
}

View file

@ -0,0 +1,18 @@
//go:build !remote && (linux || freebsd) && cgo
package libpod
import (
"errors"
sqlite3 "github.com/mattn/go-sqlite3"
)
// isSQLiteConstraint reports whether err is a SQLite constraint violation
// (for example a UNIQUE or primary-key conflict). It inspects the typed driver
// error instead of the error message so it stays correct even if the message
// wording changes.
func isSQLiteConstraint(err error) bool {
var sqliteErr sqlite3.Error
return errors.As(err, &sqliteErr) && sqliteErr.Code == sqlite3.ErrConstraint
}

View file

@ -0,0 +1,14 @@
//go:build !remote && (linux || freebsd) && !cgo
package libpod
// isSQLiteConstraint reports whether err is a SQLite constraint violation.
//
// The github.com/mattn/go-sqlite3 driver and its typed errors are only
// available with cgo, and the driver itself requires cgo to function, so this
// build can never actually talk to SQLite at runtime. This stub exists solely
// so the package still compiles for CGO-free static analysis (for example the
// FreeBSD lint run performed by "make validatepr").
func isSQLiteConstraint(_ error) bool {
return false
}

View file

@ -6,6 +6,7 @@ import (
"database/sql"
"errors"
"fmt"
"io/fs"
"os"
"path/filepath"
goruntime "runtime"
@ -17,9 +18,6 @@ import (
"go.podman.io/common/pkg/config"
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/storage"
// SQLite backend for database/sql
_ "github.com/mattn/go-sqlite3"
)
const schemaVersion = 1
@ -1352,6 +1350,96 @@ func (s *SQLiteState) RewriteVolumeConfig(volume *Volume, newCfg *VolumeConfig)
return nil
}
// RenameVolume renames the given volume in the database and rewrites its
// configuration. RewriteVolumeConfig cannot be used here because volume names
// are stored in both VolumeConfig and VolumeState. Both tables must be updated
// in one transaction to satisfy the deferred foreign-key relationship between
// them.
func (s *SQLiteState) RenameVolume(volume *Volume, newCfg *VolumeConfig) (defErr error) {
if !s.valid {
return define.ErrDBClosed
}
if !volume.valid {
return define.ErrVolumeRemoved
}
newName := newCfg.Name
oldName := volume.Name()
oldPath := s.runtime.volumePath(oldName)
newPath := s.runtime.volumePath(newName)
json, err := json.Marshal(newCfg)
if err != nil {
return fmt.Errorf("error marshalling volume %s new config JSON: %w", volume.Name(), err)
}
tx, err := s.conn.Begin()
if err != nil {
return fmt.Errorf("beginning transaction to rename volume %s: %w", volume.Name(), err)
}
defer func() {
if defErr != nil {
if err := tx.Rollback(); err != nil {
logrus.Errorf("Rolling back transaction to rename volume %s: %v", volume.Name(), err)
}
}
}()
// Update VolumeState first.
// VolumeState may not exist for all volumes, so we intentionally
// do not check RowsAffected here.
// The Name column is unique, so renaming to an existing name surfaces as a
// SQLite constraint violation on the UPDATE rather than needing a separate
// existence query; we map that to ErrVolumeExists.
if _, err := tx.Exec("UPDATE VolumeState SET Name=? WHERE Name=?;", newName, oldName); err != nil {
if isSQLiteConstraint(err) {
return fmt.Errorf("volume with name %q already exists: %w", newName, define.ErrVolumeExists)
}
return fmt.Errorf("updating volume state name for volume %s: %w", volume.Name(), err)
}
// Update VolumeConfig (Name column + JSON blob)
results, err := tx.Exec("UPDATE VolumeConfig SET Name=?, JSON=? WHERE Name=?;", newName, json, oldName)
if err != nil {
if isSQLiteConstraint(err) {
return fmt.Errorf("volume with name %q already exists: %w", newName, define.ErrVolumeExists)
}
return fmt.Errorf("updating volume config for volume %s: %w", volume.Name(), err)
}
rows, err := results.RowsAffected()
if err != nil {
return fmt.Errorf("retrieving volume %s config rename rows affected: %w", volume.Name(), err)
}
if rows == 0 {
volume.valid = false
return fmt.Errorf("no volume with name %q found in DB: %w", volume.Name(), define.ErrNoSuchVolume)
}
if rows > 1 {
return fmt.Errorf("renaming volume %s affected %d rows: %w", volume.Name(), rows, define.ErrInternal)
}
renamedStorage := false
if err := os.Rename(oldPath, newPath); err != nil {
if !errors.Is(err, fs.ErrNotExist) {
return fmt.Errorf("renaming volume directory %q to %q: %w", oldPath, newPath, err)
}
} else {
renamedStorage = true
}
if err := tx.Commit(); err != nil {
if renamedStorage {
if rerr := os.Rename(newPath, oldPath); rerr != nil {
logrus.Errorf("Failed to rollback volume %s directory rename to %q after database commit failed: %v", newName, oldName, rerr)
}
}
return fmt.Errorf("committing transaction to rename volume %s: %w", volume.Name(), err)
}
return nil
}
// Pod retrieves a pod given its full ID
func (s *SQLiteState) Pod(id string) (*Pod, error) {
if id == "" {

View file

@ -209,6 +209,9 @@ type State interface { //nolint:interfacebloat
// and checks if the volume is being used by any container. If it is
// a slice of container IDs using the volume is returned
VolumeInUse(volume *Volume) ([]string, error)
// RenameVolume renames the given volume and persists the provided
// configuration. The new name must not already be in use.
RenameVolume(volume *Volume, newCfg *VolumeConfig) error
// AddVolume adds the specified volume to state. The volume's name
// must be unique within the list of existing volumes
AddVolume(volume *Volume) error

View file

@ -5,7 +5,6 @@ package libpod
import (
"fmt"
"os"
"path/filepath"
"go.podman.io/podman/v6/libpod/define"
)
@ -29,8 +28,9 @@ func (v *Volume) teardownStorage() error {
return nil
}
// TODO: Should this be converted to use v.config.MountPoint?
return os.RemoveAll(filepath.Join(v.runtime.config.Engine.VolumePath, v.Name()))
// Remove the whole volume directory rather than v.config.MountPoint,
// which only points at the _data subdirectory, so no state is left behind.
return os.RemoveAll(v.runtime.volumePath(v.Name()))
}
// Volumes with options set, or a filesystem type, or a device to mount need to

View file

@ -271,3 +271,43 @@ func ImportVolume(w http.ResponseWriter, r *http.Request) {
utils.WriteResponse(w, http.StatusNoContent, "")
}
// RenameVolume renames an existing volume.
func RenameVolume(w http.ResponseWriter, r *http.Request) {
runtime := r.Context().Value(api.RuntimeKey).(*libpod.Runtime)
decoder := r.Context().Value(api.DecoderKey).(*schema.Decoder)
name := utils.GetName(r)
if name == "" {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("volume name must not be empty: %w", define.ErrInvalidArg))
return
}
query := struct {
NewName string `schema:"newName"`
}{}
if err := decoder.Decode(&query, r.URL.Query()); err != nil {
utils.Error(w, http.StatusBadRequest, fmt.Errorf("failed to parse parameters for %s: %w", r.URL.String(), err))
return
}
vol, err := runtime.LookupVolume(name)
if err != nil {
utils.VolumeNotFound(w, name, err)
return
}
if _, err := runtime.RenameVolume(r.Context(), vol, query.NewName); err != nil {
switch {
case errors.Is(err, define.ErrVolumeExists), errors.Is(err, define.ErrVolumeBeingUsed):
utils.Error(w, http.StatusConflict, err)
case errors.Is(err, define.ErrInvalidArg):
utils.Error(w, http.StatusBadRequest, err)
default:
utils.InternalServerError(w, err)
}
return
}
utils.WriteResponse(w, http.StatusNoContent, nil)
}

View file

@ -214,6 +214,41 @@ func (s *APIServer) registerVolumeHandlers(r *mux.Router) error {
// $ref: "#/responses/internalError"
r.Handle(VersionedPath("/libpod/volumes/{name}/import"), s.APIHandler(libpod.ImportVolume)).Methods(http.MethodPost)
// swagger:operation POST /libpod/volumes/{name}/rename libpod VolumeRenameLibpod
// ---
// tags:
// - volumes
// summary: Rename an existing volume
// description: |
// Rename a volume when the source volume exists, the new name is valid and unused,
// the volume is not mounted or used by any container, and the volume uses the
// local driver.
// parameters:
// - in: path
// name: name
// type: string
// required: true
// description: the name or ID of the volume
// - in: query
// name: newName
// type: string
// required: true
// description: new volume name
// produces:
// - application/json
// responses:
// 204:
// description: Volume successfully renamed
// 404:
// $ref: "#/responses/volumeNotFound"
// 400:
// $ref: "#/responses/badParamError"
// 409:
// description: Volume is in use or new name already exists
// 500:
// $ref: "#/responses/internalError"
r.Handle(VersionedPath("/libpod/volumes/{name}/rename"), s.APIHandler(libpod.RenameVolume)).Methods(http.MethodPost)
/*
* Docker compatibility endpoints
*/

View file

@ -68,6 +68,46 @@ var _ = Describe("Podman volumes", func() {
Expect(data.Name).To(Equal(vol.Name))
})
It("rename volume", func() {
oldName := "rename-old"
newName := "rename-new"
existingName := "rename-existing"
vol, err := volumes.Create(connText, entities.VolumeCreateOptions{Name: oldName}, nil)
Expect(err).ToNot(HaveOccurred())
err = volumes.Rename(connText, vol.Name, new(volumes.RenameOptions).WithNewName(newName))
Expect(err).ToNot(HaveOccurred())
_, err = volumes.Inspect(connText, oldName, nil)
Expect(err).To(HaveOccurred())
code, err := bindings.CheckResponseCode(err)
Expect(err).ToNot(HaveOccurred())
Expect(code).To(BeNumerically("==", http.StatusNotFound))
data, err := volumes.Inspect(connText, newName, nil)
Expect(err).ToNot(HaveOccurred())
Expect(data.Name).To(Equal(newName))
_, err = volumes.Create(connText, entities.VolumeCreateOptions{Name: existingName}, nil)
Expect(err).ToNot(HaveOccurred())
err = volumes.Rename(connText, newName, new(volumes.RenameOptions).WithNewName(existingName))
Expect(err).To(HaveOccurred())
code, err = bindings.CheckResponseCode(err)
Expect(err).ToNot(HaveOccurred())
Expect(code).To(BeNumerically("==", http.StatusConflict))
session := bt.runPodman([]string{"create", "-v", fmt.Sprintf("%s:/data", newName), alpine.name, "true"})
session.Wait(45)
Expect(session.ExitCode()).To(BeZero())
err = volumes.Rename(connText, newName, new(volumes.RenameOptions).WithNewName("rename-blocked"))
Expect(err).To(HaveOccurred())
code, err = bindings.CheckResponseCode(err)
Expect(err).ToNot(HaveOccurred())
Expect(code).To(BeNumerically("==", http.StatusConflict))
})
It("remove volume", func() {
// removing a bogus volume should result in 404
err := volumes.Remove(connText, "foobar", nil)

View file

@ -0,0 +1,30 @@
package volumes
import (
"context"
"net/http"
"go.podman.io/podman/v6/pkg/bindings"
)
// Rename an existing volume.
func Rename(ctx context.Context, nameOrID string, options *RenameOptions) error {
if options == nil {
options = new(RenameOptions)
}
conn, err := bindings.GetClient(ctx)
if err != nil {
return err
}
params, err := options.ToParams()
if err != nil {
return err
}
response, err := conn.DoRequest(ctx, nil, http.MethodPost, "/volumes/%s/rename", params, nil, nameOrID)
if err != nil {
return err
}
defer response.Body.Close()
return response.Process(nil)
}

View file

@ -42,3 +42,11 @@ type RemoveOptions struct {
//
//go:generate go run ../generator/generator.go ExistsOptions
type ExistsOptions struct{}
// RenameOptions are optional options for renaming volumes
//
//go:generate go run ../generator/generator.go RenameOptions
type RenameOptions struct {
// New name for the volume
NewName *string
}

View file

@ -0,0 +1,33 @@
// Code generated by go generate; DO NOT EDIT.
package volumes
import (
"net/url"
"go.podman.io/podman/v6/pkg/bindings/internal/util"
)
// Changed returns true if named field has been set
func (o *RenameOptions) Changed(fieldName string) bool {
return util.Changed(o, fieldName)
}
// ToParams formats struct fields to be passed to API service
func (o *RenameOptions) ToParams() (url.Values, error) {
return util.ToParams(o)
}
// WithNewName set field NewName to given value
func (o *RenameOptions) WithNewName(value string) *RenameOptions {
o.NewName = &value
return o
}
// GetNewName returns value of field NewName
func (o *RenameOptions) GetNewName() string {
if o.NewName == nil {
var z string
return z
}
return *o.NewName
}

View file

@ -120,6 +120,7 @@ type ContainerEngine interface { //nolint:interfacebloat
VolumeList(ctx context.Context, opts VolumeListOptions) ([]*VolumeListReport, error)
VolumeMount(ctx context.Context, namesOrIds []string) ([]*VolumeMountReport, error)
VolumePrune(ctx context.Context, options VolumePruneOptions) ([]*reports.PruneReport, error)
VolumeRename(ctx context.Context, nameOrID string, options VolumeRenameOptions) error
VolumeRm(ctx context.Context, namesOrIds []string, opts VolumeRmOptions) ([]*VolumeRmReport, error)
VolumeUnmount(ctx context.Context, namesOrIds []string) ([]*VolumeUnmountReport, error)
VolumeReload(ctx context.Context) (*VolumeReloadReport, error)

View file

@ -52,6 +52,11 @@ type VolumeExportOptions struct {
Output io.Writer
}
// VolumeRenameOptions describes the options for renaming a volume.
type VolumeRenameOptions struct {
NewName string
}
// VolumeImportOptions describes the options required to import a volume
type VolumeImportOptions struct {
// Input will be closed upon being fully consumed

View file

@ -280,3 +280,12 @@ func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, opti
return nil
}
func (ic *ContainerEngine) VolumeRename(ctx context.Context, nameOrID string, opts entities.VolumeRenameOptions) error {
vol, err := ic.Libpod.LookupVolume(nameOrID)
if err != nil {
return err
}
_, err = ic.Libpod.RenameVolume(ctx, vol, opts.NewName)
return err
}

View file

@ -121,3 +121,7 @@ func (ic *ContainerEngine) VolumeExport(_ context.Context, nameOrID string, opti
func (ic *ContainerEngine) VolumeImport(_ context.Context, nameOrID string, options entities.VolumeImportOptions) error {
return volumes.Import(ic.ClientCtx, nameOrID, options.Input)
}
func (ic *ContainerEngine) VolumeRename(_ context.Context, nameOrID string, opts entities.VolumeRenameOptions) error {
return volumes.Rename(ic.ClientCtx, nameOrID, new(volumes.RenameOptions).WithNewName(opts.NewName))
}

View file

@ -84,6 +84,13 @@ t GET libpod/volumes/nonexistent/json 404 \
.message~.* \
.response=404
## rename volume
t POST libpod/volumes/foo2/rename?newName=foo2-renamed 204
t GET libpod/volumes/foo2/json 404
t GET libpod/volumes/foo2-renamed/json 200 \
.Name=foo2-renamed \
.Mountpoint=$volumepath/foo2-renamed/_data
## Remove volumes
t DELETE libpod/volumes/foo1 204
#After remove foo1 volume, this volume should not exist

View file

@ -0,0 +1,117 @@
//go:build linux || freebsd
package integration
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
. "go.podman.io/podman/v6/test/utils"
)
var _ = Describe("Podman volume rename", func() {
AfterEach(func() {
podmanTest.CleanupVolume()
})
It("podman volume rename", func() {
podmanTest.PodmanExitCleanly("volume", "create", "myvol")
rename := podmanTest.PodmanExitCleanly("volume", "rename", "myvol", "newvol")
Expect(rename.OutputToString()).To(BeEmpty())
check := podmanTest.PodmanExitCleanly("volume", "inspect", "newvol")
Expect(check.OutputToString()).To(ContainSubstring("newvol"))
// Old name should no longer exist
check = podmanTest.Podman([]string{"volume", "inspect", "myvol"})
check.WaitWithDefaultTimeout()
Expect(check).To(ExitWithError(125, "no such volume"))
})
It("podman volume rename data persists", func() {
podmanTest.PodmanExitCleanly("volume", "create", "myvol")
podmanTest.PodmanExitCleanly("run", "--rm", "--network=none", "-v", "myvol:/data", ALPINE, "sh", "-c", "echo hello > /data/testfile")
podmanTest.PodmanExitCleanly("volume", "rename", "myvol", "newvol")
session := podmanTest.PodmanExitCleanly("run", "--rm", "--network=none", "-v", "newvol:/data", ALPINE, "cat", "/data/testfile")
Expect(session.OutputToString()).To(Equal("hello"))
})
It("podman volume rename fails when used by a stopped container", func() {
podmanTest.PodmanExitCleanly("volume", "create", "myvol")
podmanTest.PodmanExitCleanly("create", "-v", "myvol:/data", ALPINE, "true")
session := podmanTest.Podman([]string{"volume", "rename", "myvol", "newvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "volume is being used"))
})
It("podman volume rename fails when used by a running container", func() {
podmanTest.PodmanExitCleanly("volume", "create", "myvol")
podmanTest.PodmanExitCleanly("run", "-d", "--network=none", "-v", "myvol:/data", ALPINE, "top")
session := podmanTest.Podman([]string{"volume", "rename", "myvol", "newvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "volume is being used"))
})
It("podman volume rename handles error cases", func() {
podmanTest.PodmanExitCleanly("volume", "create", "vol1")
podmanTest.PodmanExitCleanly("volume", "create", "vol2")
session := podmanTest.Podman([]string{"volume", "rename", "vol1", "vol2"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "volume already exists"))
session = podmanTest.Podman([]string{"volume", "rename", "nosuchvol", "newvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "no such volume"))
session = podmanTest.Podman([]string{"volume", "rename", "vol1", "invalid/name"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "invalid argument"))
session = podmanTest.Podman([]string{"volume", "rename", "vol1", " newvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "names must match"))
podmanTest.AddImageToRWStore(FEDORA_MINIMAL)
podmanTest.PodmanExitCleanly("volume", "create", "--driver", "image", "--opt", "image="+FEDORA_MINIMAL, "imagevol")
session = podmanTest.Podman([]string{"volume", "rename", "imagevol", "newvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "rename is not supported for volumes using driver \"image\""))
})
It("podman volume rename to same name succeeds", func() {
podmanTest.PodmanExitCleanly("volume", "create", "myvol")
rename := podmanTest.PodmanExitCleanly("volume", "rename", "myvol", "myvol")
Expect(rename.OutputToString()).To(BeEmpty())
inspect := podmanTest.PodmanExitCleanly("volume", "inspect", "myvol")
Expect(inspect.OutputToString()).To(ContainSubstring("myvol"))
})
It("podman volume rename converts anonymous volumes to named volumes", func() {
ctr := podmanTest.PodmanExitCleanly("create", "-v", "/data", ALPINE, "true")
volumes := podmanTest.PodmanExitCleanly("volume", "list", "--quiet")
volumeNames := volumes.OutputToStringArray()
Expect(volumeNames).To(HaveLen(1))
podmanTest.PodmanExitCleanly("rm", ctr.OutputToString())
podmanTest.PodmanExitCleanly("volume", "rename", volumeNames[0], "namedvol")
inspect := podmanTest.PodmanExitCleanly("volume", "inspect", "--format", "{{.Anonymous}}", "namedvol")
Expect(inspect.OutputToString()).To(Equal("false"))
})
It("podman volume rename requires exactly 2 args", func() {
session := podmanTest.Podman([]string{"volume", "rename", "myvol"})
session.WaitWithDefaultTimeout()
Expect(session).To(ExitWithError(125, "accepts 2 arg(s)"))
})
})