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/sql_state_internal.go | 248 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 libpod/sql_state_internal.go (limited to 'libpod/sql_state_internal.go') diff --git a/libpod/sql_state_internal.go b/libpod/sql_state_internal.go new file mode 100644 index 000000000..2597c4bb9 --- /dev/null +++ b/libpod/sql_state_internal.go @@ -0,0 +1,248 @@ +package libpod + +import ( + "database/sql" + "encoding/json" + "io/ioutil" + "path/filepath" + "time" + + _ "github.com/mattn/go-sqlite3" + spec "github.com/opencontainers/runtime-spec/specs-go" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +// 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 EXIST 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 EXIST 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 { + if i == 0 { + return false + } + + return true +} + +// 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 +} -- cgit v1.2.3-54-g00ecf From cb56716fc4bb632db84fab372fff8f5a9007ed3f Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Tue, 7 Nov 2017 14:57:29 -0500 Subject: Address review comments, fix gofmt and lint Signed-off-by: Matthew Heon --- libpod/container.go | 2 +- libpod/sql_state.go | 12 +++++++----- libpod/sql_state_internal.go | 4 +++- 3 files changed, 11 insertions(+), 7 deletions(-) (limited to 'libpod/sql_state_internal.go') diff --git a/libpod/container.go b/libpod/container.go index bef3f1088..097455112 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -92,7 +92,7 @@ type containerConfig struct { RootfsImageName string `json:"rootfsImageName,omitempty"` UseImageConfig bool `json:"useImageConfig"` // SELinux mount label for root filesystem - MountLabel string `json:"MountLabel,omitempty"` + MountLabel string `json:"MountLabel,omitempty"` // Static directory for container content that will persist across // reboot StaticDir string `json:"staticDir"` diff --git a/libpod/sql_state.go b/libpod/sql_state.go index 0564d124f..1218bfa79 100644 --- a/libpod/sql_state.go +++ b/libpod/sql_state.go @@ -7,12 +7,14 @@ import ( "os" "github.com/containers/storage" - _ "github.com/mattn/go-sqlite3" "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 +// SQLState is a state implementation backed by a persistent SQLite3 database type SQLState struct { db *sql.DB specsDir string @@ -22,7 +24,7 @@ type SQLState struct { } // NewSqlState initializes a SQL-backed state, created the database if necessary -func NewSqlState(dbPath, lockPath, specsDir string, runtime *Runtime) (State, error) { +func NewSQLState(dbPath, lockPath, specsDir string, runtime *Runtime) (State, error) { state := new(SQLState) state.runtime = runtime @@ -85,8 +87,7 @@ func (s *SQLState) Close() error { s.valid = false - err := s.db.Close() - if err != nil { + if err := s.db.Close(); err != nil { return errors.Wrapf(err, "error closing database") } @@ -427,6 +428,7 @@ 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 index 2597c4bb9..3f5f695bb 100644 --- a/libpod/sql_state_internal.go +++ b/libpod/sql_state_internal.go @@ -7,10 +7,12 @@ import ( "path/filepath" "time" - _ "github.com/mattn/go-sqlite3" 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 -- cgit v1.2.3-54-g00ecf From 657cb1b7f6be1610d6888acf18bb77027108ea9e Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 9 Nov 2017 15:03:57 -0500 Subject: Fix lingering SQL error Signed-off-by: Matthew Heon --- libpod/sql_state.go | 16 ++++++++-------- libpod/sql_state_internal.go | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) (limited to 'libpod/sql_state_internal.go') diff --git a/libpod/sql_state.go b/libpod/sql_state.go index 4754bf71e..424eb81fb 100644 --- a/libpod/sql_state.go +++ b/libpod/sql_state.go @@ -104,10 +104,10 @@ func (s *SQLState) Container(id string) (*Container, error) { containerState.StartedTime, containerState.FinishedTime, containerState.ExitCode - FROM containers - INNER JOIN - containerState ON containers.Id = containerState.Id - WHERE containers.Id=?;` + FROM containers + INNER JOIN + containerState ON containers.Id = containerState.Id + WHERE containers.Id=?;` if !s.valid { return nil, ErrDBClosed @@ -133,10 +133,10 @@ func (s *SQLState) LookupContainer(idOrName string) (*Container, error) { containerState.StartedTime, containerState.FinishedTime, containerState.ExitCode - FROM containers - INNER JOIN - containerState ON containers.Id = containerState.Id - WHERE (containers.Id LIKE ?) OR containers.Name=?;` + FROM containers + INNER JOIN + containerState ON containers.Id = containerState.Id + WHERE (containers.Id LIKE ?) OR containers.Name=?;` if !s.valid { return nil, ErrDBClosed diff --git a/libpod/sql_state_internal.go b/libpod/sql_state_internal.go index 3f5f695bb..da7c708e7 100644 --- a/libpod/sql_state_internal.go +++ b/libpod/sql_state_internal.go @@ -32,7 +32,7 @@ func prepareDB(db *sql.DB) (err error) { // Create a table for unchanging container data const createCtr = ` - CREATE TABLE IF NOT EXIST containers( + CREATE TABLE IF NOT EXISTS containers( Id TEXT NOT NULL PRIMARY KEY, Name TEXT NOT NULL UNIQUE, MountLabel TEXT NOT NULL, @@ -40,7 +40,7 @@ func prepareDB(db *sql.DB) (err error) { Stdin INTEGER NOT NULL, LabelsJSON TEXT NOT NULL, StopSignal INTEGER NOT NULL, - CreatedTime TEXT NOT NULL + CreatedTime TEXT NOT NULL, RootfsImageID TEXT NOT NULL, RootfsImageName TEXT NOT NULL, UseImageConfig INTEGER NOT NULL, @@ -52,7 +52,7 @@ func prepareDB(db *sql.DB) (err error) { // Create a table for changing container state const createCtrState = ` - CREATE TABLE IF NOT EXIST containerState( + CREATE TABLE IF NOT EXISTS containerState( Id TEXT NOT NULL PRIMARY KEY, State INTEGER NOT NULL, ConfigPath TEXT NOT NULL, -- cgit v1.2.3-54-g00ecf From b10fb66c28d64a6ea102ce527fc7688015e975ff Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Thu, 9 Nov 2017 16:17:38 -0500 Subject: StopSignal is allowed to be 0 If StopSignal is 0, it is assumed that the default signal will be used. Signed-off-by: Matthew Heon --- libpod/sql_state_internal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'libpod/sql_state_internal.go') diff --git a/libpod/sql_state_internal.go b/libpod/sql_state_internal.go index da7c708e7..5c9da03e2 100644 --- a/libpod/sql_state_internal.go +++ b/libpod/sql_state_internal.go @@ -46,7 +46,7 @@ func prepareDB(db *sql.DB) (err error) { UseImageConfig INTEGER NOT NULL, CHECK (Stdin IN (0, 1)), CHECK (UseImageConfig IN (0, 1)), - CHECK (StopSignal>0) + CHECK (StopSignal>=0) ); ` -- cgit v1.2.3-54-g00ecf From f2894eda689a24c069b55b1e2ad3e6136836b326 Mon Sep 17 00:00:00 2001 From: Matthew Heon Date: Sat, 18 Nov 2017 13:17:03 -0500 Subject: Fix lint errors Signed-off-by: Matthew Heon --- libpod/sql_state_internal.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) (limited to 'libpod/sql_state_internal.go') diff --git a/libpod/sql_state_internal.go b/libpod/sql_state_internal.go index 5c9da03e2..698b0433c 100644 --- a/libpod/sql_state_internal.go +++ b/libpod/sql_state_internal.go @@ -110,11 +110,7 @@ func boolToSQL(b bool) int { // Convert a bool from SQL-readable format func boolFromSQL(i int) bool { - if i == 0 { - return false - } - - return true + return i != 0 } // Convert a time.Time into SQL-readable format @@ -208,7 +204,7 @@ func ctrFromScannable(row scannable, runtime *Runtime, specsDir string) (*Contai } labels := make(map[string]string) - if err := json.Unmarshal([]byte(labelsJSON), labels); err != nil { + 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 -- cgit v1.2.3-54-g00ecf