From c6fe4430b76ceeecd6b0b609cca8e705921db0c4 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Tue, 7 Nov 2017 13:46:30 -0500 Subject: Compile-tested implementation of SQL-backed state Signed-off-by: Matthew Heon --- libpod/in_memory_state.go | 6 ++++++ 1 file changed, 6 insertions(+) (limited to 'libpod/in_memory_state.go') diff --git a/libpod/in_memory_state.go b/libpod/in_memory_state.go index dd193f57b..87caac808 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 == "" { -- cgit v1.2.3-54-g00ecf From 763e3726493c122d9722e0e294634ae8d78c426b Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 9 Nov 2017 13:51:20 -0500 Subject: Wire SQL backed state into rest of libpod Signed-off-by: Matthew Heon --- libpod/container.go | 63 +++++++++++++-------- libpod/in_memory_state.go | 15 +++++ libpod/options.go | 14 +++++ libpod/runtime.go | 45 ++++++++++++--- libpod/runtime_ctr.go | 20 ++++--- libpod/sql_state.go | 140 ++++++++++++++++++++++++++++++++++++++++++++-- libpod/state.go | 4 ++ 7 files changed, 257 insertions(+), 44 deletions(-) (limited to 'libpod/in_memory_state.go') diff --git a/libpod/container.go b/libpod/container.go index 097455112..c8a09fe9e 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -150,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 } @@ -252,6 +251,10 @@ func (c *Container) Create() (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()) } @@ -308,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 } @@ -319,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 } @@ -334,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 } @@ -360,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{} @@ -369,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 @@ -414,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/in_memory_state.go b/libpod/in_memory_state.go index 87caac808..5d03e62e6 100644 --- a/libpod/in_memory_state.go +++ b/libpod/in_memory_state.go @@ -153,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 fb64c0c41..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 { diff --git a/libpod/runtime.go b/libpod/runtime.go index 80202c567..1bfb79947 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{ @@ -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..a8a5b789d 100644 --- a/libpod/runtime_ctr.go +++ b/libpod/runtime_ctr.go @@ -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 index 1218bfa79..4754bf71e 100644 --- a/libpod/sql_state.go +++ b/libpod/sql_state.go @@ -23,7 +23,7 @@ type SQLState struct { valid bool } -// NewSqlState initializes a SQL-backed state, created the database if necessary +// NewSQLState initializes a SQL-backed state, created the database if necessary func NewSQLState(dbPath, lockPath, specsDir string, runtime *Runtime) (State, error) { state := new(SQLState) @@ -295,6 +295,141 @@ func (s *SQLState) AddContainer(ctr *Container) (err error) { 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) + 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 ( @@ -305,9 +440,6 @@ func (s *SQLState) RemoveContainer(ctr *Container) error { s.lock.Lock() defer s.lock.Unlock() - ctr.lock.Lock() - defer ctr.lock.Unlock() - if !s.valid { return ErrDBClosed } diff --git a/libpod/state.go b/libpod/state.go index 41f44ac8e..4093f14f1 100644 --- a/libpod/state.go +++ b/libpod/state.go @@ -21,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) -- cgit v1.2.3-54-g00ecf