aboutsummaryrefslogtreecommitdiff
path: root/libpod
diff options
context:
space:
mode:
authorDaniel J Walsh <dwalsh@redhat.com>2017-11-20 14:45:01 -0500
committerGitHub <noreply@github.com>2017-11-20 14:45:01 -0500
commit6e0944f2f128534caaf884251d1e42fa4f1ba235 (patch)
tree4ecca5c1388b93de16b1dd91244ffb3488b0ddd1 /libpod
parent3e04604dc2619b1502b609083c3b6ecb0949f1d5 (diff)
parentf2894eda689a24c069b55b1e2ad3e6136836b326 (diff)
downloadpodman-6e0944f2f128534caaf884251d1e42fa4f1ba235.tar.gz
podman-6e0944f2f128534caaf884251d1e42fa4f1ba235.tar.bz2
podman-6e0944f2f128534caaf884251d1e42fa4f1ba235.zip
Merge pull request #26 from mheon/sql_state
Implementation of SQL-backed state
Diffstat (limited to 'libpod')
-rw-r--r--libpod/container.go141
-rw-r--r--libpod/errors.go4
-rw-r--r--libpod/in_memory_state.go21
-rw-r--r--libpod/options.go36
-rw-r--r--libpod/runtime.go47
-rw-r--r--libpod/runtime_ctr.go26
-rw-r--r--libpod/sql_state.go567
-rw-r--r--libpod/sql_state_internal.go246
-rw-r--r--libpod/state.go8
9 files changed, 975 insertions, 121 deletions
diff --git a/libpod/container.go b/libpod/container.go
index 8e84a1f3e..ffc4c6314 100644
--- a/libpod/container.go
+++ b/libpod/container.go
@@ -87,21 +87,17 @@ type containerConfig struct {
Spec *spec.Spec `json:"spec"`
ID string `json:"id"`
Name string `json:"name"`
- // RootfsFromImage indicates whether the container uses a root
- // filesystem from an image, or from a user-provided directory
- RootfsFromImage bool
- // Directory used as a root filesystem if not configured with an image
- RootfsDir string `json:"rootfsDir,omitempty"`
// Information on the image used for the root filesystem
RootfsImageID string `json:"rootfsImageID,omitempty"`
RootfsImageName string `json:"rootfsImageName,omitempty"`
- MountLabel string `json:"MountLabel,omitempty"`
UseImageConfig bool `json:"useImageConfig"`
- // Whether to keep container STDIN open
- Stdin bool
+ // SELinux mount label for root filesystem
+ MountLabel string `json:"MountLabel,omitempty"`
// Static directory for container content that will persist across
// reboot
StaticDir string `json:"staticDir"`
+ // Whether to keep container STDIN open
+ Stdin bool
// Pod the container belongs to
Pod string `json:"pod,omitempty"`
// Labels is a set of key-value pairs providing additional information
@@ -154,10 +150,9 @@ func (c *Container) State() (ContainerState, error) {
c.lock.Lock()
defer c.lock.Unlock()
- // TODO uncomment when working
- // if err := c.runtime.ociRuntime.updateContainerStatus(c); err != nil {
- // return ContainerStateUnknown, err
- // }
+ if err := c.runtime.state.UpdateContainer(c); err != nil {
+ return ContainerStateUnknown, errors.Wrapf(err, "error updating container %s state", c.ID())
+ }
return c.state.State, nil
}
@@ -207,18 +202,6 @@ func (c *Container) setupStorage() error {
return errors.Wrapf(ErrCtrStateInvalid, "container %s must be in Configured state to have storage set up", c.ID())
}
- // If we're configured to use a directory, perform that setup
- if !c.config.RootfsFromImage {
- // TODO implement directory-based root filesystems
- return ErrNotImplemented
- }
-
- // Not using a directory, so call into containers/storage
- return c.setupImageRootfs()
-}
-
-// Set up an image as root filesystem using containers/storage
-func (c *Container) setupImageRootfs() error {
// Need both an image ID and image name, plus a bool telling us whether to use the image configuration
if c.config.RootfsImageID == "" || c.config.RootfsImageName == "" {
return errors.Wrapf(ErrInvalidArg, "must provide image ID and image name to use an image")
@@ -248,16 +231,6 @@ func (c *Container) teardownStorage() error {
return errors.Wrapf(ErrCtrStateInvalid, "cannot remove storage for container %s as it is running or paused", c.ID())
}
- if !c.config.RootfsFromImage {
- // TODO implement directory-based root filesystems
- return ErrNotImplemented
- }
-
- return c.teardownImageRootfs()
-}
-
-// Completely remove image-based root filesystem for a container
-func (c *Container) teardownImageRootfs() error {
if c.state.Mounted {
if err := c.runtime.storageService.StopContainer(c.ID()); err != nil {
return errors.Wrapf(err, "error unmounting container %s root filesystem", c.ID())
@@ -273,11 +246,15 @@ func (c *Container) teardownImageRootfs() error {
return nil
}
-// Create creates a container in the OCI runtime
-func (c *Container) Create() (err error) {
+// Init creates a container in the OCI runtime
+func (c *Container) Init() (err error) {
c.lock.Lock()
defer c.lock.Unlock()
+ if err := c.runtime.state.UpdateContainer(c); err != nil {
+ return errors.Wrapf(err, "error updating container %s state", c.ID())
+ }
+
if !c.valid {
return errors.Wrapf(ErrCtrRemoved, "container %s is not valid", c.ID())
}
@@ -286,33 +263,25 @@ func (c *Container) Create() (err error) {
return errors.Wrapf(ErrCtrExists, "container %s has already been created in runtime", c.ID())
}
- // If using containers/storage, mount the container
- if !c.config.RootfsFromImage {
- // TODO implement directory-based root filesystems
- if !c.state.Mounted {
- return ErrNotImplemented
- }
- } else {
- mountPoint, err := c.runtime.storageService.StartContainer(c.ID())
- if err != nil {
- return errors.Wrapf(err, "error mounting storage for container %s", c.ID())
- }
- c.state.Mounted = true
- c.state.Mountpoint = mountPoint
-
- logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint)
+ mountPoint, err := c.runtime.storageService.StartContainer(c.ID())
+ if err != nil {
+ return errors.Wrapf(err, "error mounting storage for container %s", c.ID())
+ }
+ c.state.Mounted = true
+ c.state.Mountpoint = mountPoint
- defer func() {
- if err != nil {
- if err2 := c.runtime.storageService.StopContainer(c.ID()); err2 != nil {
- logrus.Errorf("Error unmounting storage for container %s: %v", c.ID(), err2)
- }
+ logrus.Debugf("Created root filesystem for container %s at %s", c.ID(), c.state.Mountpoint)
- c.state.Mounted = false
- c.state.Mountpoint = ""
+ defer func() {
+ if err != nil {
+ if err2 := c.runtime.storageService.StopContainer(c.ID()); err2 != nil {
+ logrus.Errorf("Error unmounting storage for container %s: %v", c.ID(), err2)
}
- }()
- }
+
+ c.state.Mounted = false
+ c.state.Mountpoint = ""
+ }
+ }()
// Make the OCI runtime spec we will use
c.runningSpec = new(spec.Spec)
@@ -342,9 +311,12 @@ func (c *Container) Create() (err error) {
logrus.Debugf("Created container %s in runc", c.ID())
- // TODO should flush this state to disk here
c.state.State = ContainerStateCreated
+ if err := c.runtime.state.SaveContainer(c); err != nil {
+ return errors.Wrapf(err, "error saving container %s state", c.ID())
+ }
+
return nil
}
@@ -353,6 +325,10 @@ func (c *Container) Start() error {
c.lock.Lock()
defer c.lock.Unlock()
+ if err := c.runtime.state.UpdateContainer(c); err != nil {
+ return errors.Wrapf(err, "error updating container %s state", c.ID())
+ }
+
if !c.valid {
return ErrCtrRemoved
}
@@ -368,10 +344,13 @@ func (c *Container) Start() error {
logrus.Debugf("Started container %s", c.ID())
- // TODO should flush state to disk here
c.state.StartedTime = time.Now()
c.state.State = ContainerStateRunning
+ if err := c.runtime.state.SaveContainer(c); err != nil {
+ return errors.Wrapf(err, "error saving container %s state", c.ID())
+ }
+
return nil
}
@@ -394,6 +373,25 @@ func (c *Container) Exec(cmd []string, tty bool, stdin bool) (string, error) {
// Attach attaches to a container
// Returns fully qualified URL of streaming server for the container
func (c *Container) Attach(noStdin bool, keys string, attached chan<- bool) error {
+ if err := c.runtime.state.UpdateContainer(c); err != nil {
+ return errors.Wrapf(err, "error updating container %s state", c.ID())
+ }
+
+ if !c.valid {
+ return errors.Wrapf(ErrCtrRemoved, "container %s is not valid", c.ID())
+ }
+
+ if c.state.State == ContainerStateRunning || c.state.State == ContainerStatePaused {
+ return errors.Wrapf(ErrCtrStateInvalid, "cannot remove storage for container %s as it is running or paused", c.ID())
+ }
+
+ // TODO is it valid to attach to a frozen container?
+ if c.state.State == ContainerStateUnknown ||
+ c.state.State == ContainerStateConfigured ||
+ c.state.State == ContainerStatePaused {
+ return errors.Wrapf(ErrCtrStateInvalid, "can only attach to created, running, or stopped containers")
+ }
+
// Check the validity of the provided keys first
var err error
detachKeys := []byte{}
@@ -403,25 +401,12 @@ func (c *Container) Attach(noStdin bool, keys string, attached chan<- bool) erro
return errors.Wrapf(err, "invalid detach keys")
}
}
- cStatus := c.state.State
- if !(cStatus == ContainerStateRunning || cStatus == ContainerStateCreated) {
- return errors.Errorf("%s is not created or running", c.Name())
- }
resize := make(chan remotecommand.TerminalSize)
defer close(resize)
- err = c.attachContainerSocket(resize, noStdin, detachKeys, attached)
+ err = c.attachContainerSocket(resize, noStdin, detachKeys, attached)
return err
-
- // TODO
- // Re-enable this when mheon is done wth it
- //if err != nil {
- // return err
- //}
- //c.ContainerStateToDisk(c)
-
- //return err
}
// Mount mounts a container's filesystem on the host
@@ -448,10 +433,6 @@ func (c *Container) Export(path string) error {
// Commit commits the changes between a container and its image, creating a new
// image
-// If the container was not created from an image (for example,
-// WithRootFSFromPath will create a container from a directory on the system),
-// a new base image will be created from the contents of the container's
-// filesystem
func (c *Container) Commit() (*storage.Image, error) {
return nil, ErrNotImplemented
}
diff --git a/libpod/errors.go b/libpod/errors.go
index 245445005..180ca51db 100644
--- a/libpod/errors.go
+++ b/libpod/errors.go
@@ -56,6 +56,10 @@ var (
// further operations can be performed on it
ErrPodRemoved = errors.New("pod has already been removed")
+ // ErrDBClosed indicates that the connection to the state database has
+ // already been closed
+ ErrDBClosed = errors.New("database connection already closed")
+
// ErrNotImplemented indicates that the requested functionality is not
// yet present
ErrNotImplemented = errors.New("not yet implemented")
diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go
index dd193f57b..5d03e62e6 100644
--- a/libpod/in_memory_state.go
+++ b/libpod/in_memory_state.go
@@ -32,6 +32,12 @@ func NewInMemoryState() (State, error) {
return state, nil
}
+// Close the state before shutdown
+// This is a no-op as we have no backing disk
+func (s *InMemoryState) Close() error {
+ return nil
+}
+
// Container retrieves a container from its full ID
func (s *InMemoryState) Container(id string) (*Container, error) {
if id == "" {
@@ -147,6 +153,21 @@ func (s *InMemoryState) RemoveContainer(ctr *Container) error {
return nil
}
+// UpdateContainer updates a container's state
+// As all state is in-memory, no update will be required
+// As such this is a no-op
+func (s *InMemoryState) UpdateContainer(ctr *Container) error {
+ return nil
+}
+
+// SaveContainer saves a container's state
+// As all state is in-memory, any changes are always reflected as soon as they
+// are made
+// As such this is a no-op
+func (s *InMemoryState) SaveContainer(ctr *Container) error {
+ return nil
+}
+
// AllContainers retrieves all containers from the state
func (s *InMemoryState) AllContainers() ([]*Container, error) {
ctrs := make([]*Container, 0, len(s.containers))
diff --git a/libpod/options.go b/libpod/options.go
index 10cb605c2..4c21a70c9 100644
--- a/libpod/options.go
+++ b/libpod/options.go
@@ -94,6 +94,20 @@ func WithSignaturePolicy(path string) RuntimeOption {
}
}
+// WithInMemoryState specifies that the runtime will be backed by an in-memory
+// state only, and state will not persist after the runtime is shut down
+func WithInMemoryState() RuntimeOption {
+ return func(rt *Runtime) error {
+ if rt.valid {
+ return ErrRuntimeFinalized
+ }
+
+ rt.config.InMemoryState = true
+
+ return nil
+ }
+}
+
// WithOCIRuntime specifies an OCI runtime to use for running containers
func WithOCIRuntime(runtimePath string) RuntimeOption {
return func(rt *Runtime) error {
@@ -236,25 +250,6 @@ func WithNoPivotRoot(noPivot bool) RuntimeOption {
// Container Creation Options
-// WithRootFSFromPath uses the given path as a container's root filesystem
-// No further setup is performed on this path
-func WithRootFSFromPath(path string) CtrCreateOption {
- return func(ctr *Container) error {
- if ctr.valid {
- return ErrCtrFinalized
- }
-
- if ctr.config.RootfsDir != "" || ctr.config.RootfsImageID != "" || ctr.config.RootfsImageName != "" {
- return errors.Wrapf(ErrInvalidArg, "container already configured with root filesystem")
- }
-
- ctr.config.RootfsDir = path
- ctr.config.RootfsFromImage = false
-
- return nil
- }
-}
-
// WithSELinuxMountLabel sets the mount label for SELinux
func WithSELinuxMountLabel(mountLabel string) CtrCreateOption {
return func(ctr *Container) error {
@@ -277,14 +272,13 @@ func WithRootFSFromImage(imageID string, imageName string, useImageConfig bool)
return ErrCtrFinalized
}
- if ctr.config.RootfsDir != "" || ctr.config.RootfsImageID != "" || ctr.config.RootfsImageName != "" {
+ if ctr.config.RootfsImageID != "" || ctr.config.RootfsImageName != "" {
return errors.Wrapf(ErrInvalidArg, "container already configured with root filesystem")
}
ctr.config.RootfsImageID = imageID
ctr.config.RootfsImageName = imageName
ctr.config.UseImageConfig = useImageConfig
- ctr.config.RootfsFromImage = true
return nil
}
diff --git a/libpod/runtime.go b/libpod/runtime.go
index 80202c567..39b3677a2 100644
--- a/libpod/runtime.go
+++ b/libpod/runtime.go
@@ -2,6 +2,7 @@ package libpod
import (
"os"
+ "path/filepath"
"sync"
is "github.com/containers/image/storage"
@@ -35,6 +36,7 @@ type RuntimeConfig struct {
InsecureRegistries []string
Registries []string
SignaturePolicyPath string
+ InMemoryState bool
RuntimePath string
ConmonPath string
ConmonEnvVars []string
@@ -52,6 +54,7 @@ var (
// Leave this empty so containers/storage will use its defaults
StorageConfig: storage.StoreOptions{},
ImageDefaultTransport: "docker://",
+ InMemoryState: false,
RuntimePath: "/usr/bin/runc",
ConmonPath: "/usr/local/libexec/crio/conmon",
ConmonEnvVars: []string{
@@ -94,7 +97,7 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
if err != nil {
// Don't forcibly shut down
// We could be opening a store in use by another libpod
- _, err2 := runtime.store.Shutdown(false)
+ _, err2 := store.Shutdown(false)
if err2 != nil {
logrus.Errorf("Error removing store for partially-created runtime: %s", err2)
}
@@ -114,13 +117,6 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
SignaturePolicyPath: runtime.config.SignaturePolicyPath,
}
- // Set up the state
- state, err := NewInMemoryState()
- if err != nil {
- return nil, err
- }
- runtime.state = state
-
// Make an OCI runtime to perform container operations
ociRuntime, err := newOCIRuntime("runc", runtime.config.RuntimePath,
runtime.config.ConmonPath, runtime.config.ConmonEnvVars,
@@ -149,6 +145,34 @@ func NewRuntime(options ...RuntimeOption) (runtime *Runtime, err error) {
}
}
+ // Set up the state
+ if runtime.config.InMemoryState {
+ state, err := NewInMemoryState()
+ if err != nil {
+ return nil, err
+ }
+ runtime.state = state
+ } else {
+ dbPath := filepath.Join(runtime.config.StaticDir, "state.sql")
+ lockPath := filepath.Join(runtime.config.TmpDir, "state.lck")
+ specsDir := filepath.Join(runtime.config.StaticDir, "ocispec")
+
+ // Make a directory to hold JSON versions of container OCI specs
+ if err := os.MkdirAll(specsDir, 0755); err != nil {
+ // The directory is allowed to exist
+ if !os.IsExist(err) {
+ return nil, errors.Wrapf(err, "error creating runtime OCI specs directory %s",
+ specsDir)
+ }
+ }
+
+ state, err := NewSQLState(dbPath, lockPath, specsDir, runtime)
+ if err != nil {
+ return nil, err
+ }
+ runtime.state = state
+ }
+
// Mark the runtime as valid - ready to be used, cannot be modified
// further
runtime.valid = true
@@ -188,5 +212,10 @@ func (r *Runtime) Shutdown(force bool) error {
r.valid = false
_, err := r.store.Shutdown(force)
- return err
+ if err != nil {
+ return err
+ }
+
+ // TODO: Should always call this even if store.Shutdown failed
+ return r.state.Close()
}
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index b23c65287..aa8ff7d88 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -19,14 +19,14 @@ type CtrCreateOption func(*Container) error
type ContainerFilter func(*Container) bool
// NewContainer creates a new container from a given OCI config
-func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (ctr *Container, err error) {
+func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (c *Container, err error) {
r.lock.Lock()
defer r.lock.Unlock()
if !r.valid {
return nil, ErrRuntimeStopped
}
- ctr, err = newContainer(spec)
+ ctr, err := newContainer(spec)
if err != nil {
return nil, err
}
@@ -60,7 +60,7 @@ func (r *Runtime) NewContainer(spec *spec.Spec, options ...CtrCreateOption) (ctr
}
}
defer func() {
- if err != nil {
+ if err != nil && ctr.pod != nil {
if err2 := ctr.pod.removeContainer(ctr); err2 != nil {
logrus.Errorf("Error removing partially-created container from pod %s: %s", ctr.pod.ID(), err2)
}
@@ -95,17 +95,21 @@ func (r *Runtime) RemoveContainer(c *Container, force bool) error {
return ErrCtrRemoved
}
- // TODO check container status and unmount storage
- // TODO check that no other containers depend on this container's
- // namespaces
- status, err := c.State()
- if err != nil {
+ // Update the container to get current state
+ if err := r.state.UpdateContainer(c); err != nil {
return err
}
- // A container cannot be removed if it is running
- if status == ContainerStateRunning {
- return errors.Wrapf(ErrCtrStateInvalid, "cannot remove container %s as it is running", c.ID())
+ // Check that the container's in a good state to be removed
+ if !(c.state.State == ContainerStateConfigured ||
+ c.state.State == ContainerStateCreated ||
+ c.state.State == ContainerStateStopped) {
+ return errors.Wrapf(ErrCtrStateInvalid, "cannot remove container %s as it is running or paused", c.ID())
+ }
+
+ // Stop the container's storage
+ if err := c.teardownStorage(); err != nil {
+ return err
}
if err := r.state.RemoveContainer(c); err != nil {
diff --git a/libpod/sql_state.go b/libpod/sql_state.go
new file mode 100644
index 000000000..034fc03e1
--- /dev/null
+++ b/libpod/sql_state.go
@@ -0,0 +1,567 @@
+package libpod
+
+import (
+ "database/sql"
+ "encoding/json"
+ "io/ioutil"
+ "os"
+
+ "github.com/containers/storage"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+
+ // Use SQLite backend for sql package
+ _ "github.com/mattn/go-sqlite3"
+)
+
+// SQLState is a state implementation backed by a persistent SQLite3 database
+type SQLState struct {
+ db *sql.DB
+ specsDir string
+ runtime *Runtime
+ lock storage.Locker
+ valid bool
+}
+
+// NewSQLState initializes a SQL-backed state, created the database if necessary
+func NewSQLState(dbPath, lockPath, specsDir string, runtime *Runtime) (State, error) {
+ state := new(SQLState)
+
+ state.runtime = runtime
+
+ // Make our lock file
+ lock, err := storage.GetLockfile(lockPath)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error creating lockfile for state")
+ }
+ state.lock = lock
+
+ // Make the directory that will hold JSON copies of container runtime specs
+ if err := os.MkdirAll(specsDir, 0750); err != nil {
+ // The directory is allowed to exist
+ if !os.IsExist(err) {
+ return nil, errors.Wrapf(err, "error creating OCI specs dir %s", specsDir)
+ }
+ }
+ state.specsDir = specsDir
+
+ // Acquire the lock while we open the database and perform initial setup
+ state.lock.Lock()
+ defer state.lock.Unlock()
+
+ // TODO add a separate temporary database for per-boot container
+ // state
+
+ // Open the database
+ // Use loc=auto to get accurate locales for timestamps
+ db, err := sql.Open("sqlite3", dbPath+"?_loc=auto")
+ if err != nil {
+ return nil, errors.Wrapf(err, "error opening database")
+ }
+
+ // Ensure connectivity
+ if err := db.Ping(); err != nil {
+ return nil, errors.Wrapf(err, "cannot establish connection to database")
+ }
+
+ // Prepare database
+ if err := prepareDB(db); err != nil {
+ return nil, err
+ }
+
+ state.db = db
+
+ state.valid = true
+
+ return state, nil
+}
+
+// Close the state's database connection
+func (s *SQLState) Close() error {
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ s.valid = false
+
+ if err := s.db.Close(); err != nil {
+ return errors.Wrapf(err, "error closing database")
+ }
+
+ return nil
+}
+
+// Container retrieves a container from its full ID
+func (s *SQLState) Container(id string) (*Container, error) {
+ const query = `SELECT containers.*,
+ containerState.State,
+ containerState.ConfigPath,
+ containerState.RunDir,
+ containerState.MountPoint,
+ containerState.StartedTime,
+ containerState.FinishedTime,
+ containerState.ExitCode
+ FROM containers
+ INNER JOIN
+ containerState ON containers.Id = containerState.Id
+ WHERE containers.Id=?;`
+
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ row := s.db.QueryRow(query, id)
+
+ ctr, err := ctrFromScannable(row, s.runtime, s.specsDir)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving container %s from database", id)
+ }
+
+ return ctr, nil
+}
+
+// LookupContainer retrieves a container by full or unique partial ID or name
+func (s *SQLState) LookupContainer(idOrName string) (*Container, error) {
+ const query = `SELECT containers.*,
+ containerState.State,
+ containerState.ConfigPath,
+ containerState.RunDir,
+ containerState.MountPoint,
+ containerState.StartedTime,
+ containerState.FinishedTime,
+ containerState.ExitCode
+ FROM containers
+ INNER JOIN
+ containerState ON containers.Id = containerState.Id
+ WHERE (containers.Id LIKE ?) OR containers.Name=?;`
+
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ rows, err := s.db.Query(query, idOrName+"%", idOrName)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving container %s row from database", idOrName)
+ }
+ defer rows.Close()
+
+ foundResult := false
+ var ctr *Container
+ for rows.Next() {
+ if foundResult {
+ return nil, errors.Wrapf(ErrCtrExists, "more than one result for ID or name %s", idOrName)
+ }
+
+ var err error
+ ctr, err = ctrFromScannable(rows, s.runtime, s.specsDir)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving container %s from database", idOrName)
+ }
+ foundResult = true
+ }
+ if err := rows.Err(); err != nil {
+ return nil, errors.Wrapf(err, "error retrieving rows for container ID or name %s", idOrName)
+ }
+
+ if !foundResult {
+ return nil, errors.Wrapf(ErrNoSuchCtr, "no container with ID or name %s found", idOrName)
+ }
+
+ return ctr, nil
+}
+
+// HasContainer checks if the given container is present in the state
+// It accepts a full ID
+func (s *SQLState) HasContainer(id string) (bool, error) {
+ const query = "SELECT 1 FROM containers WHERE Id=?;"
+
+ if !s.valid {
+ return false, ErrDBClosed
+ }
+
+ row := s.db.QueryRow(query, id)
+
+ var check int
+ err := row.Scan(&check)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return false, nil
+ }
+
+ return false, errors.Wrapf(err, "error questing database for existence of container %s", id)
+ } else if check != 1 {
+ return false, errors.Wrapf(ErrInternal, "check digit for HasContainer query incorrect")
+ }
+
+ return true, nil
+}
+
+// AddContainer adds the given container to the state
+// If the container belongs to a pod, that pod must already be present in the
+// state, and the container will be added to the pod
+func (s *SQLState) AddContainer(ctr *Container) (err error) {
+ const (
+ addCtr = `INSERT INTO containers VALUES (
+ ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
+ );`
+ addCtrState = `INSERT INTO containerState VALUES (
+ ?, ?, ?, ?, ?, ?, ?, ?
+ );`
+ )
+
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !ctr.valid {
+ return ErrCtrRemoved
+ }
+
+ labelsJSON, err := json.Marshal(ctr.config.Labels)
+ if err != nil {
+ return errors.Wrapf(err, "error marshaling container %s labels to JSON", ctr.ID())
+ }
+
+ // Save the container's runtime spec to disk
+ specJSON, err := json.Marshal(ctr.config.Spec)
+ if err != nil {
+ return errors.Wrapf(err, "error marshalling container %s spec to JSON", ctr.ID())
+ }
+ specPath := getSpecPath(s.specsDir, ctr.ID())
+ if err := ioutil.WriteFile(specPath, specJSON, 0750); err != nil {
+ return errors.Wrapf(err, "error saving container %s spec JSON to disk", ctr.ID())
+ }
+ defer func() {
+ if err != nil {
+ if err2 := os.Remove(specPath); err2 != nil {
+ logrus.Errorf("Error removing container %s JSON spec from state: %v", ctr.ID(), err2)
+ }
+ }
+ }()
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ tx, err := s.db.Begin()
+ if err != nil {
+ return errors.Wrapf(err, "error beginning database transaction")
+ }
+ defer func() {
+ if err != nil {
+ if err2 := tx.Rollback(); err2 != nil {
+ logrus.Errorf("Error rolling back transaction to add container %s: %v", ctr.ID(), err2)
+ }
+ }
+ }()
+
+ // Add static container information
+ _, err = tx.Exec(addCtr,
+ ctr.ID(),
+ ctr.Name(),
+ ctr.config.MountLabel,
+ ctr.config.StaticDir,
+ boolToSQL(ctr.config.Stdin),
+ string(labelsJSON),
+ ctr.config.StopSignal,
+ timeToSQL(ctr.config.CreatedTime),
+ ctr.config.RootfsImageID,
+ ctr.config.RootfsImageName,
+ boolToSQL(ctr.config.UseImageConfig))
+ if err != nil {
+ return errors.Wrapf(err, "error adding static information for container %s to database", ctr.ID())
+ }
+
+ // Add container state to the database
+ _, err = tx.Exec(addCtrState,
+ ctr.ID(),
+ ctr.state.State,
+ ctr.state.ConfigPath,
+ ctr.state.RunDir,
+ ctr.state.Mountpoint,
+ timeToSQL(ctr.state.StartedTime),
+ timeToSQL(ctr.state.FinishedTime),
+ ctr.state.ExitCode)
+ if err != nil {
+ return errors.Wrapf(err, "error adding container %s state to database", ctr.ID())
+ }
+
+ if err := tx.Commit(); err != nil {
+ return errors.Wrapf(err, "error committing transaction to add container %s", ctr.ID())
+ }
+
+ return nil
+}
+
+// UpdateContainer updates a container's state from the database
+func (s *SQLState) UpdateContainer(ctr *Container) error {
+ const query = `SELECT State,
+ ConfigPath,
+ RunDir,
+ Mountpoint,
+ StartedTime,
+ FinishedTime,
+ ExitCode
+ FROM containerState WHERE ID=?;`
+
+ var (
+ state int
+ configPath string
+ runDir string
+ mountpoint string
+ startedTimeString string
+ finishedTimeString string
+ exitCode int32
+ )
+
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !ctr.valid {
+ return ErrCtrRemoved
+ }
+
+ row := s.db.QueryRow(query, ctr.ID())
+ err := row.Scan(
+ &state,
+ &configPath,
+ &runDir,
+ &mountpoint,
+ &startedTimeString,
+ &finishedTimeString,
+ &exitCode)
+ if err != nil {
+ // The container may not exist in the database
+ if err == sql.ErrNoRows {
+ // Assume that the container was removed by another process
+ // As such make it invalid
+ ctr.valid = false
+
+ return errors.Wrapf(ErrNoSuchCtr, "no container with ID %s found in database", ctr.ID())
+ }
+
+ return errors.Wrapf(err, "error parsing database state for container %s", ctr.ID())
+ }
+
+ newState := new(containerRuntimeInfo)
+ newState.State = ContainerState(state)
+ newState.ConfigPath = configPath
+ newState.RunDir = runDir
+ newState.Mountpoint = mountpoint
+ newState.ExitCode = exitCode
+
+ if newState.Mountpoint != "" {
+ newState.Mounted = true
+ }
+
+ startedTime, err := timeFromSQL(startedTimeString)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing container %s started time", ctr.ID())
+ }
+ newState.StartedTime = startedTime
+
+ finishedTime, err := timeFromSQL(finishedTimeString)
+ if err != nil {
+ return errors.Wrapf(err, "error parsing container %s finished time", ctr.ID())
+ }
+ newState.FinishedTime = finishedTime
+
+ // New state compiled successfully, swap it into the current state
+ ctr.state = newState
+
+ return nil
+}
+
+// SaveContainer updates a container's state in the database
+func (s *SQLState) SaveContainer(ctr *Container) error {
+ const update = `UPDATE containerState SET
+ State=?,
+ ConfigPath=?,
+ RunDir=?,
+ Mountpoint=?,
+ StartedTime=?,
+ FinishedTime=?,
+ ExitCode=?
+ WHERE Id=?;`
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ if !ctr.valid {
+ return ErrCtrRemoved
+ }
+
+ tx, err := s.db.Begin()
+ if err != nil {
+ return errors.Wrapf(err, "error beginning database transaction")
+ }
+ defer func() {
+ if err != nil {
+ if err2 := tx.Rollback(); err2 != nil {
+ logrus.Errorf("Error rolling back transaction to add container %s: %v", ctr.ID(), err2)
+ }
+ }
+ }()
+
+ // Add container state to the database
+ _, err = tx.Exec(update,
+ ctr.state.State,
+ ctr.state.ConfigPath,
+ ctr.state.RunDir,
+ ctr.state.Mountpoint,
+ timeToSQL(ctr.state.StartedTime),
+ timeToSQL(ctr.state.FinishedTime),
+ ctr.state.ExitCode,
+ ctr.ID())
+ if err != nil {
+ return errors.Wrapf(err, "error updating container %s state in database", ctr.ID())
+ }
+
+ if err := tx.Commit(); err != nil {
+ return errors.Wrapf(err, "error committing transaction to update container %s", ctr.ID())
+ }
+
+ return nil
+}
+
+// RemoveContainer removes the container from the state
+func (s *SQLState) RemoveContainer(ctr *Container) error {
+ const (
+ removeCtr = "DELETE FROM containers WHERE Id=?;"
+ removeState = "DELETE FROM containerState WHERE ID=?;"
+ )
+
+ s.lock.Lock()
+ defer s.lock.Unlock()
+
+ if !s.valid {
+ return ErrDBClosed
+ }
+
+ committed := false
+
+ tx, err := s.db.Begin()
+ if err != nil {
+ return errors.Wrapf(err, "error beginning database transaction")
+ }
+ defer func() {
+ if err != nil && !committed {
+ if err2 := tx.Rollback(); err2 != nil {
+ logrus.Errorf("Error rolling back transaction to add container %s: %v", ctr.ID(), err2)
+ }
+ }
+ }()
+
+ // Check rows acted on for the first transaction, verify we actually removed something
+ result, err := tx.Exec(removeCtr, ctr.ID())
+ if err != nil {
+ return errors.Wrapf(err, "error removing container %s from containers table", ctr.ID())
+ }
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return errors.Wrapf(err, "error retrieving number of rows in transaction removing container %s", ctr.ID())
+ } else if rows == 0 {
+ return ErrNoSuchCtr
+ }
+
+ if _, err := tx.Exec(removeState, ctr.ID()); err != nil {
+ return errors.Wrapf(err, "error removing container %s from state table", ctr.ID())
+ }
+
+ if err := tx.Commit(); err != nil {
+ return errors.Wrapf(err, "error committing transaction to remove container %s", ctr.ID())
+ }
+
+ committed = true
+
+ // Remove the container's JSON from disk
+ jsonPath := getSpecPath(s.specsDir, ctr.ID())
+ if err := os.Remove(jsonPath); err != nil {
+ return errors.Wrapf(err, "error removing JSON spec from state for container %s", ctr.ID())
+ }
+
+ ctr.valid = false
+
+ return nil
+}
+
+// AllContainers retrieves all the containers presently in the state
+func (s *SQLState) AllContainers() ([]*Container, error) {
+ // TODO maybe do an ORDER BY here?
+ const query = `SELECT containers.*,
+ containerState.State,
+ containerState.ConfigPath,
+ containerState.RunDir,
+ containerState.MountPoint,
+ containerState.StartedTime,
+ containerState.FinishedTime,
+ containerState.ExitCode
+ FROM containers
+ INNER JOIN
+ containerState ON containers.Id = containerState.Id;`
+
+ if !s.valid {
+ return nil, ErrDBClosed
+ }
+
+ rows, err := s.db.Query(query)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error retrieving containers from database")
+ }
+ defer rows.Close()
+
+ containers := []*Container{}
+
+ for rows.Next() {
+ ctr, err := ctrFromScannable(rows, s.runtime, s.specsDir)
+ if err != nil {
+ return nil, err
+ }
+
+ containers = append(containers, ctr)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, errors.Wrapf(err, "error retrieving container rows")
+ }
+
+ return containers, nil
+}
+
+// Pod retrieves a pod by its full ID
+func (s *SQLState) Pod(id string) (*Pod, error) {
+ return nil, ErrNotImplemented
+}
+
+// LookupPod retrieves a pot by full or unique partial ID or name
+func (s *SQLState) LookupPod(idOrName string) (*Pod, error) {
+ return nil, ErrNotImplemented
+}
+
+// HasPod checks if a pod exists given its full ID
+func (s *SQLState) HasPod(id string) (bool, error) {
+ return false, ErrNotImplemented
+}
+
+// AddPod adds a pod to the state
+// Only empty pods can be added to the state
+func (s *SQLState) AddPod(pod *Pod) error {
+ return ErrNotImplemented
+}
+
+// RemovePod removes a pod from the state
+// Only empty pods can be removed
+func (s *SQLState) RemovePod(pod *Pod) error {
+ return ErrNotImplemented
+}
+
+// AllPods retrieves all pods presently in the state
+func (s *SQLState) AllPods() ([]*Pod, error) {
+ return nil, ErrNotImplemented
+}
diff --git a/libpod/sql_state_internal.go b/libpod/sql_state_internal.go
new file mode 100644
index 000000000..698b0433c
--- /dev/null
+++ b/libpod/sql_state_internal.go
@@ -0,0 +1,246 @@
+package libpod
+
+import (
+ "database/sql"
+ "encoding/json"
+ "io/ioutil"
+ "path/filepath"
+ "time"
+
+ spec "github.com/opencontainers/runtime-spec/specs-go"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+
+ // Use SQLite backend for sql package
+ _ "github.com/mattn/go-sqlite3"
+)
+
+// Performs database setup including by not limited to initializing tables in
+// the database
+func prepareDB(db *sql.DB) (err error) {
+ // TODO create pod tables
+ // TODO add Pod ID to CreateStaticContainer as a FOREIGN KEY referencing podStatic(Id)
+ // TODO add ctr shared namespaces information - A separate table, probably? So we can FOREIGN KEY the ID
+ // TODO schema migration might be necessary and should be handled here
+ // TODO add a table for the runtime, and refuse to load the database if the runtime configuration
+ // does not match the one in the database
+
+ // Enable foreign keys in SQLite
+ if _, err := db.Exec("PRAGMA foreign_keys = ON;"); err != nil {
+ return errors.Wrapf(err, "error enabling foreign key support in database")
+ }
+
+ // Create a table for unchanging container data
+ const createCtr = `
+ CREATE TABLE IF NOT EXISTS containers(
+ Id TEXT NOT NULL PRIMARY KEY,
+ Name TEXT NOT NULL UNIQUE,
+ MountLabel TEXT NOT NULL,
+ StaticDir TEXT NOT NULL,
+ Stdin INTEGER NOT NULL,
+ LabelsJSON TEXT NOT NULL,
+ StopSignal INTEGER NOT NULL,
+ CreatedTime TEXT NOT NULL,
+ RootfsImageID TEXT NOT NULL,
+ RootfsImageName TEXT NOT NULL,
+ UseImageConfig INTEGER NOT NULL,
+ CHECK (Stdin IN (0, 1)),
+ CHECK (UseImageConfig IN (0, 1)),
+ CHECK (StopSignal>=0)
+ );
+ `
+
+ // Create a table for changing container state
+ const createCtrState = `
+ CREATE TABLE IF NOT EXISTS containerState(
+ Id TEXT NOT NULL PRIMARY KEY,
+ State INTEGER NOT NULL,
+ ConfigPath TEXT NOT NULL,
+ RunDir TEXT NOT NULL,
+ Mountpoint TEXT NOT NULL,
+ StartedTime TEXT NUT NULL,
+ FinishedTime TEXT NOT NULL,
+ ExitCode INTEGER NOT NULL,
+ CHECK (State>0),
+ FOREIGN KEY (Id) REFERENCES containers(Id) DEFERRABLE INITIALLY DEFERRED
+ );
+ `
+
+ // Create the tables
+ tx, err := db.Begin()
+ if err != nil {
+ return errors.Wrapf(err, "error beginning database transaction")
+ }
+ defer func() {
+ if err != nil {
+ if err2 := tx.Rollback(); err2 != nil {
+ logrus.Errorf("Error rolling back transaction to create tables: %v", err2)
+ }
+ }
+
+ }()
+
+ if _, err := tx.Exec(createCtr); err != nil {
+ return errors.Wrapf(err, "error creating containers table in database")
+ }
+ if _, err := tx.Exec(createCtrState); err != nil {
+ return errors.Wrapf(err, "error creating container state table in database")
+ }
+
+ if err := tx.Commit(); err != nil {
+ return errors.Wrapf(err, "error committing table creation transaction in database")
+ }
+
+ return nil
+}
+
+// Get filename for OCI spec on disk
+func getSpecPath(specsDir, id string) string {
+ return filepath.Join(specsDir, id)
+}
+
+// Convert a bool into SQL-readable format
+func boolToSQL(b bool) int {
+ if b {
+ return 1
+ }
+
+ return 0
+}
+
+// Convert a bool from SQL-readable format
+func boolFromSQL(i int) bool {
+ return i != 0
+}
+
+// Convert a time.Time into SQL-readable format
+func timeToSQL(t time.Time) string {
+ return t.Format(time.RFC3339Nano)
+}
+
+// Convert a SQL-readable time back to a time.Time
+func timeFromSQL(s string) (time.Time, error) {
+ return time.Parse(time.RFC3339Nano, s)
+}
+
+// Interface to abstract sql.Rows and sql.Row so they can both be used
+type scannable interface {
+ Scan(dest ...interface{}) error
+}
+
+// Read a single container from a single row result in the database
+func ctrFromScannable(row scannable, runtime *Runtime, specsDir string) (*Container, error) {
+ var (
+ id string
+ name string
+ mountLabel string
+ staticDir string
+ stdin int
+ labelsJSON string
+ stopSignal uint
+ createdTimeString string
+ rootfsImageID string
+ rootfsImageName string
+ useImageConfig int
+ state int
+ configPath string
+ runDir string
+ mountpoint string
+ startedTimeString string
+ finishedTimeString string
+ exitCode int32
+ )
+
+ err := row.Scan(
+ &id,
+ &name,
+ &mountLabel,
+ &staticDir,
+ &stdin,
+ &labelsJSON,
+ &stopSignal,
+ &createdTimeString,
+ &rootfsImageID,
+ &rootfsImageName,
+ &useImageConfig,
+ &state,
+ &configPath,
+ &runDir,
+ &mountpoint,
+ &startedTimeString,
+ &finishedTimeString,
+ &exitCode)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, ErrNoSuchCtr
+ }
+
+ return nil, errors.Wrapf(err, "error parsing database row into container")
+ }
+
+ ctr := new(Container)
+ ctr.config = new(containerConfig)
+ ctr.state = new(containerRuntimeInfo)
+
+ ctr.config.ID = id
+ ctr.config.Name = name
+ ctr.config.RootfsImageID = rootfsImageID
+ ctr.config.RootfsImageName = rootfsImageName
+ ctr.config.UseImageConfig = boolFromSQL(useImageConfig)
+ ctr.config.MountLabel = mountLabel
+ ctr.config.StaticDir = staticDir
+ ctr.config.Stdin = boolFromSQL(stdin)
+ ctr.config.StopSignal = stopSignal
+
+ ctr.state.State = ContainerState(state)
+ ctr.state.ConfigPath = configPath
+ ctr.state.RunDir = runDir
+ ctr.state.Mountpoint = mountpoint
+ ctr.state.ExitCode = exitCode
+
+ // TODO should we store this in the database separately instead?
+ if ctr.state.Mountpoint != "" {
+ ctr.state.Mounted = true
+ }
+
+ labels := make(map[string]string)
+ if err := json.Unmarshal([]byte(labelsJSON), &labels); err != nil {
+ return nil, errors.Wrapf(err, "error parsing container %s labels JSON", id)
+ }
+ ctr.config.Labels = labels
+
+ createdTime, err := timeFromSQL(createdTimeString)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing container %s created time", id)
+ }
+ ctr.config.CreatedTime = createdTime
+
+ startedTime, err := timeFromSQL(startedTimeString)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing container %s started time", id)
+ }
+ ctr.state.StartedTime = startedTime
+
+ finishedTime, err := timeFromSQL(finishedTimeString)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error parsing container %s finished time", id)
+ }
+ ctr.state.FinishedTime = finishedTime
+
+ ctr.valid = true
+ ctr.runtime = runtime
+
+ // Retrieve the spec from disk
+ ociSpec := new(spec.Spec)
+ specPath := getSpecPath(specsDir, id)
+ fileContents, err := ioutil.ReadFile(specPath)
+ if err != nil {
+ return nil, errors.Wrapf(err, "error reading container %s OCI spec", id)
+ }
+ if err := json.Unmarshal(fileContents, ociSpec); err != nil {
+ return nil, errors.Wrapf(err, "error parsing container %s OCI spec", id)
+ }
+ ctr.config.Spec = ociSpec
+
+ return ctr, nil
+}
diff --git a/libpod/state.go b/libpod/state.go
index 1c21911bb..4093f14f1 100644
--- a/libpod/state.go
+++ b/libpod/state.go
@@ -2,6 +2,10 @@ package libpod
// State is a storage backend for libpod's current state
type State interface {
+ // Close performs any pre-exit cleanup (e.g. closing database
+ // connections) that may be required
+ Close() error
+
// Accepts full ID of container
Container(id string) (*Container, error)
// Accepts full or partial IDs (as long as they are unique) and names
@@ -17,6 +21,10 @@ type State interface {
// The container will only be removed from the state, not from the pod
// which the container belongs to
RemoveContainer(ctr *Container) error
+ // UpdateContainer updates a container's state from the backing store
+ UpdateContainer(ctr *Container) error
+ // SaveContainer saves a container's current state to the backing store
+ SaveContainer(ctr *Container) error
// Retrieves all containers presently in state
AllContainers() ([]*Container, error)