Skip to content

Commit c1614f0

Browse files
authored
Merge pull request containerd#3446 from apostasie/dev-3440-locking
Prevent concurrency on commit and remove
2 parents 772feb4 + 2d29792 commit c1614f0

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)