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 } // 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() ctr.lock.Lock() defer ctr.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 }