Skip to content

Commit 2d29792

Browse files
committed
Prevent concurrency on commit and remove
Some container operations require exclusive access to the container, and/or have no elegant way to deal with concurrent container removal. See for example: containerd#1391 This PR adds a locking method that containers can use, backed by a store instance inside the container statedir, and enforces exclusive locking for `Commit` and `Remove`. There may be other container operations where we would like to apply the lock as well (to be evaluated). Finally note that this locking mechanism is to prevent another instance of nerdctl from executing concurrently, and does not offer guarantees that the container will not be manipulated by a different cli or otherwise change state on its own. Signed-off-by: apostasie <[email protected]>
1 parent a40889b commit 2d29792

File tree

5 files changed

+84
-30
lines changed

5 files changed

+84
-30
lines changed

cmd/nerdctl/container/container_remove_linux_test.go renamed to cmd/nerdctl/container/container_remove_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ import (
2424

2525
func TestRemoveContainer(t *testing.T) {
2626
t.Parallel()
27+
2728
base := testutil.NewBase(t)
2829
tID := testutil.Identifier(t)
2930

3031
// ignore error
3132
base.Cmd("rm", tID, "-f").AssertOK()
3233

33-
base.Cmd("run", "-d", "--name", tID, testutil.CommonImage, "sleep", "infinity").AssertOK()
34+
base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK()
3435
defer base.Cmd("rm", tID, "-f").AssertOK()
3536
base.Cmd("rm", tID).AssertFail()
3637

37-
base.Cmd("kill", tID).AssertOK()
38+
// `kill` does return before the container actually stops
39+
base.Cmd("stop", tID).AssertOK()
3840
base.Cmd("rm", tID).AssertOK()
3941
}

cmd/nerdctl/container/container_remove_windows_test.go

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,6 @@ import (
2424
"github.com/containerd/nerdctl/v2/pkg/testutil"
2525
)
2626

27-
func TestRemoveProcessContainer(t *testing.T) {
28-
base := testutil.NewBase(t)
29-
tID := testutil.Identifier(t)
30-
31-
// ignore error
32-
base.Cmd("rm", tID, "-f").AssertOK()
33-
34-
base.Cmd("run", "-d", "--name", tID, testutil.NginxAlpineImage).AssertOK()
35-
defer base.Cmd("rm", tID, "-f").AssertOK()
36-
base.Cmd("rm", tID).AssertFail()
37-
38-
base.Cmd("kill", tID).AssertOK()
39-
base.Cmd("rm", tID).AssertOK()
40-
}
41-
4227
func TestRemoveHyperVContainer(t *testing.T) {
4328
base := testutil.NewBase(t)
4429
tID := testutil.Identifier(t)

pkg/cmd/container/remove.go

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -101,23 +101,37 @@ func Remove(ctx context.Context, client *containerd.Client, containers []string,
101101
// - then and ONLY then, on a successful container remove, clean things-up on our side (volume store, etcetera)
102102
// If you do need to add more cleanup, please do so at the bottom of the defer function
103103
func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions types.GlobalCommandOptions, force bool, removeAnonVolumes bool, client *containerd.Client) (retErr error) {
104-
// defer the storage of remove error in the dedicated label
104+
// Get labels
105+
containerLabels, err := c.Labels(ctx)
106+
if err != nil {
107+
return err
108+
}
109+
110+
// Lock the container state
111+
lf, err := containerutil.Lock(ctx, c)
112+
if err != nil {
113+
return err
114+
}
115+
105116
defer func() {
117+
// If there was an error, update the label
118+
// Note that we will (obviously) not store any unlocking or statedir removal error from below
106119
if retErr != nil {
107120
containerutil.UpdateErrorLabel(ctx, c, retErr)
108121
}
122+
// Release the lock
123+
retErr = errors.Join(lf.Release(), retErr)
124+
// Note: technically, this is racy...
125+
if retErr == nil {
126+
retErr = os.RemoveAll(containerLabels[labels.StateDir])
127+
}
109128
}()
110129

111130
// Get namespace
112131
containerNamespace, err := namespaces.NamespaceRequired(ctx)
113132
if err != nil {
114133
return err
115134
}
116-
// Get labels
117-
containerLabels, err := c.Labels(ctx)
118-
if err != nil {
119-
return err
120-
}
121135
// Get datastore
122136
dataStore, err := clientutil.DataStore(globalOptions.DataRoot, globalOptions.Address)
123137
if err != nil {
@@ -139,9 +153,8 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions
139153
return err
140154
}
141155

142-
// Get the container id, stateDir and name
156+
// Get the container id and name
143157
id := c.ID()
144-
stateDir := containerLabels[labels.StateDir]
145158
name := containerLabels[labels.Name]
146159

147160
// This will evaluate retErr to decide if we proceed with removal or not
@@ -198,11 +211,6 @@ func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions
198211
log.G(ctx).WithError(err).Warnf("failed to cleanup IPC for container %q", id)
199212
}
200213

201-
// Remove state dir - soft failure
202-
if err = os.RemoveAll(stateDir); err != nil {
203-
log.G(ctx).WithError(err).Warnf("failed to remove container state dir %s", stateDir)
204-
}
205-
206214
// Enforce release name here in case the poststop hook name release fails - soft failure
207215
if name != "" {
208216
// Double-releasing may happen with containers started with --rm, so, ignore NotFound errors

pkg/containerutil/lock.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package containerutil
18+
19+
import (
20+
"context"
21+
"errors"
22+
"path/filepath"
23+
24+
"github.com/containerd/containerd/v2/client"
25+
26+
"github.com/containerd/nerdctl/v2/pkg/labels"
27+
"github.com/containerd/nerdctl/v2/pkg/store"
28+
)
29+
30+
func Lock(ctx context.Context, c client.Container) (store.Store, error) {
31+
containerLabels, err := c.Labels(ctx)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
stateDir := containerLabels[labels.StateDir]
37+
if stateDir == "" {
38+
return nil, errors.New("container is missing statedir label")
39+
}
40+
41+
stor, err := store.New(filepath.Join(stateDir, "oplock"), 0, 0)
42+
if err != nil {
43+
return nil, err
44+
}
45+
46+
err = stor.Lock()
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return stor, nil
52+
}

pkg/imgutil/commit/commit.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import (
4444
"github.com/containerd/log"
4545
"github.com/containerd/platforms"
4646

47+
"github.com/containerd/nerdctl/v2/pkg/containerutil"
4748
imgutil "github.com/containerd/nerdctl/v2/pkg/imgutil"
4849
"github.com/containerd/nerdctl/v2/pkg/labels"
4950
)
@@ -66,6 +67,12 @@ var (
6667
)
6768

6869
func Commit(ctx context.Context, client *containerd.Client, container containerd.Container, opts *Opts) (digest.Digest, error) {
70+
lf, err := containerutil.Lock(ctx, container)
71+
if err != nil {
72+
return emptyDigest, err
73+
}
74+
defer lf.Release()
75+
6976
id := container.ID()
7077
info, err := container.Info(ctx)
7178
if err != nil {

0 commit comments

Comments
 (0)