Skip to content

Commit 5f14dc3

Browse files
committed
libpod: defer volume plugin init when loading volumes from DB
Add e2e regression tests for refresh and lazy plugin use paths. Signed-off-by: Yoonseo Han <yooncer00@gmail.com>
1 parent 9e13b9e commit 5f14dc3

8 files changed

Lines changed: 134 additions & 57 deletions

libpod/boltdb_state_internal.go

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import (
77
"os"
88
"sort"
99
"strings"
10+
"sync"
1011

1112
"github.com/sirupsen/logrus"
1213
bolt "go.etcd.io/bbolt"
1314
"go.podman.io/common/libnetwork/types"
1415
"go.podman.io/podman/v6/libpod/define"
16+
"go.podman.io/podman/v6/libpod/plugin"
1517
)
1618

1719
const (
@@ -297,21 +299,12 @@ func (s *BoltState) getVolumeFromDB(name []byte, volume *Volume, volBkt *bolt.Bu
297299

298300
// Need this for UsesVolumeDriver() so set it now.
299301
volume.runtime = s.runtime
300-
301-
// Retrieve volume driver
302-
if volume.UsesVolumeDriver() {
303-
plugin, err := s.runtime.getVolumePlugin(volume.config)
304-
if err != nil {
305-
// We want to fail gracefully here, to ensure that we
306-
// can still remove volumes even if their plugin is
307-
// missing. Otherwise, we end up with volumes that
308-
// cannot even be retrieved from the database and will
309-
// cause things like `volume ls` to fail.
310-
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)
311-
} else {
312-
volume.plugin = plugin
302+
volume.volumePlugin = sync.OnceValues(func() (*plugin.VolumePlugin, error) {
303+
if !volume.UsesVolumeDriver() {
304+
return nil, nil
313305
}
314-
}
306+
return volume.runtime.getVolumePlugin(volume.config)
307+
})
315308

316309
// Get the lock
317310
lock, err := s.runtime.lockManager.RetrieveLock(volume.config.LockID)

libpod/runtime_volume_common.go

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,14 @@ func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, opti
7070
}
7171
}
7272

73-
// Plugin can be nil if driver is local, but that's OK - superfluous
74-
// assignment doesn't hurt much.
75-
plugin, err := r.getVolumePlugin(volume.config)
76-
if err != nil {
77-
return nil, fmt.Errorf("volume %s uses volume plugin %s but it could not be retrieved: %w", volume.config.Name, volume.config.Driver, err)
73+
var plugin *volplugin.VolumePlugin
74+
if volume.UsesVolumeDriver() {
75+
var err error
76+
plugin, err = volume.volumePlugin()
77+
if err != nil {
78+
return nil, fmt.Errorf("volume %s uses volume plugin %s but it could not be retrieved: %w", volume.config.Name, volume.config.Driver, err)
79+
}
7880
}
79-
volume.plugin = plugin
8081

8182
if volume.config.Driver == define.VolumeDriverLocal {
8283
logrus.Debugf("Validating options for local driver")
@@ -150,13 +151,13 @@ func (r *Runtime) newVolume(ctx context.Context, noCreatePluginVolume bool, opti
150151

151152
// Now we get conditional: we either need to make the volume in the
152153
// volume plugin, or on disk if not using a plugin.
153-
if volume.plugin != nil && !noCreatePluginVolume {
154+
if volume.UsesVolumeDriver() && !noCreatePluginVolume {
154155
// We can't chown, or relabel, or similar the path the volume is
155156
// using, because it's not managed by us.
156157
// TODO: reevaluate this once we actually have volume plugins in
157158
// use in production - it may be safe, but I can't tell without
158159
// knowing what the actual plugin does...
159-
if err := makeVolumeInPluginIfNotExist(volume.config.Name, volume.config.Options, volume.plugin); err != nil {
160+
if err := makeVolumeInPluginIfNotExist(volume.config.Name, volume.config.Options, plugin); err != nil {
160161
return nil, err
161162
}
162163
} else {
@@ -437,27 +438,22 @@ func (r *Runtime) removeVolume(ctx context.Context, v *Volume, force bool, timeo
437438
if v.UsesVolumeDriver() && !ignoreVolumePlugin {
438439
canRemove := true
439440

440-
// Do we have a volume driver?
441-
if v.plugin == nil {
441+
plugin, err := v.volumePlugin()
442+
if err != nil {
442443
canRemove = false
443-
removalErr = fmt.Errorf("cannot remove volume %s from plugin %s, but it has been removed from Podman: %w", v.Name(), v.Driver(), define.ErrMissingPlugin)
444+
removalErr = fmt.Errorf("cannot remove volume %s from plugin %s, but it has been removed from Podman: %w", v.Name(), v.Driver(), err)
444445
} else {
445-
// Ping the plugin first to verify the volume still
446-
// exists.
447-
// We're trying to be very tolerant of missing volumes
448-
// in the backend, to avoid the problems we see with
449-
// sync between c/storage and the Libpod DB.
450446
getReq := new(pluginapi.GetRequest)
451447
getReq.Name = v.Name()
452-
if _, err := v.plugin.GetVolume(getReq); err != nil {
448+
if _, err := plugin.GetVolume(getReq); err != nil {
453449
canRemove = false
454450
removalErr = fmt.Errorf("volume %s could not be retrieved from plugin %s, but it has been removed from Podman: %w", v.Name(), v.Driver(), err)
455451
}
456452
}
457453
if canRemove {
458454
req := new(pluginapi.RemoveRequest)
459455
req.Name = v.Name()
460-
if err := v.plugin.RemoveVolume(req); err != nil {
456+
if err := plugin.RemoveVolume(req); err != nil {
461457
return fmt.Errorf("volume %s could not be removed from plugin %s: %w", v.Name(), v.Driver(), err)
462458
}
463459
}

libpod/sqlite_state_internal.go

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ import (
1111
"slices"
1212
"sort"
1313
"strings"
14+
"sync"
1415

1516
"github.com/sirupsen/logrus"
1617
"go.podman.io/common/libnetwork/types"
1718
"go.podman.io/podman/v6/libpod/define"
19+
"go.podman.io/podman/v6/libpod/plugin"
1820
"go.podman.io/storage/pkg/fileutils"
1921

2022
// SQLite backend for database/sql
@@ -350,22 +352,13 @@ func finalizeVolumeSqlite(vol *Volume) error {
350352
}
351353
vol.lock = lock
352354

353-
// Retrieve volume driver
354-
if vol.UsesVolumeDriver() {
355-
plugin, err := vol.runtime.getVolumePlugin(vol.config)
356-
if err != nil {
357-
// We want to fail gracefully here, to ensure that we
358-
// can still remove volumes even if their plugin is
359-
// missing. Otherwise, we end up with volumes that
360-
// cannot even be retrieved from the database and will
361-
// cause things like `volume ls` to fail.
362-
logrus.Errorf("Volume %s uses volume plugin %s, but it cannot be accessed - some functionality may not be available: %v", vol.Name(), vol.config.Driver, err)
363-
} else {
364-
vol.plugin = plugin
365-
}
366-
}
367-
368355
vol.valid = true
356+
vol.volumePlugin = sync.OnceValues(func() (*plugin.VolumePlugin, error) {
357+
if !vol.UsesVolumeDriver() {
358+
return nil, nil
359+
}
360+
return vol.runtime.getVolumePlugin(vol.config)
361+
})
369362

370363
return nil
371364
}

libpod/volume.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ type Volume struct {
2727

2828
ignoreIfExists bool
2929
valid bool
30-
plugin *plugin.VolumePlugin
30+
volumePlugin func() (*plugin.VolumePlugin, error)
3131
runtime *Runtime
3232
lock lock.Locker
3333
}

libpod/volume_inspect.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ func (v *Volume) Inspect() (*define.InspectVolumeData, error) {
3232
logrus.Debugf("Querying volume plugin %s for status", v.config.Driver)
3333
data.Mountpoint = v.state.MountPoint
3434

35-
if v.plugin == nil {
36-
return nil, fmt.Errorf("volume %s uses volume plugin %s but it is not available, cannot inspect: %w", v.Name(), v.config.Driver, define.ErrMissingPlugin)
35+
plugin, err := v.volumePlugin()
36+
if err != nil {
37+
return nil, fmt.Errorf("volume %s uses volume plugin %s but it is not available, cannot inspect: %w", v.Name(), v.config.Driver, err)
3738
}
3839

3940
// Retrieve status for the volume.
4041
// Need to query the volume driver.
4142
req := new(pluginapi.GetRequest)
4243
req.Name = v.Name()
43-
resp, err := v.plugin.GetVolume(req)
44+
resp, err := plugin.GetVolume(req)
4445
if err != nil {
4546
return nil, fmt.Errorf("retrieving volume %s information from plugin %s: %w", v.Name(), v.Driver(), err)
4647
}

libpod/volume_internal.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"fmt"
77
"os"
88
"path/filepath"
9+
"sync"
910

1011
"go.podman.io/podman/v6/libpod/define"
12+
"go.podman.io/podman/v6/libpod/plugin"
1113
)
1214

1315
// Creates a new volume
@@ -20,6 +22,12 @@ func newVolume(runtime *Runtime) *Volume {
2022
volume.config.Options = make(map[string]string)
2123
volume.state.NeedsCopyUp = true
2224
volume.state.NeedsChown = true
25+
volume.volumePlugin = sync.OnceValues(func() (*plugin.VolumePlugin, error) {
26+
if !volume.UsesVolumeDriver() {
27+
return nil, nil
28+
}
29+
return volume.runtime.getVolumePlugin(volume.config)
30+
})
2331
return volume
2432
}
2533

libpod/volume_internal_common.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,14 +50,15 @@ func (v *Volume) mount() error {
5050
// ours more. So hardcode container ID to something reasonable, and use
5151
// the same one for everything.
5252
if v.UsesVolumeDriver() {
53-
if v.plugin == nil {
54-
return fmt.Errorf("volume plugin %s (needed by volume %s) missing: %w", v.Driver(), v.Name(), define.ErrMissingPlugin)
53+
plugin, err := v.volumePlugin()
54+
if err != nil {
55+
return fmt.Errorf("volume plugin %s (needed by volume %s) missing: %w", v.Driver(), v.Name(), err)
5556
}
5657

5758
req := new(pluginapi.MountRequest)
5859
req.Name = v.Name()
5960
req.ID = pseudoCtrID
60-
mountPoint, err := v.plugin.MountVolume(req)
61+
mountPoint, err := plugin.MountVolume(req)
6162
if err != nil {
6263
return err
6364
}
@@ -156,14 +157,15 @@ func (v *Volume) unmount(force bool) error {
156157

157158
if v.state.MountCount == 0 {
158159
if v.UsesVolumeDriver() {
159-
if v.plugin == nil {
160-
return fmt.Errorf("volume plugin %s (needed by volume %s) missing: %w", v.Driver(), v.Name(), define.ErrMissingPlugin)
160+
plugin, err := v.volumePlugin()
161+
if err != nil {
162+
return fmt.Errorf("volume plugin %s (needed by volume %s) missing: %w", v.Driver(), v.Name(), err)
161163
}
162164

163165
req := new(pluginapi.UnmountRequest)
164166
req.Name = v.Name()
165167
req.ID = pseudoCtrID
166-
if err := v.plugin.UnmountVolume(req); err != nil {
168+
if err := plugin.UnmountVolume(req); err != nil {
167169
return err
168170
}
169171

test/e2e/volume_plugin_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,4 +301,88 @@ Removed:
301301
Expect(volInspect2).Should(ExitCleanly())
302302
Expect(volInspect2.OutputToString()).To(ContainSubstring("3"))
303303
})
304+
305+
It("unrelated command after refresh does not log plugin errors for plugin volumes", func() {
306+
podmanTest.AddImageToRWStore(volumeTest)
307+
308+
pluginStatePath := filepath.Join(podmanTest.TempDir, "volumes")
309+
err := os.Mkdir(pluginStatePath, 0o755)
310+
Expect(err).ToNot(HaveOccurred())
311+
312+
pluginName := "testvol7"
313+
ctrName := "pluginCtrRefresh"
314+
podmanTest.PodmanExitCleanly(
315+
"run", "--name", ctrName, "--security-opt", "label=disable",
316+
"-v", "/run/docker/plugins:/run/docker/plugins",
317+
"-v", fmt.Sprintf("%v:%v", pluginStatePath, pluginStatePath),
318+
"-d", volumeTest, "--sock-name", pluginName, "--path", pluginStatePath,
319+
)
320+
321+
err = WaitForFile(fmt.Sprintf("/run/docker/plugins/%s.sock", pluginName))
322+
Expect(err).ToNot(HaveOccurred())
323+
324+
vol1 := "refreshVol1-" + stringid.GenerateRandomID()
325+
vol2 := "refreshVol2-" + stringid.GenerateRandomID()
326+
podmanTest.PodmanExitCleanly("volume", "create", "--driver", pluginName, vol1)
327+
podmanTest.PodmanExitCleanly("volume", "create", "--driver", pluginName, vol2)
328+
329+
podmanTest.StopContainer(ctrName)
330+
331+
// Simulate post-reboot: tmp state is gone but RunRoot (volume DB) persists.
332+
// Use a fresh tmpdir instead of deleting the alive file from setup.
333+
refreshTmp := filepath.Join(podmanTest.TempDir, "refresh-tmp")
334+
err = os.MkdirAll(refreshTmp, 0o755)
335+
Expect(err).ToNot(HaveOccurred())
336+
337+
// Unrelated command triggers refresh; must succeed with no stderr.
338+
podmanTest.PodmanExitCleanly("--log-level=error", "--tmpdir", refreshTmp, "version")
339+
340+
for _, vol := range []string{vol1, vol2} {
341+
remove := podmanTest.Podman([]string{"volume", "rm", vol})
342+
remove.WaitWithDefaultTimeout()
343+
Expect(remove).To(ExitWithErrorRegex(125, "has been removed from Podman"))
344+
}
345+
ls := podmanTest.PodmanExitCleanly("volume", "ls", "-q")
346+
Expect(ls.OutputToStringArray()).To(BeEmpty())
347+
})
348+
349+
It("volume inspect and ls with stopped plugin fail without finalize spam", func() {
350+
podmanTest.AddImageToRWStore(volumeTest)
351+
352+
pluginStatePath := filepath.Join(podmanTest.TempDir, "volumes")
353+
err := os.Mkdir(pluginStatePath, 0o755)
354+
Expect(err).ToNot(HaveOccurred())
355+
356+
pluginName := "testvol8"
357+
ctrName := "pluginCtrInspect"
358+
podmanTest.PodmanExitCleanly(
359+
"run", "--name", ctrName, "--security-opt", "label=disable",
360+
"-v", "/run/docker/plugins:/run/docker/plugins",
361+
"-v", fmt.Sprintf("%v:%v", pluginStatePath, pluginStatePath),
362+
"-d", volumeTest, "--sock-name", pluginName, "--path", pluginStatePath,
363+
)
364+
365+
err = WaitForFile(fmt.Sprintf("/run/docker/plugins/%s.sock", pluginName))
366+
Expect(err).ToNot(HaveOccurred())
367+
368+
volName := "inspectVol-" + stringid.GenerateRandomID()
369+
podmanTest.PodmanExitCleanly("volume", "create", "--driver", pluginName, volName)
370+
371+
podmanTest.StopContainer(ctrName)
372+
373+
inspect := podmanTest.Podman([]string{"volume", "inspect", volName})
374+
inspect.WaitWithDefaultTimeout()
375+
Expect(inspect).To(ExitWithErrorRegex(125, "cannot inspect"))
376+
377+
ls := podmanTest.Podman([]string{"volume", "ls", "-q"})
378+
ls.WaitWithDefaultTimeout()
379+
Expect(ls).To(ExitWithErrorRegex(125, "cannot inspect"))
380+
381+
remove := podmanTest.Podman([]string{"volume", "rm", volName})
382+
remove.WaitWithDefaultTimeout()
383+
Expect(remove).To(ExitWithErrorRegex(125, "has been removed from Podman"))
384+
385+
lsAfter := podmanTest.PodmanExitCleanly("volume", "ls", "-q")
386+
Expect(lsAfter.OutputToStringArray()).To(BeEmpty())
387+
})
304388
})

0 commit comments

Comments
 (0)