spiegel_podman/libpod/boltdb_state_internal.go
Matthew Heon abf885dfcc Re-add a minimal version of BoltDB for migrations
If we're going to maintain migration capability for the full
lifespan of 6 - and I think we're going to have to - the only
sane options are a separate binary that exclusively performs
migrations, or re-adding BoltDB code - in a very minimal way - to
allow us to perform migrations within the standard 6 binary.
After attempting the separate binary approach, results are not
promising - it's impossible to strip enough out to make a truly
small binary that still does what we need to perform a migration.
That leaves re-adding BoltDB code.

This adds a minimal version of the BoltDB code that no longer
claims to be a valid State (freeing us from the requirement of
continued maintenance - we should never touch these bits again
until they get removed in 7) which has just enough to get every
container, pod, and volume in the DB, so we can migrate them to
SQLite.

Signed-off-by: Matthew Heon <matthew.heon@pm.me>
2026-05-05 15:42:04 -04:00

395 lines
11 KiB
Go

//go:build !remote && (linux || freebsd)
package libpod
import (
"fmt"
"os"
"sort"
"strings"
"github.com/sirupsen/logrus"
bolt "go.etcd.io/bbolt"
"go.podman.io/common/libnetwork/types"
"go.podman.io/podman/v6/libpod/define"
)
const (
ctrName = "ctr"
allCtrsName = "all-ctrs"
podName = "pod"
allPodsName = "allPods"
volName = "vol"
allVolsName = "allVolumes"
execName = "exec"
aliasesName = "aliases"
volumeCtrsName = "volume-ctrs"
configName = "config"
stateName = "state"
netNSName = "netns"
networksName = "networks"
)
var (
ctrBkt = []byte(ctrName)
allCtrsBkt = []byte(allCtrsName)
podBkt = []byte(podName)
allPodsBkt = []byte(allPodsName)
volBkt = []byte(volName)
allVolsBkt = []byte(allVolsName)
aliasesBkt = []byte(aliasesName)
networksBkt = []byte(networksName)
configKey = []byte(configName)
stateKey = []byte(stateName)
netNSKey = []byte(netNSName)
)
// Open a connection to the database.
// Must be paired with a `defer closeDBCon()` on the returned database, to
// ensure the state is properly unlocked
func (s *BoltState) getDBCon() (*bolt.DB, error) {
// We need an in-memory lock to avoid issues around POSIX file advisory
// locks as described in the link below:
// https://www.sqlite.org/src/artifact/c230a7a24?ln=994-1081
s.dbLock.Lock()
db, err := bolt.Open(s.dbPath, 0o600, nil)
if err != nil {
return nil, fmt.Errorf("opening database %s: %w", s.dbPath, err)
}
return db, nil
}
// deferredCloseDBCon closes the bolt db but instead of returning an
// error it logs the error. it is meant to be used within the confines
// of a defer statement only
func (s *BoltState) deferredCloseDBCon(db *bolt.DB) {
if err := s.closeDBCon(db); err != nil {
logrus.Errorf("Failed to close libpod db: %q", err)
}
}
// Close a connection to the database.
// MUST be used in place of `db.Close()` to ensure proper unlocking of the
// state.
func (s *BoltState) closeDBCon(db *bolt.DB) error {
err := db.Close()
s.dbLock.Unlock()
return err
}
func getCtrBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(ctrBkt)
if bkt == nil {
return nil, fmt.Errorf("containers bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func getAllCtrsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(allCtrsBkt)
if bkt == nil {
return nil, fmt.Errorf("all containers bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func getPodBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(podBkt)
if bkt == nil {
return nil, fmt.Errorf("pods bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func getAllPodsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(allPodsBkt)
if bkt == nil {
return nil, fmt.Errorf("all pods bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func getVolBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(volBkt)
if bkt == nil {
return nil, fmt.Errorf("volumes bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func getAllVolsBucket(tx *bolt.Tx) (*bolt.Bucket, error) {
bkt := tx.Bucket(allVolsBkt)
if bkt == nil {
return nil, fmt.Errorf("all volumes bucket not found in DB: %w", define.ErrDBBadConfig)
}
return bkt, nil
}
func (s *BoltState) getContainerConfigFromDB(id []byte, config *ContainerConfig, ctrsBkt *bolt.Bucket) error {
ctrBkt := ctrsBkt.Bucket(id)
if ctrBkt == nil {
return fmt.Errorf("container %s not found in DB: %w", string(id), define.ErrNoSuchCtr)
}
configBytes := ctrBkt.Get(configKey)
if configBytes == nil {
return fmt.Errorf("container %s missing config key in DB: %w", string(id), define.ErrInternal)
}
if err := json.Unmarshal(configBytes, config); err != nil {
return fmt.Errorf("unmarshalling container %s config: %w", string(id), err)
}
// convert ports to the new format if needed
if len(config.ContainerNetworkConfig.OldPortMappings) > 0 && len(config.ContainerNetworkConfig.PortMappings) == 0 {
config.ContainerNetworkConfig.PortMappings = ocicniPortsToNetTypesPorts(config.ContainerNetworkConfig.OldPortMappings)
// keep the OldPortMappings in case an user has to downgrade podman
// indicate that the config was modified and should be written back to the db when possible
config.rewrite = true
}
return nil
}
func (s *BoltState) getContainerStateDB(id []byte, ctr *Container, ctrsBkt *bolt.Bucket) error {
newState := new(ContainerState)
ctrToUpdate := ctrsBkt.Bucket(id)
if ctrToUpdate == nil {
ctr.valid = false
return fmt.Errorf("container %s does not exist in database: %w", ctr.ID(), define.ErrNoSuchCtr)
}
newStateBytes := ctrToUpdate.Get(stateKey)
if newStateBytes == nil {
return fmt.Errorf("container %s does not have a state key in DB: %w", ctr.ID(), define.ErrInternal)
}
if err := json.Unmarshal(newStateBytes, newState); err != nil {
return fmt.Errorf("unmarshalling container %s state: %w", ctr.ID(), err)
}
// backwards compat, previously we used an extra bucket for the netns so try to get it from there
netNSBytes := ctrToUpdate.Get(netNSKey)
if netNSBytes != nil && newState.NetNS == "" {
newState.NetNS = string(netNSBytes)
}
// New state compiled successfully, swap it into the current state
ctr.state = newState
return nil
}
func (s *BoltState) getContainerFromDB(id []byte, ctr *Container, ctrsBkt *bolt.Bucket, loadState bool) error {
if err := s.getContainerConfigFromDB(id, ctr.config, ctrsBkt); err != nil {
return err
}
if loadState {
if err := s.getContainerStateDB(id, ctr, ctrsBkt); err != nil {
return err
}
}
// Get the lock
lock, err := s.runtime.lockManager.RetrieveLock(ctr.config.LockID)
if err != nil {
return fmt.Errorf("retrieving lock for container %s: %w", string(id), err)
}
ctr.lock = lock
if ctr.config.OCIRuntime == "" {
ctr.ociRuntime = s.runtime.defaultOCIRuntime
} else {
// Handle legacy containers which might use a literal path for
// their OCI runtime name.
runtimeName := ctr.config.OCIRuntime
ociRuntime, ok := s.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}, s.runtime.conmonPath, s.runtime.runtimeFlags, s.runtime.config)
if err == nil {
// The runtime lock should
// protect against concurrent
// modification of the map.
ociRuntime = newOCIRuntime
s.runtime.ociRuntimes[runtimeName] = ociRuntime
runtimeSet = true
}
}
}
if !runtimeSet {
// Use a MissingRuntime implementation
ociRuntime = getMissingRuntime(runtimeName, s.runtime)
}
}
ctr.ociRuntime = ociRuntime
}
ctr.runtime = s.runtime
ctr.valid = true
return nil
}
func (s *BoltState) getPodFromDB(id []byte, pod *Pod, podBkt *bolt.Bucket) error {
podDB := podBkt.Bucket(id)
if podDB == nil {
return fmt.Errorf("pod with ID %s not found: %w", string(id), define.ErrNoSuchPod)
}
podConfigBytes := podDB.Get(configKey)
if podConfigBytes == nil {
return fmt.Errorf("pod %s is missing configuration key in DB: %w", string(id), define.ErrInternal)
}
if err := json.Unmarshal(podConfigBytes, pod.config); err != nil {
return fmt.Errorf("unmarshalling pod %s config from DB: %w", string(id), err)
}
// Get the lock
lock, err := s.runtime.lockManager.RetrieveLock(pod.config.LockID)
if err != nil {
return fmt.Errorf("retrieving lock for pod %s: %w", string(id), err)
}
pod.lock = lock
pod.runtime = s.runtime
pod.valid = true
return nil
}
func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bucket) error {
volDB := volBkt.Bucket(name)
if volDB == nil {
return fmt.Errorf("volume with name %s not found: %w", string(name), define.ErrNoSuchVolume)
}
volConfigBytes := volDB.Get(configKey)
if volConfigBytes == nil {
return fmt.Errorf("volume %s is missing configuration key in DB: %w", string(name), define.ErrInternal)
}
if err := json.Unmarshal(volConfigBytes, volume.config); err != nil {
return fmt.Errorf("unmarshalling volume %s config from DB: %w", string(name), err)
}
// Volume state is allowed to be nil for legacy compatibility
volStateBytes := volDB.Get(stateKey)
if volStateBytes != nil {
if err := json.Unmarshal(volStateBytes, volume.state); err != nil {
return fmt.Errorf("unmarshalling volume %s state from DB: %w", string(name), err)
}
}
// Need this for UsesVolumeDriver() so set it now.
volume.runtime = s.runtime
// Retrieve volume driver
if volume.UsesVolumeDriver() {
plugin, err := s.runtime.getVolumePlugin(volume.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", volume.Name(), volume.config.Driver, err)
} else {
volume.plugin = plugin
}
}
// Get the lock
lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)
if err != nil {
return fmt.Errorf("retrieving lock for volume %q: %w", string(name), err)
}
volume.lock = lock
volume.valid = true
return nil
}
// ocicniPortsToNetTypesPorts convert the old port format to the new one
// while deduplicating ports into ranges
//
//nolint:staticcheck
func ocicniPortsToNetTypesPorts(ports []types.OCICNIPortMapping) []types.PortMapping {
if len(ports) == 0 {
return nil
}
newPorts := make([]types.PortMapping, 0, len(ports))
// first sort the ports
sort.Slice(ports, func(i, j int) bool {
return compareOCICNIPorts(ports[i], ports[j])
})
// we already check if the slice is empty so we can use the first element
currentPort := types.PortMapping{
HostIP: ports[0].HostIP,
HostPort: uint16(ports[0].HostPort),
ContainerPort: uint16(ports[0].ContainerPort),
Protocol: ports[0].Protocol,
Range: 1,
}
for i := 1; i < len(ports); i++ {
if ports[i].HostIP == currentPort.HostIP &&
ports[i].Protocol == currentPort.Protocol &&
ports[i].HostPort-int32(currentPort.Range) == int32(currentPort.HostPort) &&
ports[i].ContainerPort-int32(currentPort.Range) == int32(currentPort.ContainerPort) {
currentPort.Range++
} else {
newPorts = append(newPorts, currentPort)
currentPort = types.PortMapping{
HostIP: ports[i].HostIP,
HostPort: uint16(ports[i].HostPort),
ContainerPort: uint16(ports[i].ContainerPort),
Protocol: ports[i].Protocol,
Range: 1,
}
}
}
newPorts = append(newPorts, currentPort)
return newPorts
}
// compareOCICNIPorts will sort the ocicni ports by
// 1) host ip
// 2) protocol
// 3) hostPort
// 4) container port
//
//nolint:staticcheck
func compareOCICNIPorts(i, j types.OCICNIPortMapping) bool {
if i.HostIP != j.HostIP {
return i.HostIP < j.HostIP
}
if i.Protocol != j.Protocol {
return i.Protocol < j.Protocol
}
if i.HostPort != j.HostPort {
return i.HostPort < j.HostPort
}
return i.ContainerPort < j.ContainerPort
}