Files
podman/libpod/sqlite_state_internal.go
Valentin Rothberg 2c67ff5d40 sqlite: add container short ID to network aliases
Signed-off-by: Valentin Rothberg <vrothberg@redhat.com>
2023-03-01 16:09:51 +01:00

528 lines
18 KiB
Go

package libpod
import (
"database/sql"
"errors"
"fmt"
"os"
"strings"
"github.com/containers/common/libnetwork/types"
"github.com/containers/podman/v4/libpod/define"
"github.com/sirupsen/logrus"
// SQLite backend for database/sql
_ "github.com/mattn/go-sqlite3"
)
func (s *SQLiteState) migrateSchemaIfNecessary() (defErr error) {
row := s.conn.QueryRow("SELECT SchemaVersion FROM DBConfig;")
var schemaVer int
if err := row.Scan(&schemaVer); err != nil {
if errors.Is(err, sql.ErrNoRows) {
// Brand-new, unpopulated DB.
// Schema was just created, so it has to be the latest.
return nil
}
}
// If the schema version 0 or less, it's invalid
if schemaVer <= 0 {
return fmt.Errorf("database schema version %d is invalid: %w", schemaVer, define.ErrInternal)
}
if schemaVer != schemaVersion {
// If the DB is a later schema than we support, we have to error
if schemaVer > schemaVersion {
return fmt.Errorf("database has schema version %d while this libpod version only supports version %d: %w",
schemaVer, schemaVersion, define.ErrInternal)
}
// Perform schema migration here, one version at a time.
}
return nil
}
// Initialize all required tables for the SQLite state
func sqliteInitTables(conn *sql.DB) (defErr error) {
// Technically we could split the "CREATE TABLE IF NOT EXISTS" and ");"
// bits off each command and add them in the for loop where we actually
// run the SQL, but that seems unnecessary.
const dbConfig = `
CREATE TABLE IF NOT EXISTS DBConfig(
ID INTEGER PRIMARY KEY NOT NULL,
SchemaVersion INTEGER NOT NULL,
OS TEXT NOT NULL,
StaticDir TEXT NOT NULL,
TmpDir TEXT NOT NULL,
GraphRoot TEXT NOT NULL,
RunRoot TEXT NOT NULL,
GraphDriver TEXT NOT NULL,
VolumeDir TEXT NOT NULL,
CHECK (ID IN (1))
);`
const idNamespace = `
CREATE TABLE IF NOT EXISTS IDNamespace(
ID TEXT PRIMARY KEY NOT NULL
);`
const containerConfig = `
CREATE TABLE IF NOT EXISTS ContainerConfig(
ID TEXT PRIMARY KEY NOT NULL,
Name TEXT UNIQUE NOT NULL,
PodID TEXT,
JSON TEXT NOT NULL,
FOREIGN KEY (ID) REFERENCES IDNamespace(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (ID) REFERENCES ContainerState(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (PodID) REFERENCES PodConfig(ID)
);`
const containerState = `
CREATE TABLE IF NOT EXISTS ContainerState(
ID TEXT PRIMARY KEY NOT NULL,
State INTEGER NOT NULL,
ExitCode INTEGER,
JSON TEXT NOT NULL,
FOREIGN KEY (ID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
CHECK (ExitCode BETWEEN -1 AND 255)
);`
const containerExecSession = `
CREATE TABLE IF NOT EXISTS ContainerExecSession(
ID TEXT PRIMARY KEY NOT NULL,
ContainerID TEXT NOT NULL,
FOREIGN KEY (ContainerID) REFERENCES ContainerConfig(ID)
);`
const containerDependency = `
CREATE TABLE IF NOT EXISTS ContainerDependency(
ID TEXT NOT NULL,
DependencyID TEXT NOT NULL,
PRIMARY KEY (ID, DependencyID),
FOREIGN KEY (ID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (DependencyID) REFERENCES ContainerConfig(ID),
CHECK (ID <> DependencyID)
);`
const containerVolume = `
CREATE TABLE IF NOT EXISTS ContainerVolume(
ContainerID TEXT NOT NULL,
VolumeName TEXT NOT NULL,
PRIMARY KEY (ContainerID, VolumeName),
FOREIGN KEY (ContainerID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (VolumeName) REFERENCES VolumeConfig(Name)
);`
const containerExitCode = `
CREATE TABLE IF NOT EXISTS ContainerExitCode(
ID TEXT PRIMARY KEY NOT NULL,
Timestamp INTEGER NOT NULL,
ExitCode INTEGER NOT NULL,
CHECK (ExitCode BETWEEN -1 AND 255)
);`
const podConfig = `
CREATE TABLE IF NOT EXISTS PodConfig(
ID TEXT PRIMARY KEY NOT NULL,
Name TEXT UNIQUE NOT NULL,
JSON TEXT NOT NULL,
FOREIGN KEY (ID) REFERENCES IDNamespace(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (ID) REFERENCES PodState(ID) DEFERRABLE INITIALLY DEFERRED
);`
const podState = `
CREATE TABLE IF NOT EXISTS PodState(
ID TEXT PRIMARY KEY NOT NULL,
InfraContainerID TEXT,
JSON TEXT NOT NULL,
FOREIGN KEY (ID) REFERENCES PodConfig(ID) DEFERRABLE INITIALLY DEFERRED,
FOREIGN KEY (InfraContainerID) REFERENCES ContainerConfig(ID) DEFERRABLE INITIALLY DEFERRED
);`
const volumeConfig = `
CREATE TABLE IF NOT EXISTS VolumeConfig(
Name TEXT PRIMARY KEY NOT NULL,
StorageID TEXT,
JSON TEXT NOT NULL,
FOREIGN KEY (Name) REFERENCES VolumeState(Name) DEFERRABLE INITIALLY DEFERRED
);`
const volumeState = `
CREATE TABLE IF NOT EXISTS VolumeState(
Name TEXT PRIMARY KEY NOT NULL,
JSON TEXT NOT NULL,
FOREIGN KEY (Name) REFERENCES VolumeConfig(Name) DEFERRABLE INITIALLY DEFERRED
);`
tables := map[string]string{
"DBConfig": dbConfig,
"IDNamespace": idNamespace,
"ContainerConfig": containerConfig,
"ContainerState": containerState,
"ContainerExecSession": containerExecSession,
"ContainerDependency": containerDependency,
"ContainerVolume": containerVolume,
"ContainerExitCode": containerExitCode,
"PodConfig": podConfig,
"PodState": podState,
"VolumeConfig": volumeConfig,
"VolumeState": volumeState,
}
tx, err := conn.Begin()
if err != nil {
return fmt.Errorf("beginning transaction: %w", err)
}
defer func() {
if defErr != nil {
if err := tx.Rollback(); err != nil {
logrus.Errorf("Rolling back transaction to create tables: %v", err)
}
}
}()
for tblName, cmd := range tables {
if _, err := tx.Exec(cmd); err != nil {
return fmt.Errorf("creating table %s: %w", tblName, err)
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
// Get the config of a container with the given ID from the database
func (s *SQLiteState) getCtrConfig(id string) (*ContainerConfig, error) {
row := s.conn.QueryRow("SELECT JSON FROM ContainerConfig WHERE ID=?;", id)
var rawJSON string
if err := row.Scan(&rawJSON); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, define.ErrNoSuchCtr
}
return nil, fmt.Errorf("retrieving container %s config from DB: %w", id, err)
}
ctrCfg := new(ContainerConfig)
if err := json.Unmarshal([]byte(rawJSON), ctrCfg); err != nil {
return nil, fmt.Errorf("unmarshalling container %s config: %w", id, err)
}
return ctrCfg, nil
}
// Finalize a container that was pulled out of the database.
func finalizeCtrSqlite(ctr *Container) error {
// Get the lock
lock, err := ctr.runtime.lockManager.RetrieveLock(ctr.config.LockID)
if err != nil {
return fmt.Errorf("retrieving lock for container %s: %w", ctr.ID(), err)
}
ctr.lock = lock
// Get the OCI runtime
if ctr.config.OCIRuntime == "" {
ctr.ociRuntime = ctr.runtime.defaultOCIRuntime
} else {
// Handle legacy containers which might use a literal path for
// their OCI runtime name.
runtimeName := ctr.config.OCIRuntime
ociRuntime, ok := ctr.runtime.ociRuntimes[runtimeName]
if !ok {
runtimeSet := false
// If the path starts with a / and exists, make a new
// OCI runtime for it using the full path.
if strings.HasPrefix(runtimeName, "/") {
if stat, err := os.Stat(runtimeName); err == nil && !stat.IsDir() {
newOCIRuntime, err := newConmonOCIRuntime(runtimeName, []string{runtimeName}, ctr.runtime.conmonPath, ctr.runtime.runtimeFlags, ctr.runtime.config)
if err == nil {
// TODO: There is a potential risk of concurrent map modification here.
// This is an unlikely case, though.
ociRuntime = newOCIRuntime
ctr.runtime.ociRuntimes[runtimeName] = ociRuntime
runtimeSet = true
}
}
}
if !runtimeSet {
// Use a MissingRuntime implementation
ociRuntime = getMissingRuntime(runtimeName, ctr.runtime)
}
}
ctr.ociRuntime = ociRuntime
}
ctr.valid = true
return nil
}
// Finalize a pod that was pulled out of the database.
func (s *SQLiteState) createPod(rawJSON string) (*Pod, error) {
config := new(PodConfig)
if err := json.Unmarshal([]byte(rawJSON), config); err != nil {
return nil, fmt.Errorf("unmarshalling pod config: %w", err)
}
lock, err := s.runtime.lockManager.RetrieveLock(config.LockID)
if err != nil {
return nil, fmt.Errorf("retrieving lock for pod %s: %w", config.ID, err)
}
pod := new(Pod)
pod.config = config
pod.state = new(podState)
pod.lock = lock
pod.runtime = s.runtime
pod.valid = true
return pod, nil
}
// Finalize a volume that was pulled out of the database
func finalizeVolumeSqlite(vol *Volume) error {
// Get the lock
lock, err := vol.runtime.lockManager.RetrieveLock(vol.config.LockID)
if err != nil {
return fmt.Errorf("retrieving lock for volume %s: %w", vol.Name(), err)
}
vol.lock = lock
vol.valid = true
return nil
}
func (s *SQLiteState) rewriteContainerConfig(ctr *Container, newCfg *ContainerConfig) (defErr error) {
json, err := json.Marshal(newCfg)
if err != nil {
return fmt.Errorf("error marshalling container %s new config JSON: %w", ctr.ID(), err)
}
tx, err := s.conn.Begin()
if err != nil {
return fmt.Errorf("beginning transaction to rewrite container %s config: %w", ctr.ID(), err)
}
defer func() {
if defErr != nil {
if err := tx.Rollback(); err != nil {
logrus.Errorf("Rolling back transaction to rewrite container %s config: %v", ctr.ID(), err)
}
}
}()
results, err := tx.Exec("UPDATE ContainerConfig SET Name=?, JSON=? WHERE ID=?;", newCfg.Name, json, ctr.ID())
if err != nil {
return fmt.Errorf("updating container config table with new configuration for container %s: %w", ctr.ID(), err)
}
rows, err := results.RowsAffected()
if err != nil {
return fmt.Errorf("retrieving container %s config rewrite rows affected: %w", ctr.ID(), err)
}
if rows == 0 {
ctr.valid = false
return define.ErrNoSuchCtr
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction to rewrite container %s config: %w", ctr.ID(), err)
}
return nil
}
func (s *SQLiteState) addContainer(ctr *Container) (defErr error) {
for net := range ctr.config.Networks {
opts := ctr.config.Networks[net]
opts.Aliases = append(opts.Aliases, ctr.config.ID[:12])
ctr.config.Networks[net] = opts
}
configJSON, err := json.Marshal(ctr.config)
if err != nil {
return fmt.Errorf("marshalling container config json: %w", err)
}
stateJSON, err := json.Marshal(ctr.state)
if err != nil {
return fmt.Errorf("marshalling container state json: %w", err)
}
deps := ctr.Dependencies()
podID := sql.NullString{}
if ctr.config.Pod != "" {
podID.Valid = true
podID.String = ctr.config.Pod
}
tx, err := s.conn.Begin()
if err != nil {
return fmt.Errorf("beginning container create transaction: %w", err)
}
defer func() {
if defErr != nil {
if err := tx.Rollback(); err != nil {
logrus.Errorf("Rolling back transaction to create container: %v", err)
}
}
}()
if _, err := tx.Exec("INSERT INTO IDNamespace VALUES (?);", ctr.ID()); err != nil {
return fmt.Errorf("adding container id to database: %w", err)
}
if _, err := tx.Exec("INSERT INTO ContainerConfig VALUES (?, ?, ?, ?);", ctr.ID(), ctr.Name(), podID, configJSON); err != nil {
return fmt.Errorf("adding container config to database: %w", err)
}
if _, err := tx.Exec("INSERT INTO ContainerState VALUES (?, ?, ?, ?);", ctr.ID(), int(ctr.state.State), ctr.state.ExitCode, stateJSON); err != nil {
return fmt.Errorf("adding container state to database: %w", err)
}
for _, dep := range deps {
// Check if the dependency is in the same pod
var depPod sql.NullString
row := tx.QueryRow("SELECT PodID FROM ContainerConfig WHERE ID=?;", dep)
if err := row.Scan(&depPod); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("container dependency %s does not exist in database: %w", dep, define.ErrNoSuchCtr)
}
}
switch {
case ctr.config.Pod == "" && depPod.Valid:
return fmt.Errorf("container dependency %s is part of a pod, but container is not: %w", dep, define.ErrInvalidArg)
case ctr.config.Pod != "" && !depPod.Valid:
return fmt.Errorf("container dependency %s is not part of pod, but this container belongs to pod %s: %w", dep, ctr.config.Pod, define.ErrInvalidArg)
case ctr.config.Pod != "" && depPod.String != ctr.config.Pod:
return fmt.Errorf("container dependency %s is part of pod %s but container is part of pod %s, pods must match: %w", dep, depPod.String, ctr.config.Pod, define.ErrInvalidArg)
}
if _, err := tx.Exec("INSERT INTO ContainerDependency VALUES (?, ?);", ctr.ID(), dep); err != nil {
return fmt.Errorf("adding container dependency %s to database: %w", dep, err)
}
}
volMap := make(map[string]bool)
for _, vol := range ctr.config.NamedVolumes {
if _, ok := volMap[vol.Name]; !ok {
if _, err := tx.Exec("INSERT INTO ContainerVolume VALUES (?, ?);", ctr.ID(), vol.Name); err != nil {
return fmt.Errorf("adding container volume %s to database: %w", vol.Name, err)
}
volMap[vol.Name] = true
}
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing transaction: %w", err)
}
return nil
}
// removeContainer remove the specified container from the database.
func (s *SQLiteState) removeContainer(ctr *Container) (defErr error) {
tx, err := s.conn.Begin()
if err != nil {
return fmt.Errorf("beginning container %s removal transaction: %w", ctr.ID(), err)
}
defer func() {
if defErr != nil {
if err := tx.Rollback(); err != nil {
logrus.Errorf("Rolling back transaction to remove container %s: %v", ctr.ID(), err)
}
}
}()
if err := s.removeContainerWithTx(ctr.ID(), tx); err != nil {
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("committing container %s removal transaction: %w", ctr.ID(), err)
}
return nil
}
// removeContainerWithTx removes the container with the specified transaction.
// Callers are responsible for committing.
func (s *SQLiteState) removeContainerWithTx(id string, tx *sql.Tx) error {
// TODO TODO TODO:
// Need to verify that at least 1 row was deleted from ContainerConfig.
// Otherwise return ErrNoSuchCtr.
if _, err := tx.Exec("DELETE FROM IDNamespace WHERE ID=?;", id); err != nil {
return fmt.Errorf("removing container %s id from database: %w", id, err)
}
if _, err := tx.Exec("DELETE FROM ContainerConfig WHERE ID=?;", id); err != nil {
return fmt.Errorf("removing container %s config from database: %w", id, err)
}
if _, err := tx.Exec("DELETE FROM ContainerState WHERE ID=?;", id); err != nil {
return fmt.Errorf("removing container %s state from database: %w", id, err)
}
if _, err := tx.Exec("DELETE FROM ContainerDependency WHERE ID=?;", id); err != nil {
return fmt.Errorf("removing container %s dependencies from database: %w", id, err)
}
if _, err := tx.Exec("DELETE FROM ContainerVolume WHERE ContainerID=?;", id); err != nil {
return fmt.Errorf("removing container %s volumes from database: %w", id, err)
}
if _, err := tx.Exec("DELETE FROM ContainerExecSession WHERE ContainerID=?;", id); err != nil {
return fmt.Errorf("removing container %s exec sessions from database: %w", id, err)
}
return nil
}
// networkModify allows you to modify or add a new network, to add a new network use the new bool
func (s *SQLiteState) networkModify(ctr *Container, network string, opts types.PerNetworkOptions, new, disconnect bool) error {
if !s.valid {
return define.ErrDBClosed
}
if !ctr.valid {
return define.ErrCtrRemoved
}
if network == "" {
return fmt.Errorf("network names must not be empty: %w", define.ErrInvalidArg)
}
if new && disconnect {
return fmt.Errorf("new and disconnect are mutually exclusive: %w", define.ErrInvalidArg)
}
// Grab a fresh copy of the config, in case anything changed
newCfg, err := s.getCtrConfig(ctr.ID())
if err != nil && errors.Is(err, define.ErrNoSuchCtr) {
ctr.valid = false
return define.ErrNoSuchCtr
}
_, ok := newCfg.Networks[network]
if new && ok {
return fmt.Errorf("container %s is already connected to network %s: %w", ctr.ID(), network, define.ErrNoSuchNetwork)
}
if !ok && (!new || disconnect) {
return fmt.Errorf("container %s is not connected to network %s: %w", ctr.ID(), network, define.ErrNoSuchNetwork)
}
if !disconnect {
if newCfg.Networks == nil {
newCfg.Networks = make(map[string]types.PerNetworkOptions)
}
newCfg.Networks[network] = opts
} else {
delete(newCfg.Networks, network)
}
if err := s.rewriteContainerConfig(ctr, newCfg); err != nil {
return err
}
ctr.config = newCfg
return nil
}