mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-10-21 13:27:36 +00:00
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>
395 lines
11 KiB
Go
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
|
|
}
|