mirror of
https://github.com/containerd/containerd.git
synced 2026-11-06 05:17:02 +00:00
1161 lines
29 KiB
Go
1161 lines
29 KiB
Go
/*
|
|
Copyright The containerd Authors.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
package manager
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
bolt "go.etcd.io/bbolt"
|
|
|
|
"github.com/containerd/errdefs"
|
|
"github.com/containerd/log"
|
|
|
|
"github.com/containerd/containerd/v2/core/leases"
|
|
"github.com/containerd/containerd/v2/core/metadata"
|
|
"github.com/containerd/containerd/v2/core/metadata/boltutil"
|
|
"github.com/containerd/containerd/v2/core/mount"
|
|
"github.com/containerd/containerd/v2/internal/kmutex"
|
|
"github.com/containerd/containerd/v2/pkg/gc"
|
|
"github.com/containerd/containerd/v2/pkg/namespaces"
|
|
)
|
|
|
|
type BoltManager interface {
|
|
mount.Manager
|
|
metadata.Collector
|
|
Sync(context.Context) error
|
|
}
|
|
|
|
type managerOptions struct {
|
|
handlers map[string]mount.Handler
|
|
roots []*os.Root
|
|
}
|
|
|
|
type Opt func(*managerOptions) error
|
|
|
|
func WithMountHandler(name string, h mount.Handler) Opt {
|
|
return func(o *managerOptions) error {
|
|
if o.handlers == nil {
|
|
o.handlers = make(map[string]mount.Handler)
|
|
}
|
|
o.handlers[name] = h
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func WithAllowedRoot(root string) Opt {
|
|
return func(o *managerOptions) error {
|
|
r, err := os.OpenRoot(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.roots = append(o.roots, r)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func NewManager(db *bolt.DB, targetDir string, opts ...Opt) (mount.Manager, error) {
|
|
options := managerOptions{}
|
|
for _, o := range opts {
|
|
if err := o(&options); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if err := os.MkdirAll(targetDir, 0700); err != nil {
|
|
return nil, err
|
|
}
|
|
tr, err := os.OpenRoot(targetDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to open target root %q: %w", targetDir, err)
|
|
}
|
|
rootMap := map[string]*os.Root{
|
|
tr.Name(): tr,
|
|
}
|
|
for _, r := range options.roots {
|
|
rootMap[r.Name()] = r
|
|
}
|
|
|
|
return &mountManager{
|
|
db: db,
|
|
targets: tr,
|
|
handlers: options.handlers,
|
|
rootMap: rootMap,
|
|
activate: kmutex.New(),
|
|
}, nil
|
|
}
|
|
|
|
type mountManager struct {
|
|
db *bolt.DB
|
|
targets *os.Root
|
|
handlers map[string]mount.Handler
|
|
rootMap map[string]*os.Root
|
|
|
|
rwlock sync.RWMutex
|
|
activate kmutex.KeyedLocker
|
|
}
|
|
|
|
func (mm *mountManager) Close() error {
|
|
var errs []error
|
|
for _, r := range mm.rootMap {
|
|
if err := r.Close(); err != nil {
|
|
errs = append(errs, err)
|
|
}
|
|
}
|
|
errs = append(errs, mm.db.Close())
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func (mm *mountManager) Activate(ctx context.Context, name string, mounts []mount.Mount, opts ...mount.ActivateOpt) (info mount.ActivationInfo, retErr error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
|
|
// Serialize concurrent activations of the same name to prevent a
|
|
// racing Activate from misidentifying an in-progress activation as
|
|
// a stale record and destroying it.
|
|
if err := mm.activate.Lock(ctx, name); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
defer mm.activate.Unlock(name)
|
|
|
|
log.G(ctx).WithField("name", name).WithField("mounts", mounts).Debugf("activating mount")
|
|
|
|
lid, leased := leases.FromContext(ctx)
|
|
|
|
var config mount.ActivateOptions
|
|
for _, opt := range opts {
|
|
opt(&config)
|
|
}
|
|
|
|
transforms := map[string]mount.Transformer{
|
|
"format": mountFormatter{},
|
|
"mkfs": &mkfs{
|
|
rootMap: mm.rootMap,
|
|
},
|
|
"mkdir": &mkdir{
|
|
rootMap: mm.rootMap,
|
|
},
|
|
}
|
|
|
|
start := time.Now()
|
|
plan := planActivation(ctx, mounts, config, transforms, mm.handlers)
|
|
firstSystemMount := plan.firstSystemMount
|
|
mountConv := plan.transforms
|
|
handlers := plan.handlers
|
|
// If no mounts are handled here, return not implemented and caller
|
|
// may just perform system mounts as normal.
|
|
if firstSystemMount == -1 {
|
|
return mount.ActivationInfo{}, errdefs.ErrNotImplemented
|
|
}
|
|
|
|
// Get read lock to block GC context from starting
|
|
mm.rwlock.RLock()
|
|
defer mm.rwlock.RUnlock()
|
|
|
|
var mid uint64
|
|
var staleMID uint64
|
|
|
|
if err := mm.db.Update(func(tx *bolt.Tx) error {
|
|
v1bkt, err := tx.CreateBucketIfNotExists([]byte("v1"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
nsbkt, err := v1bkt.CreateBucketIfNotExists([]byte(namespace))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mbkt, err := nsbkt.CreateBucketIfNotExists(bucketKeyMounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bkt, err := mbkt.CreateBucket([]byte(name))
|
|
if err != nil {
|
|
existing := mbkt.Bucket([]byte(name))
|
|
if existing == nil {
|
|
return err
|
|
}
|
|
// If the mount is fully activated, return already exists
|
|
// so the caller can reuse the existing mount.
|
|
if existing.Bucket(bucketKeyActive) != nil {
|
|
return fmt.Errorf("mount %q: %w", name, errdefs.ErrAlreadyExists)
|
|
}
|
|
// The mount bucket exists but was never fully activated
|
|
// (e.g., process crashed between creating the bucket and
|
|
// completing activation). Clean up the stale entry and
|
|
// proceed with a fresh activation. Save the old mount ID
|
|
// so the target directory can be cleaned up after the
|
|
// transaction commits.
|
|
staleMID = readID(existing)
|
|
if lid := existing.Get(bucketKeyLease); len(lid) > 0 {
|
|
if lsbkt := nsbkt.Bucket(bucketKeyLeases); lsbkt != nil {
|
|
if lbkt := lsbkt.Bucket(lid); lbkt != nil {
|
|
if err := lbkt.Delete([]byte(name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err := mbkt.DeleteBucket([]byte(name)); err != nil {
|
|
return err
|
|
}
|
|
bkt, err = mbkt.CreateBucket([]byte(name))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
mid, err = v1bkt.NextSequence()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
idb, err := encodeID(mid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = bkt.Put(bucketKeyID, idb); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := boltutil.WriteLabels(bkt, config.Labels); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := boltutil.WriteTimestamps(bkt, start, start); err != nil {
|
|
return err
|
|
}
|
|
|
|
if leased {
|
|
if err = bkt.Put(bucketKeyLease, []byte(lid)); err != nil {
|
|
return err
|
|
}
|
|
|
|
lsbkt, err := nsbkt.CreateBucketIfNotExists(bucketKeyLeases)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
lbkt, err := lsbkt.CreateBucketIfNotExists([]byte(lid))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := lbkt.Put([]byte(name), nil); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// TODO: Store mount information including mountpoint
|
|
// Setup mounts now with generated targets
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
|
|
// If a stale incomplete activation was found, clean up its target
|
|
// directory which may contain leftover mounts from before a crash.
|
|
if staleMID != 0 {
|
|
staleTarget := filepath.Join(mm.targets.Name(), strconv.FormatUint(staleMID, 10))
|
|
if err := unmountAll(ctx, staleTarget, mm.handlers); err != nil {
|
|
if os.IsNotExist(err) {
|
|
log.G(ctx).WithError(err).WithField("mountid", staleMID).Debug("stale activation target does not exist, skipping cleanup")
|
|
} else {
|
|
log.G(ctx).WithError(err).WithField("mountid", staleMID).Warn("failed to unmount stale activation target")
|
|
}
|
|
}
|
|
}
|
|
|
|
defer func() {
|
|
// If error, rollback and remove by name
|
|
if retErr != nil {
|
|
if err := mm.db.Update(func(tx *bolt.Tx) error {
|
|
v1bkt := tx.Bucket([]byte("v1"))
|
|
if v1bkt == nil {
|
|
return fmt.Errorf("missing bucket: %w", errdefs.ErrUnknown)
|
|
}
|
|
|
|
nsbkt := v1bkt.Bucket([]byte(namespace))
|
|
if nsbkt == nil {
|
|
return fmt.Errorf("missing namespace %q bucket: %w", namespace, errdefs.ErrUnknown)
|
|
}
|
|
|
|
mbkt := nsbkt.Bucket(bucketKeyMounts)
|
|
if mbkt == nil {
|
|
return fmt.Errorf("missing mounts bucket: %w", errdefs.ErrUnknown)
|
|
}
|
|
|
|
if leased {
|
|
lsbkt := nsbkt.Bucket(bucketKeyLeases)
|
|
if lsbkt != nil {
|
|
lbkt := lsbkt.Bucket([]byte(lid))
|
|
if lbkt != nil {
|
|
lbkt.Delete([]byte(name))
|
|
}
|
|
if k, _ := lbkt.Cursor().First(); k == nil {
|
|
lsbkt.DeleteBucket([]byte(lid))
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
return mbkt.DeleteBucket([]byte(name))
|
|
}); err != nil {
|
|
log.G(ctx).WithError(err).WithField("name", name).Errorf("failed to rollback")
|
|
}
|
|
}
|
|
}()
|
|
|
|
targetName := strconv.FormatUint(mid, 10)
|
|
if err := mm.targets.Mkdir(targetName, 0700); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
|
|
var mounted []mount.ActiveMount
|
|
defer func() {
|
|
// If error, unmount all mounted
|
|
if retErr != nil {
|
|
for i, m := range mounted {
|
|
var err error
|
|
if h := handlers[i]; h != nil {
|
|
err = h.Unmount(ctx, m.MountPoint)
|
|
} else {
|
|
err = mount.Unmount(m.MountPoint, 0)
|
|
}
|
|
if err != nil {
|
|
log.G(ctx).WithError(err).WithField("MountPoint", m.MountPoint).Error("failed to cleanup mount after failed activation")
|
|
}
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Ensure directory order for cleanup when rare case of large number of mounts,
|
|
// this allows cleanup logic to just scan directories on cleanup.
|
|
formatMP := "%d"
|
|
formatType := "%d-type"
|
|
if firstSystemMount > 100 {
|
|
formatMP = "%03d"
|
|
formatType = "%03d-type"
|
|
} else if firstSystemMount > 10 {
|
|
formatMP = "%02d"
|
|
formatType = "%02d-type"
|
|
}
|
|
|
|
for i, m := range mounts[:firstSystemMount] {
|
|
if mountConv != nil && mountConv[i] != nil {
|
|
for _, tr := range mountConv[i] {
|
|
newM, err := tr.Transform(ctx, m, mounted)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
m = newM
|
|
}
|
|
mounts[i] = m
|
|
}
|
|
|
|
// Use cleanup order for directory names
|
|
ci := firstSystemMount - i
|
|
// TODO: Go 1.25 use targetbase.WriteFile
|
|
if err := os.WriteFile(filepath.Join(mm.targets.Name(), targetName, fmt.Sprintf(formatType, ci)), []byte(m.Type), 0600); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
|
|
mname := fmt.Sprintf(formatMP, ci)
|
|
var active mount.ActiveMount
|
|
if h := handlers[i]; h != nil {
|
|
active, err = h.Mount(ctx, m, filepath.Join(mm.targets.Name(), targetName, mname), mounted)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, fmt.Errorf("mount handler failed %v: %w", m, err)
|
|
}
|
|
} else {
|
|
if err := mm.targets.Mkdir(filepath.Join(targetName, mname), 0700); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
mp := filepath.Join(mm.targets.Name(), targetName, mname)
|
|
if err := m.Mount(mp); err != nil {
|
|
return mount.ActivationInfo{}, fmt.Errorf("mount failed %v: %w", m, err)
|
|
}
|
|
t := time.Now()
|
|
active = mount.ActiveMount{
|
|
Mount: m,
|
|
MountPoint: mp,
|
|
MountedAt: &t,
|
|
}
|
|
}
|
|
mounted = append(mounted, active)
|
|
}
|
|
|
|
// If the first system mount has transforms, apply the ones the caller
|
|
// has not claimed. A claim can only be honored as a suffix of the
|
|
// chain: applyCount always covers at least every transform up to and
|
|
// including the last unclaimed one, since each depends on the last's
|
|
// output; see planActivation.
|
|
//
|
|
// firstSystemMount can reach len(mounts) when every mount is handled
|
|
// inside the manager (for example, every mount claimed via Temporary);
|
|
// there is then no system mount left to transform.
|
|
if mountConv != nil && firstSystemMount < len(mounts) {
|
|
for _, tr := range mountConv[firstSystemMount][:plan.applyCount[firstSystemMount]] {
|
|
newM, err := tr.Transform(ctx, mounts[firstSystemMount], mounted)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
mounts[firstSystemMount] = newM
|
|
}
|
|
}
|
|
// If no system mounts, add a bind mount if temporary
|
|
// TODO: Add config for whether to add the bind mount?
|
|
if config.Temporary && firstSystemMount > 0 {
|
|
mounts = append(mounts, mount.Mount{
|
|
Type: "bind",
|
|
Source: mounted[firstSystemMount-1].MountPoint,
|
|
Options: []string{"rbind"},
|
|
})
|
|
}
|
|
|
|
info.Name = name
|
|
info.Active = mounted
|
|
info.System = mounts[firstSystemMount:]
|
|
info.Labels = config.Labels
|
|
|
|
// Open another write transaction and update state, or another way to update state?
|
|
if err := mm.db.Update(func(tx *bolt.Tx) error {
|
|
v1bkt := tx.Bucket([]byte("v1"))
|
|
if v1bkt == nil {
|
|
return fmt.Errorf("missing v1 bucket: %w", errdefs.ErrUnknown)
|
|
}
|
|
|
|
nsbkt := v1bkt.Bucket([]byte(namespace))
|
|
if nsbkt == nil {
|
|
return fmt.Errorf("missing namespace %q bucket: %w", namespace, errdefs.ErrUnknown)
|
|
}
|
|
|
|
mbkt := nsbkt.Bucket(bucketKeyMounts)
|
|
if mbkt == nil {
|
|
return fmt.Errorf("missing mounts bucket: %w", errdefs.ErrUnknown)
|
|
}
|
|
bkt := mbkt.Bucket([]byte(name))
|
|
if bkt == nil {
|
|
return fmt.Errorf("missing mount %q bucket: %w", name, errdefs.ErrUnknown)
|
|
}
|
|
|
|
abkt, err := bkt.CreateBucket(bucketKeyActive)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, active := range mounted {
|
|
// Error is i > uint8 max
|
|
cur, err := abkt.CreateBucket([]byte{byte(i)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = putActiveMount(cur, active); err != nil {
|
|
return err
|
|
}
|
|
|
|
}
|
|
|
|
if err := boltutil.WriteTimestamps(bkt, start, time.Now()); err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(info.System) > 0 {
|
|
if len(info.System) > 255 {
|
|
return fmt.Errorf("too many system mounts (%d): maximum 255", len(info.System))
|
|
}
|
|
sbkt, err := bkt.CreateBucket(bucketKeySystem)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for i, sm := range info.System {
|
|
cur, err := sbkt.CreateBucket([]byte{byte(i)})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err = putSystemMount(cur, sm); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func encodeID(id uint64) ([]byte, error) {
|
|
var (
|
|
buf [binary.MaxVarintLen64]byte
|
|
idEncoded = buf[:]
|
|
)
|
|
idEncoded = idEncoded[:binary.PutUvarint(idEncoded, id)]
|
|
|
|
if len(idEncoded) == 0 {
|
|
return nil, fmt.Errorf("failed encoding id = %v", id)
|
|
}
|
|
return idEncoded, nil
|
|
}
|
|
|
|
func readID(bkt *bolt.Bucket) uint64 {
|
|
id, _ := binary.Uvarint(bkt.Get(bucketKeyID))
|
|
return id
|
|
}
|
|
|
|
func putActiveMount(bkt *bolt.Bucket, active mount.ActiveMount) error {
|
|
if err := bkt.Put(bucketKeyType, []byte(active.Type)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Same if device?
|
|
if err := bkt.Put(bucketKeyMountPoint, []byte(active.MountPoint)); err != nil {
|
|
return err
|
|
}
|
|
|
|
mountedAt, err := active.MountedAt.MarshalBinary()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := bkt.Put(bucketKeyMountedAt, mountedAt); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Add Source
|
|
// TODO: Add Target
|
|
// TODO: Add Options
|
|
|
|
return nil
|
|
}
|
|
|
|
func readActiveMount(bkt *bolt.Bucket) (mount.ActiveMount, error) {
|
|
var active mount.ActiveMount
|
|
active.Type = string(bkt.Get(bucketKeyType))
|
|
active.MountPoint = string(bkt.Get(bucketKeyMountPoint))
|
|
if v := bkt.Get(bucketKeyMountedAt); v != nil {
|
|
var mountedAt time.Time
|
|
if err := mountedAt.UnmarshalBinary(v); err != nil {
|
|
// TODO: Should this be skipped or otherwise logged and ignored?
|
|
return mount.ActiveMount{}, err
|
|
}
|
|
active.MountedAt = &mountedAt
|
|
}
|
|
|
|
return active, nil
|
|
}
|
|
|
|
func putSystemMount(bkt *bolt.Bucket, m mount.Mount) error {
|
|
if err := bkt.Put(bucketKeyType, []byte(m.Type)); err != nil {
|
|
return err
|
|
}
|
|
if err := bkt.Put(bucketKeySource, []byte(m.Source)); err != nil {
|
|
return err
|
|
}
|
|
if err := bkt.Put(bucketKeyTarget, []byte(m.Target)); err != nil {
|
|
return err
|
|
}
|
|
if len(m.Options) > 0 {
|
|
if err := bkt.Put(bucketKeyOptions, []byte(strings.Join(m.Options, "\x00"))); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readSystemMount(bkt *bolt.Bucket) mount.Mount {
|
|
m := mount.Mount{
|
|
Type: string(bkt.Get(bucketKeyType)),
|
|
Source: string(bkt.Get(bucketKeySource)),
|
|
Target: string(bkt.Get(bucketKeyTarget)),
|
|
}
|
|
if v := bkt.Get(bucketKeyOptions); len(v) > 0 {
|
|
m.Options = strings.Split(string(v), "\x00")
|
|
}
|
|
return m
|
|
}
|
|
|
|
func readActivationInfo(name string, bkt *bolt.Bucket) (mount.ActivationInfo, error) {
|
|
info := mount.ActivationInfo{
|
|
Name: name,
|
|
}
|
|
if abkt := bkt.Bucket(bucketKeyActive); abkt != nil {
|
|
if err := abkt.ForEachBucket(func(k []byte) error {
|
|
active, err := readActiveMount(abkt.Bucket(k))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
info.Active = append(info.Active, active)
|
|
return nil
|
|
}); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
}
|
|
if sbkt := bkt.Bucket(bucketKeySystem); sbkt != nil {
|
|
if err := sbkt.ForEachBucket(func(k []byte) error {
|
|
info.System = append(info.System, readSystemMount(sbkt.Bucket(k)))
|
|
return nil
|
|
}); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
}
|
|
lbls, err := boltutil.ReadLabels(bkt)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
info.Labels = lbls
|
|
|
|
return info, nil
|
|
}
|
|
|
|
func getBucket(tx *bolt.Tx, keys ...[]byte) *bolt.Bucket {
|
|
bkt := tx.Bucket(keys[0])
|
|
if bkt == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, key := range keys[1:] {
|
|
bkt = bkt.Bucket(key)
|
|
if bkt == nil {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return bkt
|
|
}
|
|
|
|
func (mm *mountManager) Deactivate(ctx context.Context, name string) error {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var (
|
|
mid uint64
|
|
allActive []mount.ActiveMount
|
|
)
|
|
|
|
// First in a single transaction, mark the mounts as deactivated
|
|
if err := mm.db.Update(func(tx *bolt.Tx) error {
|
|
v1bkt := tx.Bucket([]byte("v1"))
|
|
if v1bkt == nil {
|
|
return fmt.Errorf("missing v1 bucket: %w", errdefs.ErrNotFound)
|
|
}
|
|
|
|
nsbkt := v1bkt.Bucket([]byte(namespace))
|
|
if nsbkt == nil {
|
|
return fmt.Errorf("missing namespace %q bucket: %w", namespace, errdefs.ErrNotFound)
|
|
}
|
|
|
|
mbkt := nsbkt.Bucket(bucketKeyMounts)
|
|
if mbkt == nil {
|
|
return fmt.Errorf("missing mounts bucket: %w", errdefs.ErrNotFound)
|
|
}
|
|
bkt := mbkt.Bucket([]byte(name))
|
|
if bkt == nil {
|
|
return fmt.Errorf("missing mount %q bucket: %w", name, errdefs.ErrNotFound)
|
|
}
|
|
|
|
mid = readID(bkt)
|
|
|
|
lid := bkt.Get(bucketKeyLease)
|
|
if lid != nil {
|
|
lssbkt := nsbkt.Bucket(bucketKeyLeases)
|
|
if lssbkt != nil {
|
|
lsbkt := lssbkt.Bucket(lid)
|
|
if lsbkt != nil {
|
|
if err = lsbkt.Delete([]byte(name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
abkt := bkt.Bucket(bucketKeyActive)
|
|
if abkt != nil {
|
|
abkt.ForEachBucket(func(k []byte) error {
|
|
active, err := readActiveMount(abkt.Bucket(k))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
allActive = append(allActive, active)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
if err = mbkt.DeleteBucket([]byte(name)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Is unmountq really needed or just delete?
|
|
|
|
return nil
|
|
}); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: Should this also be backgrounded, no much can do on failure to unmount
|
|
var mountErrors error
|
|
for _, active := range slices.Backward(allActive) {
|
|
var err error
|
|
if h := mm.handlers[active.Type]; h != nil {
|
|
err = h.Unmount(ctx, active.MountPoint)
|
|
} else {
|
|
err = mount.Unmount(active.MountPoint, 0)
|
|
}
|
|
if err != nil {
|
|
mountErrors = errors.Join(mountErrors, err)
|
|
}
|
|
}
|
|
if mountErrors != nil {
|
|
// Don't try to cleanup, GC will need to do the rest
|
|
return mountErrors
|
|
}
|
|
|
|
// Run in background, GC would handle leftovers?
|
|
// Make configurable?
|
|
// TODO: In go 1.25, use mm.targets.RemoveAll()
|
|
if err := os.RemoveAll(filepath.Join(mm.targets.Name(), fmt.Sprintf("%d", mid))); err != nil {
|
|
// TODO: Only log here, cleanup would have to occur later
|
|
log.G(ctx).WithError(err).WithField("mountid", mid).Error("failed to cleanup mount target")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mm *mountManager) Info(ctx context.Context, name string) (mount.ActivationInfo, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
var info mount.ActivationInfo
|
|
if err := mm.db.View(func(tx *bolt.Tx) error {
|
|
bkt := getBucket(tx, []byte("v1"), []byte(namespace), bucketKeyMounts, []byte(name))
|
|
if bkt == nil {
|
|
return fmt.Errorf("mount %q %w", name, errdefs.ErrNotFound)
|
|
}
|
|
var err error
|
|
info, err = readActivationInfo(name, bkt)
|
|
return err
|
|
}); err != nil {
|
|
return mount.ActivationInfo{}, err
|
|
}
|
|
return info, nil
|
|
}
|
|
|
|
func (mm *mountManager) Update(context.Context, mount.ActivationInfo, ...string) (mount.ActivationInfo, error) {
|
|
return mount.ActivationInfo{}, errdefs.ErrNotImplemented
|
|
}
|
|
|
|
func (mm *mountManager) List(ctx context.Context, filters ...string) ([]mount.ActivationInfo, error) {
|
|
namespace, err := namespaces.NamespaceRequired(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var infos []mount.ActivationInfo
|
|
if err := mm.db.View(func(tx *bolt.Tx) error {
|
|
mbkt := getBucket(tx, []byte("v1"), []byte(namespace), bucketKeyMounts)
|
|
if mbkt == nil {
|
|
return nil
|
|
}
|
|
|
|
return mbkt.ForEachBucket(func(k []byte) error {
|
|
info, err := readActivationInfo(string(k), mbkt.Bucket(k))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
infos = append(infos, info)
|
|
return nil
|
|
})
|
|
}); err != nil {
|
|
return nil, err
|
|
}
|
|
return infos, nil
|
|
}
|
|
|
|
func (mm *mountManager) StartCollection(ctx context.Context) (metadata.CollectionContext, error) {
|
|
// lock now and collection will unlock on cancel or finish
|
|
mm.rwlock.Lock()
|
|
|
|
tx, err := mm.db.Begin(true)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &collectionContext{
|
|
ctx: ctx,
|
|
tx: tx,
|
|
manager: mm,
|
|
removed: map[string]map[string]struct{}{},
|
|
}, nil
|
|
}
|
|
|
|
func (mm *mountManager) ReferenceLabel() string {
|
|
return "mount"
|
|
}
|
|
|
|
type collectionContext struct {
|
|
ctx context.Context
|
|
tx *bolt.Tx
|
|
manager *mountManager
|
|
removed map[string]map[string]struct{}
|
|
}
|
|
|
|
func (cc *collectionContext) All(fn func(gc.Node)) {
|
|
v1bkt := cc.tx.Bucket([]byte("v1"))
|
|
if v1bkt == nil {
|
|
return
|
|
}
|
|
nsc := v1bkt.Cursor()
|
|
for nsk, nsv := nsc.First(); nsk != nil; nsk, nsv = nsc.Next() {
|
|
if nsv != nil {
|
|
continue
|
|
}
|
|
mntsbkt := v1bkt.Bucket(nsk).Bucket(bucketKeyMounts)
|
|
if mntsbkt == nil {
|
|
continue
|
|
}
|
|
mc := mntsbkt.Cursor()
|
|
for mk, mv := mc.First(); mk != nil; mk, mv = mc.Next() {
|
|
if mv != nil {
|
|
continue
|
|
}
|
|
fn(gc.Node{
|
|
Type: metadata.ResourceMount,
|
|
Namespace: string(nsk),
|
|
Key: string(mk),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func gcnode(t gc.ResourceType, ns, key string) gc.Node {
|
|
return gc.Node{
|
|
Type: t,
|
|
Namespace: ns,
|
|
Key: key,
|
|
}
|
|
}
|
|
|
|
func (cc *collectionContext) ActiveWithBackRefs(ns string, fn func(gc.Node), bref func(gc.Node, gc.Node)) {
|
|
nsbkt := getBucket(cc.tx, []byte("v1"), []byte(ns), bucketKeyMounts)
|
|
if nsbkt != nil {
|
|
mc := nsbkt.Cursor()
|
|
for mk, mv := mc.First(); mk != nil; mk, mv = mc.Next() {
|
|
if mv != nil {
|
|
continue
|
|
}
|
|
n := gcnode(metadata.ResourceMount, ns, string(mk))
|
|
lbkt := nsbkt.Bucket(mk).Bucket(bucketKeyLabels)
|
|
if lbkt != nil {
|
|
lc := lbkt.Cursor()
|
|
for _, h := range []struct {
|
|
key []byte
|
|
handler func([]byte, []byte)
|
|
}{
|
|
{
|
|
key: labelGCContainerBackRef,
|
|
handler: func(k, v []byte) {
|
|
if ks := string(k); ks != string(labelGCContainerBackRef) {
|
|
// Allow reference naming separated by . or /, ignore names
|
|
if ks[len(labelGCContainerBackRef)] != '.' && ks[len(labelGCContainerBackRef)] != '/' {
|
|
return
|
|
}
|
|
}
|
|
|
|
bref(gcnode(metadata.ResourceContainer, ns, string(v)), n)
|
|
},
|
|
},
|
|
{
|
|
key: labelGCContentBackRef,
|
|
handler: func(k, v []byte) {
|
|
if ks := string(k); ks != string(labelGCContentBackRef) {
|
|
// Allow reference naming separated by . or /, ignore names
|
|
if ks[len(labelGCContentBackRef)] != '.' && ks[len(labelGCContentBackRef)] != '/' {
|
|
return
|
|
}
|
|
}
|
|
|
|
bref(gcnode(metadata.ResourceContent, ns, string(v)), n)
|
|
},
|
|
},
|
|
{
|
|
key: labelGCImageBackRef,
|
|
handler: func(k, v []byte) {
|
|
if ks := string(k); ks != string(labelGCImageBackRef) {
|
|
// Allow reference naming separated by . or /, ignore names
|
|
if ks[len(labelGCImageBackRef)] != '.' && ks[len(labelGCImageBackRef)] != '/' {
|
|
return
|
|
}
|
|
}
|
|
|
|
bref(gcnode(metadata.ResourceImage, ns, string(v)), n)
|
|
},
|
|
},
|
|
{
|
|
key: labelGCSnapBackRef,
|
|
handler: func(k, v []byte) {
|
|
snapshotter := k[len(labelGCSnapBackRef):]
|
|
if i := bytes.IndexByte(snapshotter, '/'); i >= 0 {
|
|
snapshotter = snapshotter[:i]
|
|
}
|
|
bref(gcnode(metadata.ResourceSnapshot, ns, fmt.Sprintf("%s/%s", snapshotter, v)), n)
|
|
},
|
|
},
|
|
// TODO: Consider support for root/expire labels
|
|
} {
|
|
for k, v := lc.Seek(h.key); k != nil && bytes.HasPrefix(k, h.key); k, v = lc.Next() {
|
|
h.handler(k, v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cc *collectionContext) Active(ns string, fn func(gc.Node)) {
|
|
cc.ActiveWithBackRefs(ns, fn, func(gc.Node, gc.Node) {})
|
|
}
|
|
|
|
func (cc *collectionContext) Leased(ns, lease string, fn func(gc.Node)) {
|
|
bkt := getBucket(cc.tx, []byte("v1"), []byte(ns), []byte("leases"), []byte(lease))
|
|
if bkt != nil {
|
|
c := bkt.Cursor()
|
|
for k, _ := c.First(); k != nil; k, _ = c.Next() {
|
|
fn(gc.Node{
|
|
Type: metadata.ResourceMount,
|
|
Namespace: ns,
|
|
Key: string(k),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cc *collectionContext) Remove(n gc.Node) {
|
|
log.G(cc.ctx).WithFields(log.Fields{"namespace": n.Namespace, "name": n.Key}).Debugf("remove mount")
|
|
if n.Type != metadata.ResourceMount {
|
|
return
|
|
}
|
|
nmap, ok := cc.removed[n.Namespace]
|
|
if ok {
|
|
if _, ok = nmap[n.Key]; !ok {
|
|
nmap[n.Key] = struct{}{}
|
|
}
|
|
} else {
|
|
cc.removed[n.Namespace] = map[string]struct{}{
|
|
n.Key: {},
|
|
}
|
|
}
|
|
}
|
|
|
|
func (cc *collectionContext) Cancel() (err error) {
|
|
err = cc.tx.Rollback()
|
|
cc.manager.rwlock.Unlock()
|
|
return
|
|
}
|
|
|
|
func (cc *collectionContext) Finish() error {
|
|
// TODO: Get list of all remaining
|
|
remaining, err := cc.applyRemove()
|
|
if err != nil {
|
|
if rerr := cc.tx.Rollback(); rerr != nil {
|
|
err = errors.Join(err, rerr)
|
|
}
|
|
} else {
|
|
err = cc.tx.Commit()
|
|
}
|
|
if err != nil {
|
|
cc.manager.rwlock.Unlock()
|
|
return err
|
|
}
|
|
|
|
// TODO: Consider using unmount q
|
|
cleanup, err := cc.getCleanupDirectories(remaining)
|
|
|
|
cc.manager.rwlock.Unlock()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return cleanupAll(cc.ctx, cleanup, cc.manager.handlers)
|
|
}
|
|
|
|
func (cc *collectionContext) applyRemove() (map[uint64]struct{}, error) {
|
|
remaining := map[uint64]struct{}{}
|
|
v1bkt := cc.tx.Bucket([]byte("v1"))
|
|
if v1bkt == nil {
|
|
return remaining, nil
|
|
}
|
|
nsc := v1bkt.Cursor()
|
|
for nsk, nsv := nsc.First(); nsk != nil; nsk, nsv = nsc.Next() {
|
|
if nsv != nil {
|
|
continue
|
|
}
|
|
removed := cc.removed[string(nsk)]
|
|
nsbkt := v1bkt.Bucket(nsk)
|
|
msbkt := nsbkt.Bucket(bucketKeyMounts)
|
|
if msbkt == nil {
|
|
continue
|
|
}
|
|
lsbkt := nsbkt.Bucket(bucketKeyLeases)
|
|
msc := msbkt.Cursor()
|
|
for msk, msv := msc.First(); msk != nil; msk, msv = msc.Next() {
|
|
if msv != nil {
|
|
continue
|
|
}
|
|
mbkt := msbkt.Bucket(msk)
|
|
var remove bool
|
|
if removed != nil {
|
|
_, remove = removed[string(msk)]
|
|
}
|
|
|
|
if remove {
|
|
if lsbkt != nil {
|
|
lid := mbkt.Get(bucketKeyLease)
|
|
if len(lid) > 0 {
|
|
lbkt := lsbkt.Bucket(lid)
|
|
if lbkt != nil {
|
|
lbkt.Delete(msk)
|
|
if k, _ := lbkt.Cursor().First(); k == nil {
|
|
lsbkt.DeleteBucket(lid)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
msbkt.DeleteBucket(msk)
|
|
} else {
|
|
remaining[readID(mbkt)] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
return remaining, nil
|
|
}
|
|
|
|
func (cc *collectionContext) getCleanupDirectories(remaining map[uint64]struct{}) ([]string, error) {
|
|
fd, err := cc.manager.targets.Open(".")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fd.Close()
|
|
|
|
dirs, err := fd.Readdirnames(0)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cleanup := []string{}
|
|
for _, d := range dirs {
|
|
id, err := strconv.ParseUint(d, 10, 64)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if _, ok := remaining[id]; ok {
|
|
continue
|
|
}
|
|
cleanup = append(cleanup, filepath.Join(cc.manager.targets.Name(), d))
|
|
}
|
|
|
|
return cleanup, nil
|
|
}
|
|
|
|
func cleanupAll(ctx context.Context, roots []string, handlers map[string]mount.Handler) error {
|
|
var errs []error
|
|
for _, root := range roots {
|
|
if err := unmountAll(ctx, root, handlers); err != nil {
|
|
errs = append(errs, fmt.Errorf("unmount all failed during cleanup up %s: %w", root, err))
|
|
} else {
|
|
log.G(ctx).WithField("root", root).Debugf("unmounted")
|
|
}
|
|
}
|
|
return errors.Join(errs...)
|
|
}
|
|
|
|
func unmountAll(ctx context.Context, root string, handlers map[string]mount.Handler) error {
|
|
fd, err := os.Open(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
dirs, err := fd.Readdirnames(0)
|
|
fd.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var mountErrs []error
|
|
for i := len(dirs) - 1; i >= 0; {
|
|
var (
|
|
d = dirs[i]
|
|
mp string
|
|
h mount.Handler
|
|
)
|
|
i--
|
|
|
|
if strings.HasSuffix(d, "-type") {
|
|
name := d[:len(d)-5]
|
|
if i >= 0 && dirs[i] == name {
|
|
i--
|
|
}
|
|
if b, rerr := os.ReadFile(filepath.Join(root, d)); rerr == nil {
|
|
h = handlers[string(b)]
|
|
} else {
|
|
return rerr
|
|
}
|
|
mp = filepath.Join(root, name)
|
|
} else {
|
|
mp = filepath.Join(root, d)
|
|
// If type file exists, continue and try again with "-type" file
|
|
if _, serr := os.Stat(mp + "-type"); serr == nil {
|
|
continue
|
|
} else if !os.IsNotExist(serr) {
|
|
return serr
|
|
} else {
|
|
log.G(ctx).WithField("mount", d).Infof("missing type file, attempting unmount with no handler")
|
|
}
|
|
}
|
|
|
|
if h != nil {
|
|
err = h.Unmount(ctx, mp)
|
|
} else {
|
|
err = mount.Unmount(mp, 0)
|
|
}
|
|
if err != nil {
|
|
// TODO: Ignore already unmounted
|
|
mountErrs = append(mountErrs, fmt.Errorf("failure unmounting %s: %w", d, err))
|
|
}
|
|
}
|
|
if len(mountErrs) > 0 {
|
|
return errors.Join(mountErrs...)
|
|
}
|
|
|
|
return os.RemoveAll(root)
|
|
}
|