mirror of
https://github.com/podman-container-tools/podman.git
synced 2026-08-05 00:15:44 +00:00
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>
This commit is contained in:
parent
de9589a3fb
commit
abf885dfcc
3 changed files with 994 additions and 1 deletions
2
go.mod
2
go.mod
|
|
@ -63,6 +63,7 @@ require (
|
|||
github.com/stretchr/testify v1.11.1
|
||||
github.com/vbauerster/mpb/v8 v8.12.0
|
||||
github.com/vishvananda/netlink v1.3.1
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b
|
||||
go.podman.io/common v0.67.2-0.20260504145149-b5d50461d3b9
|
||||
go.podman.io/image/v5 v5.39.3-0.20260504145149-b5d50461d3b9
|
||||
|
|
@ -171,7 +172,6 @@ require (
|
|||
github.com/vbatts/tar-split v0.12.3 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.etcd.io/bbolt v1.4.3 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
|
|
|
|||
598
libpod/boltdb_state.go
Normal file
598
libpod/boltdb_state.go
Normal file
|
|
@ -0,0 +1,598 @@
|
|||
//go:build !remote && (linux || freebsd)
|
||||
|
||||
package libpod
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"go.podman.io/common/libnetwork/types"
|
||||
"go.podman.io/podman/v6/libpod/define"
|
||||
"go.podman.io/storage/pkg/fileutils"
|
||||
)
|
||||
|
||||
// BoltState is a state implementation backed by a Bolt DB
|
||||
type BoltState struct {
|
||||
valid bool
|
||||
dbPath string
|
||||
dbLock sync.Mutex
|
||||
runtime *Runtime
|
||||
}
|
||||
|
||||
// A brief description of the format of the BoltDB state:
|
||||
// At the top level, the following buckets are created:
|
||||
// - idRegistryBkt: Maps ID to Name for containers and pods.
|
||||
// Used to ensure container and pod IDs are globally unique.
|
||||
// - nameRegistryBkt: Maps Name to ID for containers and pods.
|
||||
// Used to ensure container and pod names are globally unique.
|
||||
// - ctrBkt: Contains a sub-bucket for each container in the state.
|
||||
// Each sub-bucket has config and state keys holding the container's JSON
|
||||
// encoded configuration and state (respectively), an optional netNS key
|
||||
// containing the path to the container's network namespace, a dependencies
|
||||
// bucket containing the container's dependencies, and an optional pod key
|
||||
// containing the ID of the pod the container is joined to.
|
||||
// After updates to include exec sessions, may also include an exec bucket
|
||||
// with the IDs of exec sessions currently in use by the container.
|
||||
// - allCtrsBkt: Map of ID to name containing only containers. Used for
|
||||
// container lookup operations.
|
||||
// - podBkt: Contains a sub-bucket for each pod in the state.
|
||||
// Each sub-bucket has config and state keys holding the pod's JSON encoded
|
||||
// configuration and state, plus a containers sub bucket holding the IDs of
|
||||
// containers in the pod.
|
||||
// - allPodsBkt: Map of ID to name containing only pods. Used for pod lookup
|
||||
// operations.
|
||||
// - execBkt: Map of exec session ID to container ID - used for resolving
|
||||
// exec session IDs to the containers that hold the exec session.
|
||||
// - networksBkt: Contains all network names as key with their options json
|
||||
// encoded as value.
|
||||
// - aliasesBkt - Deprecated, use the networksBkt. Used to contain a bucket
|
||||
// for each CNI network which contain a map of network alias (an extra name
|
||||
// for containers in DNS) to the ID of the container holding the alias.
|
||||
// Aliases must be unique per-network, and cannot conflict with names
|
||||
// registered in nameRegistryBkt.
|
||||
// - runtimeConfigBkt: Contains configuration of the libpod instance that
|
||||
// initially created the database. This must match for any further instances
|
||||
// that access the database, to ensure that state mismatches with
|
||||
// containers/storage do not occur.
|
||||
// - exitCodeBucket/exitCodeTimeStampBucket: (#14559) exit codes must be part
|
||||
// of the database to resolve a previous race condition when one process waits
|
||||
// for the exit file to be written and another process removes it along with
|
||||
// the container during auto-removal. The same race would happen trying to
|
||||
// read the exit code from the containers bucket. Hence, exit codes go into
|
||||
// their own bucket. To avoid the rather expensive JSON (un)marshalling, we
|
||||
// have two buckets: one for the exit codes, the other for the timestamps.
|
||||
|
||||
// NewBoltState creates a new bolt-backed state database
|
||||
func NewBoltState(path string, runtime *Runtime) (*BoltState, error) {
|
||||
logrus.Info("Using boltdb as database backend")
|
||||
state := new(BoltState)
|
||||
state.dbPath = path
|
||||
state.runtime = runtime
|
||||
|
||||
logrus.Debugf("Opening legacy boltdb state at %s", path)
|
||||
|
||||
if err := fileutils.Exists(path); err != nil && errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, fmt.Errorf("boltdb database %s does not exist", path)
|
||||
}
|
||||
|
||||
db, err := bolt.Open(path, 0o600, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("opening database %s: %w", path, err)
|
||||
}
|
||||
// Everywhere else, we use s.deferredCloseDBCon(db) to ensure the state's DB
|
||||
// mutex is also unlocked.
|
||||
// However, here, the mutex has not been locked, since we just created
|
||||
// the DB connection, and it hasn't left this function yet - no risk of
|
||||
// concurrent access.
|
||||
// As such, just a db.Close() is fine here.
|
||||
defer db.Close()
|
||||
|
||||
state.valid = true
|
||||
|
||||
return state, nil
|
||||
}
|
||||
|
||||
// Close closes the state and prevents further use
|
||||
func (s *BoltState) Close() error {
|
||||
s.valid = false
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateContainer updates a container's state from the database
|
||||
func (s *BoltState) UpdateContainer(ctr *Container) error {
|
||||
if !s.valid {
|
||||
return define.ErrDBClosed
|
||||
}
|
||||
|
||||
if !ctr.valid {
|
||||
return define.ErrCtrRemoved
|
||||
}
|
||||
|
||||
ctrID := []byte(ctr.ID())
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
return db.View(func(tx *bolt.Tx) error {
|
||||
ctrBucket, err := getCtrBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.getContainerStateDB(ctrID, ctr, ctrBucket)
|
||||
})
|
||||
}
|
||||
|
||||
// AllContainers retrieves all the containers in the database
|
||||
// If `loadState` is set, the containers' state will be loaded as well.
|
||||
func (s *BoltState) AllContainers(loadState bool) ([]*Container, error) {
|
||||
if !s.valid {
|
||||
return nil, define.ErrDBClosed
|
||||
}
|
||||
|
||||
ctrs := []*Container{}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
allCtrsBucket, err := getAllCtrsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctrBucket, err := getCtrBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return allCtrsBucket.ForEach(func(id, _ []byte) error {
|
||||
// If performance becomes an issue, this check can be
|
||||
// removed. But the error messages that come back will
|
||||
// be much less helpful.
|
||||
ctrExists := ctrBucket.Bucket(id)
|
||||
if ctrExists == nil {
|
||||
return fmt.Errorf("state is inconsistent - container ID %s in all containers, but container not found: %w", string(id), define.ErrInternal)
|
||||
}
|
||||
|
||||
ctr := new(Container)
|
||||
ctr.config = new(ContainerConfig)
|
||||
ctr.state = new(ContainerState)
|
||||
|
||||
if err := s.getContainerFromDB(id, ctr, ctrBucket, loadState); err != nil {
|
||||
logrus.Errorf("Error retrieving container from database: %v", err)
|
||||
} else {
|
||||
ctrs = append(ctrs, ctr)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return ctrs, nil
|
||||
}
|
||||
|
||||
// GetNetworks returns the networks this container is a part of.
|
||||
func (s *BoltState) GetNetworks(ctr *Container) (map[string]types.PerNetworkOptions, error) {
|
||||
if !s.valid {
|
||||
return nil, define.ErrDBClosed
|
||||
}
|
||||
|
||||
if !ctr.valid {
|
||||
return nil, define.ErrCtrRemoved
|
||||
}
|
||||
|
||||
// if the network mode is not bridge return no networks
|
||||
if !ctr.config.NetMode.IsBridge() {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
ctrID := []byte(ctr.ID())
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
networks := make(map[string]types.PerNetworkOptions)
|
||||
|
||||
var convertDB bool
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
ctrBucket, err := getCtrBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbCtr := ctrBucket.Bucket(ctrID)
|
||||
if dbCtr == nil {
|
||||
ctr.valid = false
|
||||
return fmt.Errorf("container %s does not exist in database: %w", ctr.ID(), define.ErrNoSuchCtr)
|
||||
}
|
||||
|
||||
ctrNetworkBkt := dbCtr.Bucket(networksBkt)
|
||||
if ctrNetworkBkt == nil {
|
||||
// convert if needed
|
||||
convertDB = true
|
||||
return nil
|
||||
}
|
||||
|
||||
return ctrNetworkBkt.ForEach(func(network, v []byte) error {
|
||||
opts := types.PerNetworkOptions{}
|
||||
if err := json.Unmarshal(v, &opts); err != nil {
|
||||
// special case for backwards compat
|
||||
// earlier version used the container id as value so we set a
|
||||
// special error to indicate the we have to migrate the db
|
||||
if !bytes.Equal(v, ctrID) {
|
||||
return err
|
||||
}
|
||||
convertDB = true
|
||||
}
|
||||
networks[string(network)] = opts
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if convertDB {
|
||||
err = db.Update(func(tx *bolt.Tx) error {
|
||||
ctrBucket, err := getCtrBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dbCtr := ctrBucket.Bucket(ctrID)
|
||||
if dbCtr == nil {
|
||||
ctr.valid = false
|
||||
return fmt.Errorf("container %s does not exist in database: %w", ctr.ID(), define.ErrNoSuchCtr)
|
||||
}
|
||||
|
||||
var networkList []string
|
||||
|
||||
ctrNetworkBkt := dbCtr.Bucket(networksBkt)
|
||||
if ctrNetworkBkt == nil {
|
||||
ctrNetworkBkt, err = dbCtr.CreateBucket(networksBkt)
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating networks bucket for container %s: %w", ctr.ID(), err)
|
||||
}
|
||||
// the container has no networks in the db lookup config and write to the db
|
||||
networkList = ctr.config.NetworksDeprecated
|
||||
// if there are no networks we have to add the default
|
||||
if len(networkList) == 0 {
|
||||
networkList = []string{ctr.runtime.config.Network.DefaultNetwork}
|
||||
}
|
||||
} else {
|
||||
err = ctrNetworkBkt.ForEach(func(network, _ []byte) error {
|
||||
networkList = append(networkList, string(network))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// the container has no networks in the db lookup config and write to the db
|
||||
for i, network := range networkList {
|
||||
var intName string
|
||||
if ctr.state.NetInterfaceDescriptions != nil {
|
||||
eth, exists := ctr.state.NetInterfaceDescriptions[network]
|
||||
if !exists {
|
||||
return fmt.Errorf("no network interface name for container %s on network %s", ctr.config.ID, network)
|
||||
}
|
||||
intName = fmt.Sprintf("eth%d", eth)
|
||||
} else {
|
||||
intName = fmt.Sprintf("eth%d", i)
|
||||
}
|
||||
getAliases := func(network string) []string {
|
||||
var aliases []string
|
||||
ctrAliasesBkt := dbCtr.Bucket(aliasesBkt)
|
||||
if ctrAliasesBkt == nil {
|
||||
return nil
|
||||
}
|
||||
netAliasesBkt := ctrAliasesBkt.Bucket([]byte(network))
|
||||
if netAliasesBkt == nil {
|
||||
// No aliases for this specific network.
|
||||
return nil
|
||||
}
|
||||
|
||||
// let's ignore the error here there is nothing we can do
|
||||
_ = netAliasesBkt.ForEach(func(alias, _ []byte) error {
|
||||
aliases = append(aliases, string(alias))
|
||||
return nil
|
||||
})
|
||||
// also add the short container id as alias
|
||||
return aliases
|
||||
}
|
||||
|
||||
netOpts := &types.PerNetworkOptions{
|
||||
InterfaceName: intName,
|
||||
// we have to add the short id as alias for docker compat
|
||||
Aliases: append(getAliases(network), ctr.config.ID[:12]),
|
||||
}
|
||||
|
||||
optsBytes, err := json.Marshal(netOpts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// insert into network map because we need to return this
|
||||
networks[network] = *netOpts
|
||||
|
||||
err = ctrNetworkBkt.Put([]byte(network), optsBytes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
// GetContainerConfig returns a container config from the database by full ID
|
||||
func (s *BoltState) GetContainerConfig(id string) (*ContainerConfig, error) {
|
||||
if len(id) == 0 {
|
||||
return nil, define.ErrEmptyID
|
||||
}
|
||||
|
||||
if !s.valid {
|
||||
return nil, define.ErrDBClosed
|
||||
}
|
||||
|
||||
config := new(ContainerConfig)
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
ctrBucket, err := getCtrBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return s.getContainerConfigFromDB([]byte(id), config, ctrBucket)
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// UpdateVolume updates the volume's state from the database.
|
||||
func (s *BoltState) UpdateVolume(volume *Volume) error {
|
||||
if !s.valid {
|
||||
return define.ErrDBClosed
|
||||
}
|
||||
|
||||
if !volume.valid {
|
||||
return define.ErrVolumeRemoved
|
||||
}
|
||||
|
||||
newState := new(VolumeState)
|
||||
volumeName := []byte(volume.Name())
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
volBucket, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volToUpdate := volBucket.Bucket(volumeName)
|
||||
if volToUpdate == nil {
|
||||
volume.valid = false
|
||||
return fmt.Errorf("no volume with name %s found in database: %w", volume.Name(), define.ErrNoSuchVolume)
|
||||
}
|
||||
|
||||
stateBytes := volToUpdate.Get(stateKey)
|
||||
if stateBytes == nil {
|
||||
// Having no state is valid.
|
||||
// Return nil, use the empty state.
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(stateBytes, newState); err != nil {
|
||||
return fmt.Errorf("unmarshalling volume %s state: %w", volume.Name(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volume.state = newState
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllVolumes returns all volumes present in the state
|
||||
func (s *BoltState) AllVolumes() ([]*Volume, error) {
|
||||
if !s.valid {
|
||||
return nil, define.ErrDBClosed
|
||||
}
|
||||
|
||||
volumes := []*Volume{}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
allVolsBucket, err := getAllVolsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volBucket, err := getVolBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = allVolsBucket.ForEach(func(id, _ []byte) error {
|
||||
volExists := volBucket.Bucket(id)
|
||||
// This check can be removed if performance becomes an
|
||||
// issue, but much less helpful errors will be produced
|
||||
if volExists == nil {
|
||||
return fmt.Errorf("inconsistency in state - volume %s is in all volumes bucket but volume not found: %w", string(id), define.ErrInternal)
|
||||
}
|
||||
|
||||
volume := new(Volume)
|
||||
volume.config = new(VolumeConfig)
|
||||
volume.state = new(VolumeState)
|
||||
|
||||
if err := s.getVolumeFromDB(id, volume, volBucket); err != nil {
|
||||
if !errors.Is(err, define.ErrNSMismatch) {
|
||||
logrus.Errorf("Retrieving volume %s from the database: %v", string(id), err)
|
||||
}
|
||||
} else {
|
||||
volumes = append(volumes, volume)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return volumes, nil
|
||||
}
|
||||
|
||||
// UpdatePod updates a pod's state from the database
|
||||
func (s *BoltState) UpdatePod(pod *Pod) error {
|
||||
if !s.valid {
|
||||
return define.ErrDBClosed
|
||||
}
|
||||
|
||||
if !pod.valid {
|
||||
return define.ErrPodRemoved
|
||||
}
|
||||
|
||||
newState := new(podState)
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
podID := []byte(pod.ID())
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
podBkt, err := getPodBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podDB := podBkt.Bucket(podID)
|
||||
if podDB == nil {
|
||||
pod.valid = false
|
||||
return fmt.Errorf("no pod with ID %s found in database: %w", pod.ID(), define.ErrNoSuchPod)
|
||||
}
|
||||
|
||||
// Get the pod state JSON
|
||||
podStateBytes := podDB.Get(stateKey)
|
||||
if podStateBytes == nil {
|
||||
return fmt.Errorf("pod %s is missing state key in DB: %w", pod.ID(), define.ErrInternal)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(podStateBytes, newState); err != nil {
|
||||
return fmt.Errorf("unmarshalling pod %s state JSON: %w", pod.ID(), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pod.state = newState
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllPods returns all pods present in the state
|
||||
func (s *BoltState) AllPods() ([]*Pod, error) {
|
||||
if !s.valid {
|
||||
return nil, define.ErrDBClosed
|
||||
}
|
||||
|
||||
pods := []*Pod{}
|
||||
|
||||
db, err := s.getDBCon()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer s.deferredCloseDBCon(db)
|
||||
|
||||
err = db.View(func(tx *bolt.Tx) error {
|
||||
allPodsBucket, err := getAllPodsBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
podBucket, err := getPodBucket(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = allPodsBucket.ForEach(func(id, _ []byte) error {
|
||||
podExists := podBucket.Bucket(id)
|
||||
// This check can be removed if performance becomes an
|
||||
// issue, but much less helpful errors will be produced
|
||||
if podExists == nil {
|
||||
return fmt.Errorf("inconsistency in state - pod %s is in all pods bucket but pod not found: %w", string(id), define.ErrInternal)
|
||||
}
|
||||
|
||||
pod := new(Pod)
|
||||
pod.config = new(PodConfig)
|
||||
pod.state = new(podState)
|
||||
|
||||
if err := s.getPodFromDB(id, pod, podBucket); err != nil {
|
||||
if !errors.Is(err, define.ErrNSMismatch) {
|
||||
logrus.Errorf("Retrieving pod %s from the database: %v", string(id), err)
|
||||
}
|
||||
} else {
|
||||
pods = append(pods, pod)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pods, nil
|
||||
}
|
||||
395
libpod/boltdb_state_internal.go
Normal file
395
libpod/boltdb_state_internal.go
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
//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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue