Skip to content

Commit b4068b4

Browse files
committed
Add migration code for BoltDB to SQLite
This is gated behind a new option in `podman system migrate`, `--migrate-db`, or by a system restart being performed. BoltDB support was removed in Podman 6, so we are certain that, when we start Podman, a SQLite state is in use. However, if we also detect a valid BoltDB state, we will attempt a migration. Migration is performed by retrieving all volumes, pods, and containers (in that order, to ensure there are no dependency conflicts) from the Bolt database, when adding them to the SQLite database. If there is a conflict - IE, a container exists in both SQLite and Bolt - we skip migration for that object. The old DB is then renamed so we do not try to migrate it again. Our ability to test complex migration scenarios is limited, but this should handle simple migrations easily. This is a heavily adapted version of #27660 rebuilt to work with Podman 6.0. This cannot be tested automatically, as the ability to create Bolt databases has been entirely removed with Podman 6. Signed-off-by: Matt Heon <matthew.heon@pm.me>
1 parent 330b41e commit b4068b4

9 files changed

Lines changed: 283 additions & 9 deletions

File tree

cmd/podman/system/migrate.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ func init() {
4444
newRuntimeFlagName := "new-runtime"
4545
flags.StringVar(&migrateOptions.NewRuntime, newRuntimeFlagName, "", "Specify a new runtime for all containers")
4646
_ = migrateCommand.RegisterFlagCompletionFunc(newRuntimeFlagName, completion.AutocompleteNone)
47+
48+
flags.BoolVar(&migrateOptions.MigrateDB, "migrate-db", false, "Migrate database from BoltDB to SQLite")
4749
}
4850

4951
func migrate(_ *cobra.Command, _ []string) error {

docs/source/markdown/podman-system-migrate.1.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ newly configured mappings.
2626

2727
## OPTIONS
2828

29+
#### **--migrate-db**
30+
31+
Migrate from the legacy BoltDB database to SQLite.
32+
Support for BoltDB will be removed in Podman 6.0.
33+
Podman will display a warning if this migration is necessary.
34+
To ensure complete migration, all other Podman commands should be shut down before database migration.
35+
In particular, systemd-activated services like **podman system service** and Quadlets should be manually stopped prior to migration.
36+
The legacy database will not be removed, so no data loss should occur even on failure.
37+
2938
#### **--new-runtime**=*runtime*
3039

3140
Set a new OCI runtime for all containers.

libpod/container_graph.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,89 @@ func startNode(ctx context.Context, node *containerNode, setError bool, ctrError
289289
}
290290
}
291291

292+
// Migrates all nodes to the new SQLite state
293+
func migrateNodeDatabase(node *containerNode, setError bool, ctrErrors map[string]error, ctrsVisited map[string]bool, sqliteState State) {
294+
// First, check if we have already visited the node
295+
if ctrsVisited[node.id] {
296+
return
297+
}
298+
299+
// If setError is true, a dependency of us failed
300+
// Mark us as failed and recurse
301+
if setError {
302+
// Mark us as visited, and set an error
303+
ctrsVisited[node.id] = true
304+
ctrErrors[node.id] = fmt.Errorf("a dependency of container %s failed to migrate: %w", node.id, define.ErrCtrStateInvalid)
305+
306+
// Hit anyone who depends on us, and set errors on them too
307+
for _, successor := range node.dependedOn {
308+
migrateNodeDatabase(successor, true, ctrErrors, ctrsVisited, sqliteState)
309+
}
310+
311+
return
312+
}
313+
314+
// Have all our dependencies started?
315+
// If not, don't visit the node yet
316+
depsVisited := true
317+
for _, dep := range node.dependsOn {
318+
depsVisited = depsVisited && ctrsVisited[dep.id]
319+
}
320+
if !depsVisited {
321+
// Don't visit us yet, all dependencies are not up
322+
// We'll hit the dependencies eventually, and when we do it will
323+
// recurse here
324+
return
325+
}
326+
327+
// Going to try to migrate the container, mark us as visited
328+
ctrsVisited[node.id] = true
329+
330+
ctrErrored := false
331+
ctrExists := false
332+
333+
// Add and save the container
334+
if node.container.config.Pod == "" {
335+
if err := sqliteState.AddContainer(node.container); err != nil {
336+
if errors.Is(err, define.ErrPodExists) {
337+
logrus.Warnf("Container with name %s already exists in the SQLite database; refusing to migrate from BoltDB", node.container.Name())
338+
ctrExists = true
339+
} else {
340+
ctrErrored = true
341+
ctrErrors[node.id] = err
342+
}
343+
}
344+
} else {
345+
// We don't actually *use* the pod in this operation, other than its ID...
346+
// So fake it
347+
pod := new(Pod)
348+
pod.config = new(PodConfig)
349+
pod.config.ID = node.container.config.Pod
350+
pod.valid = true
351+
352+
if err := sqliteState.AddContainer(node.container); err != nil {
353+
if errors.Is(err, define.ErrPodExists) {
354+
logrus.Warnf("Container with name %s already exists in the SQLite database; refusing to migrate from BoltDB", node.container.Name())
355+
ctrExists = true
356+
} else {
357+
ctrErrored = true
358+
ctrErrors[node.id] = err
359+
}
360+
}
361+
}
362+
if !ctrErrored && !ctrExists {
363+
if err := sqliteState.SaveContainer(node.container); err != nil {
364+
ctrErrored = true
365+
ctrErrors[node.id] = err
366+
}
367+
}
368+
369+
// Recurse to anyone who depends on us and start them
370+
for _, successor := range node.dependedOn {
371+
migrateNodeDatabase(successor, ctrErrored, ctrErrors, ctrsVisited, sqliteState)
372+
}
373+
}
374+
292375
// Contains all details required for traversing the container graph.
293376
type nodeTraversal struct {
294377
// Optional. but *MUST* be locked.

libpod/runtime.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,6 +792,16 @@ func (r *Runtime) Shutdown(force bool) error {
792792
func (r *Runtime) refresh(ctx context.Context, alivePath string) error {
793793
logrus.Debugf("Podman detected system restart - performing state refresh")
794794

795+
if err := r.checkCanMigrate(); err != nil {
796+
if errors.Is(err, errCannotMigrateHardcodedBolt) {
797+
logrus.Infof("Refusing to automatically migrate from BoltDB to SQLite as BoltDB is hardcoded in containers.conf")
798+
}
799+
} else {
800+
if err := r.migrateDB(); err != nil {
801+
logrus.Errorf("Automatic migration from BoltDB to SQLite failed: %v", err)
802+
}
803+
}
804+
795805
// Clear state of database if not running in container
796806
if !graphRootMounted() {
797807
// First clear the state in the database

libpod/runtime_migrate.go

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,23 @@
33
package libpod
44

55
import (
6+
"errors"
67
"fmt"
8+
"os"
79
"path/filepath"
810
"strings"
911

1012
"github.com/sirupsen/logrus"
13+
"go.podman.io/common/pkg/config"
1114
"go.podman.io/podman/v6/libpod/define"
1215
"go.podman.io/podman/v6/pkg/namespaces"
16+
"go.podman.io/storage/pkg/fileutils"
1317
)
1418

1519
// Migrate stops the rootless pause process and performs any necessary database
1620
// migrations that are required. It can also migrate all containers to a new OCI
1721
// runtime, if requested.
18-
func (r *Runtime) Migrate(newRuntime string) error {
22+
func (r *Runtime) Migrate(newRuntime string, migrateDB bool) error {
1923
// Acquire the alive lock and hold it.
2024
// Ensures that we don't let other Podman commands run while we are
2125
// rewriting things in the DB.
@@ -97,5 +101,161 @@ func (r *Runtime) Migrate(newRuntime string) error {
97101
}
98102
}
99103

104+
if migrateDB {
105+
if err := r.checkCanMigrate(); err != nil {
106+
switch {
107+
case errors.Is(err, errCannotMigrateNoBolt):
108+
fmt.Printf("No migration is necessary: %v", err)
109+
return r.stopPauseProcess()
110+
case errors.Is(err, errCannotMigrateHardcodedBolt):
111+
logrus.Errorf("In containers.conf, database_backend is manually set to \"boltdb\" - comment this line out and run `podman system migrate --migrate-db` or restart the system to complete migration to SQLite")
112+
return fmt.Errorf("unable to migrate to SQLite database as database backend manually set")
113+
default:
114+
return err
115+
}
116+
}
117+
118+
if err := r.migrateDB(); err != nil {
119+
return fmt.Errorf("migrating database from BoltDB to SQLite: %w", err)
120+
}
121+
}
122+
100123
return r.stopPauseProcess()
101124
}
125+
126+
var (
127+
errCannotMigrateNoBolt = errors.New("no BoltDB database to migrate")
128+
errCannotMigrateHardcodedBolt = errors.New("database_backend in containers.conf is manually set to \"boltdb\"")
129+
)
130+
131+
func (r *Runtime) checkCanMigrate() error {
132+
boltPath := getBoltDBPath(r)
133+
if err := fileutils.Exists(boltPath); err != nil {
134+
return errCannotMigrateNoBolt
135+
}
136+
137+
// Necessary as database configuration is overwritten when the state is set up.
138+
// So we need a completely new state from disk to see what the user set.
139+
newCfg, err := config.New(nil)
140+
if err != nil {
141+
return fmt.Errorf("reloading configuration to check database backend in use")
142+
}
143+
backend, err := config.ParseDBBackend(newCfg.Engine.DBBackend)
144+
if err != nil {
145+
return fmt.Errorf("invalid DB backend configured - please change containers.conf database_backend to \"sqlite\"")
146+
}
147+
if backend == config.DBBackendBoltDB {
148+
return errCannotMigrateHardcodedBolt
149+
}
150+
151+
return nil
152+
}
153+
154+
func (r *Runtime) migrateDB() error {
155+
boltPath := getBoltDBPath(r)
156+
// Get us a Bolt database
157+
oldState, err := NewBoltState(boltPath, r)
158+
if err != nil {
159+
return fmt.Errorf("opening legacy Bolt database at %s: %w", boltPath, err)
160+
}
161+
162+
// Migrate volumes, then pods, then containers.
163+
// Containers must be last as the pods they are part of and volumes they use must already exist.
164+
allVolumes, err := oldState.AllVolumes()
165+
if err != nil {
166+
return fmt.Errorf("retrieving volumes from boltdb: %w", err)
167+
}
168+
for _, vol := range allVolumes {
169+
if err := r.state.AddVolume(vol); err != nil {
170+
if errors.Is(err, define.ErrVolumeExists) {
171+
logrus.Warnf("Volume with name %s already exists in the SQLite database; refusing to migrate from BoltDB", vol.Name())
172+
continue
173+
}
174+
return err
175+
}
176+
if err := oldState.UpdateVolume(vol); err != nil {
177+
return err
178+
}
179+
if err := r.state.SaveVolume(vol); err != nil {
180+
return err
181+
}
182+
}
183+
184+
allPods, err := oldState.AllPods()
185+
if err != nil {
186+
return fmt.Errorf("retrieving pods from boltdb: %w", err)
187+
}
188+
for _, pod := range allPods {
189+
if err := r.state.AddPod(pod); err != nil {
190+
if errors.Is(err, define.ErrPodExists) {
191+
logrus.Warnf("Pod with name %s already exists in the SQLite database; refusing to migrate from BoltDB", pod.Name())
192+
continue
193+
}
194+
return err
195+
}
196+
if err := oldState.UpdatePod(pod); err != nil {
197+
return err
198+
}
199+
if err := r.state.SavePod(pod); err != nil {
200+
return err
201+
}
202+
}
203+
204+
// Containers must be done as a graph due to dependencies.
205+
// The state will error if we add a container before its dependencies.
206+
allCtrs, err := oldState.AllContainers(true)
207+
if err != nil {
208+
return fmt.Errorf("retrieving containers from boltdb: %w", err)
209+
}
210+
211+
// BoltDB doesn't actually populate container networks on initial pull
212+
// from the database, that needs to be done separately.
213+
for _, ctr := range allCtrs {
214+
ctrNetworks, err := oldState.GetNetworks(ctr)
215+
if err != nil {
216+
return err
217+
}
218+
ctr.config.Networks = convertLegacyNetworks(ctrNetworks)
219+
}
220+
221+
graph, err := BuildContainerGraph(allCtrs)
222+
if err != nil {
223+
return err
224+
}
225+
226+
ctrErrors := make(map[string]error)
227+
ctrsVisited := make(map[string]bool)
228+
229+
for _, node := range graph.noDepNodes {
230+
migrateNodeDatabase(node, false, ctrErrors, ctrsVisited, r.state)
231+
}
232+
var ctrError error
233+
for id, err := range ctrErrors {
234+
if ctrError != nil {
235+
logrus.Errorf("Migrating containers to SQLite: %v", ctrError)
236+
}
237+
ctrError = fmt.Errorf("migrating container %s: %w", id, err)
238+
}
239+
if ctrError != nil {
240+
return ctrError
241+
}
242+
243+
oldState.Close()
244+
245+
// Move the Bolt database so it is not reused, but preserve so data is not lost.
246+
newBoltDBPath := fmt.Sprintf("%s-old", boltPath)
247+
if err := os.Rename(boltPath, newBoltDBPath); err != nil {
248+
return fmt.Errorf("renaming old database %s to %s: %w", boltPath, newBoltDBPath, err)
249+
}
250+
fmt.Printf("Old database has been renamed to %s and will no longer be used\n", newBoltDBPath)
251+
252+
return nil
253+
}
254+
255+
func getBoltDBPath(runtime *Runtime) string {
256+
baseDir := runtime.config.Engine.StaticDir
257+
if runtime.storageConfig.TransientStore {
258+
baseDir = runtime.config.Engine.TmpDir
259+
}
260+
return filepath.Join(baseDir, "bolt_state.db")
261+
}

libpod/sqlite_state.go

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ type SQLiteState struct {
3232
}
3333

3434
const (
35+
// Name of the actual database file
36+
sqliteDbFilename = "db.sql"
3537
// Deal with timezone automatically.
3638
sqliteOptionLocation = "_loc=auto"
3739
// Force an fsync after each transaction (https://www.sqlite.org/pragma.html#pragma_synchronous).
@@ -44,7 +46,7 @@ const (
4446
sqliteOptionCaseSensitiveLike = "&_cslike=TRUE"
4547

4648
// Assembled sqlite options used when opening the database.
47-
sqliteOptions = "db.sql?" +
49+
sqliteOptions = sqliteDbFilename + "?" +
4850
sqliteOptionLocation +
4951
sqliteOptionSynchronous +
5052
sqliteOptionForeignKeys +
@@ -57,12 +59,7 @@ func NewSqliteState(runtime *Runtime) (_ State, defErr error) {
5759
logrus.Info("Using sqlite as database backend")
5860
state := new(SQLiteState)
5961

60-
basePath := runtime.storageConfig.GraphRoot
61-
if runtime.storageConfig.TransientStore {
62-
basePath = runtime.storageConfig.RunRoot
63-
} else if !runtime.storageSet.StaticDirSet {
64-
basePath = runtime.config.Engine.StaticDir
65-
}
62+
basePath, _ := sqliteStatePath(runtime)
6663

6764
// c/storage is set up *after* the DB - so even though we use the c/s
6865
// root (or, for transient, runroot) dir, we need to make the dir

libpod/sqlite_state_internal.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,18 @@ import (
1919
_ "github.com/mattn/go-sqlite3"
2020
)
2121

22+
// Returns two strings. First is base path - directory we'll create in.
23+
// Second is the filename of the database itself.
24+
func sqliteStatePath(runtime *Runtime) (string, string) {
25+
basePath := runtime.storageConfig.GraphRoot
26+
if runtime.storageConfig.TransientStore {
27+
basePath = runtime.storageConfig.RunRoot
28+
} else if !runtime.storageSet.StaticDirSet {
29+
basePath = runtime.config.Engine.StaticDir
30+
}
31+
return basePath, sqliteDbFilename
32+
}
33+
2234
func initSQLiteDB(conn *sql.DB) (defErr error) {
2335
// Start with a transaction to avoid "database locked" errors.
2436
// See https://github.com/mattn/go-sqlite3/issues/274#issuecomment-1429054597

pkg/domain/entities/types/system.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ type SystemPruneReport struct {
6363
// cli to migrate runtimes of containers
6464
type SystemMigrateOptions struct {
6565
NewRuntime string
66+
MigrateDB bool
6667
}
6768

6869
// SystemDfOptions describes the options for getting df information

pkg/domain/infra/abi/system.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ func (ic *ContainerEngine) Renumber(_ context.Context) error {
309309
}
310310

311311
func (ic *ContainerEngine) Migrate(_ context.Context, options entities.SystemMigrateOptions) error {
312-
return ic.Libpod.Migrate(options.NewRuntime)
312+
return ic.Libpod.Migrate(options.NewRuntime, options.MigrateDB)
313313
}
314314

315315
func unshareEnv(graphroot, runroot string) []string {

0 commit comments

Comments
 (0)