mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-08-05 00:15:44 +00:00
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:
parent
e2a4453cbd
commit
d01b7ae534
26 changed files with 607 additions and 18 deletions
|
|
@ -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],
|
||||
}
|
||||
|
|
|
|||
34
cmd/podman/volumes/rename.go
Normal file
34
cmd/podman/volumes/rename.go
Normal 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],
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
34
docs/source/markdown/podman-volume-rename.1.md
Normal file
34
docs/source/markdown/podman-volume-rename.1.md
Normal 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
|
||||
|
|
@ -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. |
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
18
libpod/sqlite_constraint_cgo.go
Normal file
18
libpod/sqlite_constraint_cgo.go
Normal 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
|
||||
}
|
||||
14
libpod/sqlite_constraint_nocgo.go
Normal file
14
libpod/sqlite_constraint_nocgo.go
Normal 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
|
||||
}
|
||||
|
|
@ -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 == "" {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
30
pkg/bindings/volumes/rename.go
Normal file
30
pkg/bindings/volumes/rename.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
33
pkg/bindings/volumes/types_rename_options.go
Normal file
33
pkg/bindings/volumes/types_rename_options.go
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
117
test/e2e/volume_rename_test.go
Normal file
117
test/e2e/volume_rename_test.go
Normal 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)"))
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue