diff --git a/go.mod b/go.mod index 960d2e6a7b..7dbb47ce7d 100644 --- a/go.mod +++ b/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 diff --git a/libpod/boltdb_state.go b/libpod/boltdb_state.go new file mode 100644 index 0000000000..25de91b59e --- /dev/null +++ b/libpod/boltdb_state.go @@ -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 +} diff --git a/libpod/boltdb_state_internal.go b/libpod/boltdb_state_internal.go new file mode 100644 index 0000000000..f1824d1f7d --- /dev/null +++ b/libpod/boltdb_state_internal.go @@ -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 +}