Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for 'image' volume driver #15841

Merged
merged 1 commit into from
Sep 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions docs/source/markdown/podman-volume-create.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@ driver options can be set using the **--opt** flag.

#### **--driver**=*driver*

Specify the volume driver name (default **local**). Setting this to a value other than **local** 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.
Specify the volume driver name (default **local**).
There are two drivers supported by Podman itself: **local** and **image**.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we support volume plugins?

The **local** driver uses a directory on disk as the backend by default, but can also use the **mount(8)** command to mount a filesystem as the volume if **--opt** is specified.
The **image** driver uses an image as the backing store of for the volume.
An overlay filesystem will be created, which allows changes to the volume to be committed as a new layer on top of the image.
Using a value other than **local or **image**, Podman will attempt 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.

#### **--help**

Expand All @@ -43,7 +49,10 @@ The `o` option sets options for the mount, and is equivalent to the `-o` flag to
- The `o` option supports using volume options other than the UID/GID options with the **local** driver and requires root privileges.
- The `o` options supports the `timeout` option which allows users to set a driver specific timeout in seconds before volume creation fails. For example, **--opts=o=timeout=10** sets a driver timeout of 10 seconds.

When not using the **local** driver, the given options are passed directly to the volume plugin. In this case, supported options are dictated by the plugin in question, not Podman.
For the **image** driver, the only supported option is `image`, which specifies the image the volume is based on.
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.

## EXAMPLES

Expand All @@ -57,6 +66,8 @@ $ podman volume create --label foo=bar myvol
# podman volume create --opt device=tmpfs --opt type=tmpfs --opt o=nodev,noexec myvol

# podman volume create --opt device=tmpfs --opt type=tmpfs --opt o=uid=1000,gid=1000 testvol

# podman volume create --driver image --opt image=fedora:latest fedoraVol
```

## QUOTAS
Expand Down
53 changes: 53 additions & 0 deletions libpod/boltdb_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ func NewBoltState(path string, runtime *Runtime) (State, error) {
runtimeConfigBkt,
exitCodeBkt,
exitCodeTimeStampBkt,
volCtrsBkt,
}

// Does the DB need an update?
Expand Down Expand Up @@ -2551,6 +2552,11 @@ func (s *BoltState) AddVolume(volume *Volume) error {
return err
}

volCtrsBkt, err := getVolumeContainersBucket(tx)
if err != nil {
return err
}

// Check if we already have a volume with the given name
volExists := allVolsBkt.Get(volName)
if volExists != nil {
Expand Down Expand Up @@ -2580,6 +2586,12 @@ func (s *BoltState) AddVolume(volume *Volume) error {
}
}

if volume.config.StorageID != "" {
if err := volCtrsBkt.Put([]byte(volume.config.StorageID), volName); err != nil {
return fmt.Errorf("storing volume %s container ID in DB: %w", volume.Name(), err)
}
}

if err := allVolsBkt.Put(volName, volName); err != nil {
return fmt.Errorf("storing volume %s in all volumes bucket in DB: %w", volume.Name(), err)
}
Expand Down Expand Up @@ -2619,6 +2631,11 @@ func (s *BoltState) RemoveVolume(volume *Volume) error {
return err
}

volCtrIDBkt, err := getVolumeContainersBucket(tx)
if err != nil {
return err
}

// Check if the volume exists
volDB := volBkt.Bucket(volName)
if volDB == nil {
Expand Down Expand Up @@ -2665,6 +2682,11 @@ func (s *BoltState) RemoveVolume(volume *Volume) error {
if err := volBkt.DeleteBucket(volName); err != nil {
return fmt.Errorf("removing volume %s from DB: %w", volume.Name(), err)
}
if volume.config.StorageID != "" {
if err := volCtrIDBkt.Delete([]byte(volume.config.StorageID)); err != nil {
return fmt.Errorf("removing volume %s container ID from DB: %w", volume.Name(), err)
}
}

return nil
})
Expand Down Expand Up @@ -3618,3 +3640,34 @@ func (s *BoltState) AllPods() ([]*Pod, error) {

return pods, nil
}

// ContainerIDIsVolume checks if the given c/storage container ID is used as
// backing storage for a volume.
func (s *BoltState) ContainerIDIsVolume(id string) (bool, error) {
if !s.valid {
return false, define.ErrDBClosed
}

isVol := false

db, err := s.getDBCon()
if err != nil {
return false, err
}
defer s.deferredCloseDBCon(db)

err = db.View(func(tx *bolt.Tx) error {
volCtrsBkt, err := getVolumeContainersBucket(tx)
if err != nil {
return err
}

volName := volCtrsBkt.Get([]byte(id))
if volName != nil {
isVol = true
}

return nil
})
return isVol, err
}
14 changes: 13 additions & 1 deletion libpod/boltdb_state_internal.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const (
execName = "exec"
aliasesName = "aliases"
runtimeConfigName = "runtime-config"
volumeCtrsName = "volume-ctrs"

exitCodeName = "exit-code"
exitCodeTimeStampName = "exit-code-time-stamp"
Expand Down Expand Up @@ -67,6 +68,7 @@ var (
dependenciesBkt = []byte(dependenciesName)
volDependenciesBkt = []byte(volCtrDependencies)
networksBkt = []byte(networksName)
volCtrsBkt = []byte(volumeCtrsName)

exitCodeBkt = []byte(exitCodeName)
exitCodeTimeStampBkt = []byte(exitCodeTimeStampName)
Expand Down Expand Up @@ -384,6 +386,14 @@ func getExitCodeTimeStampBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
return bkt, nil
}

func getVolumeContainersBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(volCtrsBkt)
if bkt == nil {
return nil, fmt.Errorf("volume containers bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}

func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig, ctrsBkt *bolt.Bucket) error {
ctrBkt := ctrsBkt.Bucket(id)
if ctrBkt == nil {
Expand Down Expand Up @@ -528,6 +538,9 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
}
}

// Need this for UsesVolumeDriver() so set it now.
volume.runtime = s.runtime

// Retrieve volume driver
if volume.UsesVolumeDriver() {
plugin, err := s.runtime.getVolumePlugin(volume.config)
Expand All @@ -550,7 +563,6 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
}
volume.lock = lock

volume.runtime = s.runtime
volume.valid = true

return nil
Expand Down
4 changes: 4 additions & 0 deletions libpod/define/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ type InfoData struct {
// itself.
const VolumeDriverLocal = "local"

// VolumeDriverImage is the "image" volume driver. It is managed by Libpod and
// uses volumes backed by an image.
const VolumeDriverImage = "image"

const (
OCIManifestDir = "oci-dir"
OCIArchive = "oci-archive"
Expand Down
3 changes: 3 additions & 0 deletions libpod/define/volume_inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ type InspectVolumeData struct {
NeedsChown bool `json:"NeedsChown,omitempty"`
// Timeout is the specified driver timeout if given
Timeout uint `json:"Timeout,omitempty"`
// StorageID is the ID of the container backing the volume in c/storage.
// Only used with Image Volumes.
StorageID string `json:"StorageID,omitempty"`
}

type VolumeReload struct {
Expand Down
3 changes: 3 additions & 0 deletions libpod/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -1091,6 +1091,9 @@ func (r *Runtime) getVolumePlugin(volConfig *VolumeConfig) (*plugin.VolumePlugin

pluginPath, ok := r.config.Engine.VolumePlugins[name]
if !ok {
if name == define.VolumeDriverImage {
return nil, nil
}
return nil, fmt.Errorf("no volume plugin with name %s available: %w", name, define.ErrMissingPlugin)
}

Expand Down
11 changes: 11 additions & 0 deletions libpod/runtime_cstorage.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ func (r *Runtime) RemoveStorageContainer(idOrName string, force bool) error {
return fmt.Errorf("refusing to remove %q as it exists in libpod as container %s: %w", idOrName, ctr.ID, define.ErrCtrExists)
}

// Error out if this is an image-backed volume
allVols, err := r.state.AllVolumes()
if err != nil {
return err
}
for _, vol := range allVols {
if vol.config.Driver == define.VolumeDriverImage && vol.config.StorageID == ctr.ID {
return fmt.Errorf("refusing to remove %q as it exists in libpod as an image-backed volume %s: %w", idOrName, vol.Name(), define.ErrCtrExists)
}
}

if !force {
timesMounted, err := r.store.Mounted(ctr.ID)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion libpod/runtime_ctr.go
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ func (r *Runtime) setupContainer(ctx context.Context, ctr *Container) (_ *Contai
volOptions = append(volOptions, parsedOptions...)
}
}
newVol, err := r.newVolume(false, volOptions...)
newVol, err := r.newVolume(ctx, false, volOptions...)
if err != nil {
return nil, fmt.Errorf("creating named volume %q: %w", vol.Name, err)
}
Expand Down
21 changes: 21 additions & 0 deletions libpod/runtime_img.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,23 @@ func (r *Runtime) RemoveContainersForImageCallback(ctx context.Context) libimage
}
}
}

// Need to handle volumes with the image driver
vols, err := r.state.AllVolumes()
if err != nil {
return err
}
for _, vol := range vols {
if vol.config.Driver != define.VolumeDriverImage || vol.config.StorageImageID != imageID {
continue
}
// Do a force removal of the volume, and all containers
// using it.
if err := r.RemoveVolume(ctx, vol, true, nil); err != nil {
return fmt.Errorf("removing image %s: volume %s backed by image could not be removed: %w", imageID, vol.Name(), err)
}
}

// Note that `libimage` will take care of removing any leftover
// containers from the storage.
return nil
Expand All @@ -74,6 +91,10 @@ func (r *Runtime) IsExternalContainerCallback(_ context.Context) libimage.IsExte
if errors.Is(err, define.ErrNoSuchCtr) {
return true, nil
}
isVol, err := r.state.ContainerIDIsVolume(idOrName)
if err == nil && !isVol {
return true, nil
}
return false, nil
}
}
Expand Down
61 changes: 58 additions & 3 deletions libpod/runtime_volume_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,28 @@ import (
"github.com/containers/podman/v4/libpod/define"
"github.com/containers/podman/v4/libpod/events"
volplugin "github.com/containers/podman/v4/libpod/plugin"
"github.com/containers/storage"
"github.com/containers/storage/drivers/quota"
"github.com/containers/storage/pkg/idtools"
"github.com/containers/storage/pkg/stringid"
pluginapi "github.com/docker/go-plugins-helpers/volume"
"github.com/sirupsen/logrus"
)

const volumeSuffix = "+volume"

// NewVolume creates a new empty volume
func (r *Runtime) NewVolume(ctx context.Context, options ...VolumeCreateOption) (*Volume, error) {
if !r.valid {
return nil, define.ErrRuntimeStopped
}
return r.newVolume(false, options...)
return r.newVolume(ctx, false, options...)
}

// newVolume creates a new empty volume with the given options.
// The createPluginVolume can be set to true to make it not create the volume in the volume plugin,
// this is required for the UpdateVolumePlugins() function. If you are not sure, set this to false.
func (r *Runtime) newVolume(noCreatePluginVolume bool, options ...VolumeCreateOption) (_ *Volume, deferredErr error) {
func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, options ...VolumeCreateOption) (_ *Volume, deferredErr error) {
volume := newVolume(r)
for _, option := range options {
if err := option(volume); err != nil {
Expand Down Expand Up @@ -83,6 +86,50 @@ func (r *Runtime) newVolume(noCreatePluginVolume bool, options ...VolumeCreateOp
return nil, fmt.Errorf("invalid mount option %s for driver 'local': %w", key, define.ErrInvalidArg)
}
}
} else if volume.config.Driver == define.VolumeDriverImage && !volume.UsesVolumeDriver() {
logrus.Debugf("Creating image-based volume")
var imgString string
// Validate options
for key, val := range volume.config.Options {
switch strings.ToLower(key) {
case "image":
imgString = val
default:
return nil, fmt.Errorf("invalid mount option %s for driver 'image': %w", key, define.ErrInvalidArg)
}
}

if imgString == "" {
return nil, fmt.Errorf("must provide an image name when creating a volume with the image driver: %w", define.ErrInvalidArg)
}

// Look up the image
image, _, err := r.libimageRuntime.LookupImage(imgString, nil)
if err != nil {
return nil, fmt.Errorf("looking up image %s to create volume failed: %w", imgString, err)
}

// Generate a c/storage name and ID for the volume.
// Use characters Podman does not allow for the name, to ensure
// no collision with containers.
volume.config.StorageID = stringid.GenerateRandomID()
volume.config.StorageName = volume.config.Name + volumeSuffix
volume.config.StorageImageID = image.ID()

// Create a backing container in c/storage.
storageConfig := storage.ContainerOptions{
LabelOpts: []string{"filetype:container_file_t:s0"},
}
if _, err := r.storageService.CreateContainerStorage(ctx, r.imageContext, imgString, image.ID(), volume.config.StorageName, volume.config.StorageID, storageConfig); err != nil {
return nil, fmt.Errorf("creating backing storage for image driver: %w", err)
}
defer func() {
if deferredErr != nil {
if err := r.storageService.DeleteContainer(volume.config.StorageID); err != nil {
logrus.Errorf("Error removing volume %s backing storage: %v", volume.config.Name, err)
}
}
}()
}

// Now we get conditional: we either need to make the volume in the
Expand Down Expand Up @@ -196,7 +243,7 @@ func (r *Runtime) UpdateVolumePlugins(ctx context.Context) *define.VolumeReload
}
for _, vol := range vols {
allPluginVolumes[vol.Name] = struct{}{}
if _, err := r.newVolume(true, WithVolumeName(vol.Name), WithVolumeDriver(driverName)); err != nil {
if _, err := r.newVolume(ctx, true, WithVolumeName(vol.Name), WithVolumeDriver(driverName)); err != nil {
// If the volume exists this is not an error, just ignore it and log. It is very likely
// that the volume from the plugin was already in our db.
if !errors.Is(err, define.ErrVolumeExists) {
Expand Down Expand Up @@ -375,6 +422,14 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
return fmt.Errorf("volume %s could not be removed from plugin %s: %w", v.Name(), v.Driver(), err)
}
}
} else if v.config.Driver == define.VolumeDriverImage {
if err := v.runtime.storageService.DeleteContainer(v.config.StorageID); err != nil {
// Storage container is already gone, no problem.
if !(errors.Is(err, storage.ErrNotAContainer) || errors.Is(err, storage.ErrContainerUnknown)) {
return fmt.Errorf("removing volume %s storage: %w", v.Name(), err)
}
logrus.Infof("Storage for volume %s already removed", v.Name())
}
}

// Remove the volume from the state
Expand Down
8 changes: 8 additions & 0 deletions libpod/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,14 @@ type State interface {
// As with RemoveExecSession, container state will not be modified.
RemoveContainerExecSessions(ctr *Container) error

// ContainerIDIsVolume checks if the given container ID is in use by a
// volume.
// Some volumes are backed by a c/storage container. These do not have a
// corresponding Container struct in Libpod, but rather a Volume.
// This determines if a given ID from c/storage is used as a backend by
// a Podman volume.
ContainerIDIsVolume(id string) (bool, error)

// PLEASE READ FULL DESCRIPTION BEFORE USING.
// Rewrite a container's configuration.
// This function breaks libpod's normal prohibition on a read-only
Expand Down
Loading