forked from containers/podman
/
sqlite_state_internal.go
552 lines (479 loc) · 18.6 KB
/
sqlite_state_internal.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
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 migrateSchemaIfNecessary(tx *sql.Tx) error {
// First, check if the DBConfig table exists
checkRow := tx.QueryRow("SELECT 1 FROM sqlite_master WHERE type='table' AND name='DBConfig';")
var check int
if err := checkRow.Scan(&check); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil
}
return fmt.Errorf("checking if DB config table exists: %w", err)
}
if check != 1 {
// Table does not exist, fresh database, no need to migrate.
return nil
}
row := tx.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
}
return fmt.Errorf("scanning schema version from DB config: %w", err)
}
// 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(tx *sql.Tx) 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,
}
for tblName, cmd := range tables {
if _, err := tx.Exec(cmd); err != nil {
return fmt.Errorf("creating table %s: %w", tblName, 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
// Retrieve volume driver
if vol.UsesVolumeDriver() {
plugin, err := vol.runtime.getVolumePlugin(vol.config)
if err != nil {
// We want to fail gracefully here, to ensure that we
// can still remove volumes even if their plugin is
// missing. Otherwise, we end up with volumes that
// cannot even be retrieved from the database and will
// cause things like `volume ls` to fail.
logrus.Errorf("Volume %s uses volume plugin %s, but it cannot be accessed - some functionality may not be available: %v", vol.Name(), vol.config.Driver, err)
} else {
vol.plugin = plugin
}
}
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)
}
}
}()
// TODO: There has to be a better way of doing this
var check int
row := tx.QueryRow("SELECT 1 FROM ContainerConfig WHERE Name=?;", ctr.Name())
if err := row.Scan(&check); err != nil {
if !errors.Is(err, sql.ErrNoRows) {
return fmt.Errorf("checking if container name %s exists in database: %w", ctr.Name(), err)
}
} else if check != 0 {
return fmt.Errorf("name %q is in use: %w", ctr.Name(), define.ErrCtrExists)
}
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.ErrNetworkConnected)
}
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
}