summaryrefslogtreecommitdiff
path: root/libpod/sql_state.go
diff options
context:
space:
mode:
Diffstat (limited to 'libpod/sql_state.go')
-rw-r--r--libpod/sql_state.go567
1 files changed, 567 insertions, 0 deletions
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
+}