From 5a8a71ed817a4fa50fd9444846a50b76f25228d1 Mon Sep 17 00:00:00 2001
From: Matthew Heon <matthew.heon@pm.me>
Date: Fri, 30 Aug 2019 16:09:17 -0400
Subject: Add volume state

We need to be able to track the number of times a volume has been
mounted for tmpfs/nfs/etc volumes. As such, we need a mutable
state for volumes. Add one, with the expected update/save methods
in both states.

There is backwards compat here, in that older volumes without a
state will still be accepted.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
---
 libpod/boltdb_state.go          | 114 ++++++++++++++++++++++++++++++++++++++++
 libpod/boltdb_state_internal.go |   8 +++
 libpod/in_memory_state.go       |  30 +++++++++++
 libpod/state.go                 |   4 ++
 libpod/volume.go                |  31 +++++++++--
 5 files changed, 183 insertions(+), 4 deletions(-)

(limited to 'libpod')

diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go
index 1de8d80c9..ff5e62ce2 100644
--- a/libpod/boltdb_state.go
+++ b/libpod/boltdb_state.go
@@ -1352,6 +1352,16 @@ func (s *BoltState) AddVolume(volume *Volume) error {
 		return errors.Wrapf(err, "error marshalling volume %s config to JSON", volume.Name())
 	}
 
+	// Volume state is allowed to not exist
+	var volStateJSON []byte
+	if volume.state != nil {
+		stateJSON, err := json.Marshal(volume.state)
+		if err != nil {
+			return errors.Wrapf(err, "error marshalling volume %s state to JSON", volume.Name())
+		}
+		volStateJSON = stateJSON
+	}
+
 	db, err := s.getDBCon()
 	if err != nil {
 		return err
@@ -1392,6 +1402,12 @@ func (s *BoltState) AddVolume(volume *Volume) error {
 			return errors.Wrapf(err, "error storing volume %s configuration in DB", volume.Name())
 		}
 
+		if volStateJSON != nil {
+			if err := newVol.Put(stateKey, volStateJSON); err != nil {
+				return errors.Wrapf(err, "error storing volume %s state in DB", volume.Name())
+			}
+		}
+
 		if err := allVolsBkt.Put(volName, volName); err != nil {
 			return errors.Wrapf(err, "error storing volume %s in all volumes bucket in DB", volume.Name())
 		}
@@ -1483,6 +1499,103 @@ func (s *BoltState) RemoveVolume(volume *Volume) error {
 	return err
 }
 
+// UpdateVolume updates the volume's state from the database.
+func (s *BoltState) UpdateVolume(volume *Volume) error {
+	if !s.valid {
+		return define.ErrDBClosed
+	}
+
+	if !volume.valid {
+		return define.ErrVolumeRemoved
+	}
+
+	newState := new(VolumeState)
+	volumeName := []byte(volume.Name())
+
+	db, err := s.getDBCon()
+	if err != nil {
+		return err
+	}
+	defer s.deferredCloseDBCon(db)
+
+	err = db.View(func(tx *bolt.Tx) error {
+		volBucket, err := getVolBucket(tx)
+		if err != nil {
+			return err
+		}
+
+		volToUpdate := volBucket.Bucket(volumeName)
+		if volToUpdate == nil {
+			volume.valid = false
+			return errors.Wrapf(define.ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
+		}
+
+		stateBytes := volToUpdate.Get(stateKey)
+		if stateBytes == nil {
+			// Having no state is valid.
+			// Return nil, use the empty state.
+			return nil
+		}
+
+		if err := json.Unmarshal(stateBytes, newState); err != nil {
+			return errors.Wrapf(err, "error unmarshalling volume %s state", volume.Name())
+		}
+
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	volume.state = newState
+
+	return nil
+}
+
+// SaveVolume saves the volume's state to the database.
+func (s *BoltState) SaveVolume(volume *Volume) error {
+	if !s.valid {
+		return define.ErrDBClosed
+	}
+
+	if !volume.valid {
+		return define.ErrVolumeRemoved
+	}
+
+	volumeName := []byte(volume.Name())
+
+	var newStateJSON []byte
+	if volume.state != nil {
+		stateJSON, err := json.Marshal(volume.state)
+		if err != nil {
+			return errors.Wrapf(err, "error marshalling volume %s state to JSON", volume.Name())
+		}
+		newStateJSON = stateJSON
+	}
+
+	db, err := s.getDBCon()
+	if err != nil {
+		return err
+	}
+	defer s.deferredCloseDBCon(db)
+
+	err = db.Update(func(tx *bolt.Tx) error {
+		volBucket, err := getVolBucket(tx)
+		if err != nil {
+			return err
+		}
+
+		volToUpdate := volBucket.Bucket(volumeName)
+		if volToUpdate == nil {
+			volume.valid = false
+			return errors.Wrapf(define.ErrNoSuchVolume, "no volume with name %s found in database", volume.Name())
+		}
+
+		return volToUpdate.Put(stateKey, newStateJSON)
+	})
+	return err
+}
+
 // AllVolumes returns all volumes present in the state
 func (s *BoltState) AllVolumes() ([]*Volume, error) {
 	if !s.valid {
@@ -1551,6 +1664,7 @@ func (s *BoltState) Volume(name string) (*Volume, error) {
 
 	volume := new(Volume)
 	volume.config = new(VolumeConfig)
+	volume.state = new(VolumeState)
 
 	db, err := s.getDBCon()
 	if err != nil {
diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go
index 6e4179835..8dc3d1309 100644
--- a/libpod/boltdb_state_internal.go
+++ b/libpod/boltdb_state_internal.go
@@ -449,6 +449,14 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
 		return errors.Wrapf(err, "error unmarshalling volume %s config from DB", string(name))
 	}
 
+	// Volume state is allowed to be nil for legacy compatability
+	volStateBytes := volDB.Get(stateKey)
+	if volStateBytes != nil {
+		if err := json.Unmarshal(volStateBytes, volume.state); err != nil {
+			return errors.Wrapf(err, "error unmarshalling volume %s state from DB", string(name))
+		}
+	}
+
 	// Get the lock
 	lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)
 	if err != nil {
diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go
index a9b735327..280ae5f5c 100644
--- a/libpod/in_memory_state.go
+++ b/libpod/in_memory_state.go
@@ -507,6 +507,36 @@ func (s *InMemoryState) RemoveVolume(volume *Volume) error {
 	return nil
 }
 
+// UpdateVolume updates a volume from the database.
+// For the in-memory state, this is a no-op.
+func (s *InMemoryState) UpdateVolume(volume *Volume) error {
+	if !volume.valid {
+		return define.ErrVolumeRemoved
+	}
+
+	if _, ok := s.volumes[volume.Name()]; !ok {
+		volume.valid = false
+		return errors.Wrapf(define.ErrNoSuchVolume, "volume with name %q not found in state", volume.Name())
+	}
+
+	return nil
+}
+
+// SaveVolume saves a volume's state to the database.
+// For the in-memory state, this is a no-op.
+func (s *InMemoryState) SaveVolume(volume *Volume) error {
+	if !volume.valid {
+		return define.ErrVolumeRemoved
+	}
+
+	if _, ok := s.volumes[volume.Name()]; !ok {
+		volume.valid = false
+		return errors.Wrapf(define.ErrNoSuchVolume, "volume with name %q not found in state", volume.Name())
+	}
+
+	return nil
+}
+
 // VolumeInUse checks if the given volume is being used by at least one container
 func (s *InMemoryState) VolumeInUse(volume *Volume) ([]string, error) {
 	if !volume.valid {
diff --git a/libpod/state.go b/libpod/state.go
index 5d704e69a..db4667ad6 100644
--- a/libpod/state.go
+++ b/libpod/state.go
@@ -203,6 +203,10 @@ type State interface {
 	// RemoveVolume removes the specified volume.
 	// Only volumes that have no container dependencies can be removed
 	RemoveVolume(volume *Volume) error
+	// UpdateVolume updates the volume's state from the database.
+	UpdateVolume(volume *Volume) error
+	// SaveVolume saves a volume's state to the database.
+	SaveVolume(volume *Volume) error
 	// AllVolumes returns all the volumes available in the state
 	AllVolumes() ([]*Volume, error)
 }
diff --git a/libpod/volume.go b/libpod/volume.go
index e6e92c3ac..5970867c3 100644
--- a/libpod/volume.go
+++ b/libpod/volume.go
@@ -6,17 +6,20 @@ import (
 	"github.com/containers/libpod/libpod/lock"
 )
 
-// Volume is the type used to create named volumes
-// TODO: all volumes should be created using this and the Volume API
+// Volume is a libpod named volume.
+// Named volumes may be shared by multiple containers, and may be created using
+// more complex options than normal bind mounts. They may be backed by a mounted
+// filesystem on the host.
 type Volume struct {
 	config *VolumeConfig
+	state  *VolumeState
 
 	valid   bool
 	runtime *Runtime
 	lock    lock.Locker
 }
 
-// VolumeConfig holds the volume's config information
+// VolumeConfig holds the volume's immutable configuration.
 type VolumeConfig struct {
 	// Name of the volume.
 	Name string `json:"name"`
@@ -34,7 +37,15 @@ type VolumeConfig struct {
 	// Options to pass to the volume driver. For the local driver, this is
 	// a list of mount options. For other drivers, they are passed to the
 	// volume driver handling the volume.
-	Options map[string]string `json:"volumeOptions"`
+	Options map[string]string `json:"volumeOptions,omitempty"`
+	// Type is the type of the volume. This is only used with the local
+	// driver. It the the filesystem that we will attempt to mount - nfs,
+	// tmpfs, etc.
+	Type string `json:"type,omitempty"`
+	// Device is the device of the volume. This is only used with the local
+	// driver, and only with some filesystem types (e.g., not required by
+	// tmpfs). It is the device to mount.
+	Device string `json:"device,omitempty"`
 	// Whether this volume was created for a specific container and will be
 	// removed with it.
 	IsCtrSpecific bool `json:"ctrSpecific"`
@@ -44,6 +55,18 @@ type VolumeConfig struct {
 	GID int `json:"gid"`
 }
 
+// VolumeState holds the volume's mutable state.
+// Volumes are not guaranteed to have a state. Only volumes using the Local
+// driver that have mount options set will create a state.
+type VolumeState struct {
+	// MountCount is the number of times this volume has been requested to
+	// be mounted.
+	// It is incremented on mount() and decremented on unmount().
+	// On incrementing from 0, the volume will be mounted on the host.
+	// On decrementing to 0, the volume will be unmounted on the host.
+	MountCount uint `json:"mountCount"`
+}
+
 // Name retrieves the volume's name
 func (v *Volume) Name() string {
 	return v.config.Name
-- 
cgit v1.2.3-54-g00ecf