diff options
author | Daniel J Walsh <dwalsh@redhat.com> | 2017-11-20 14:45:01 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-11-20 14:45:01 -0500 |
commit | 6e0944f2f128534caaf884251d1e42fa4f1ba235 (patch) | |
tree | 4ecca5c1388b93de16b1dd91244ffb3488b0ddd1 /libpod | |
parent | 3e04604dc2619b1502b609083c3b6ecb0949f1d5 (diff) | |
parent | f2894eda689a24c069b55b1e2ad3e6136836b326 (diff) | |
download | podman-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.go | 141 | ||||
-rw-r--r-- | libpod/errors.go | 4 | ||||
-rw-r--r-- | libpod/in_memory_state.go | 21 | ||||
-rw-r--r-- | libpod/options.go | 36 | ||||
-rw-r--r-- | libpod/runtime.go | 47 | ||||
-rw-r--r-- | libpod/runtime_ctr.go | 26 | ||||
-rw-r--r-- | libpod/sql_state.go | 567 | ||||
-rw-r--r-- | libpod/sql_state_internal.go | 246 | ||||
-rw-r--r-- | libpod/state.go | 8 |
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) |