Skip to content

Commit 7cbdedd

Browse files
committed
feat: create network cleanup function for kill and stop cmd
Signed-off-by: Alessio Greggi <[email protected]>
1 parent 18f29fb commit 7cbdedd

File tree

6 files changed

+309
-90
lines changed

6 files changed

+309
-90
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
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 main
18+
19+
import (
20+
"fmt"
21+
"strings"
22+
"testing"
23+
24+
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
25+
"github.com/containerd/nerdctl/v2/pkg/testutil"
26+
iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables"
27+
"github.com/coreos/go-iptables/iptables"
28+
"gotest.tools/v3/assert"
29+
)
30+
31+
// TestKillCleanupForwards runs a container that exposes a port and then kill it.
32+
// The test checks that the kill command effectively clean up
33+
// the iptables forwards creted from the run.
34+
func TestKillCleanupForwards(t *testing.T) {
35+
const (
36+
hostPort = 9999
37+
testContainerName = "ngx"
38+
)
39+
base := testutil.NewBase(t)
40+
defer func() {
41+
base.Cmd("rm", "-f", testContainerName).Run()
42+
}()
43+
44+
// skip if rootless
45+
if rootlessutil.IsRootless() {
46+
t.Skip("pkg/testutil/iptables does not support rootless")
47+
}
48+
49+
ipt, err := iptables.New()
50+
assert.NilError(t, err)
51+
52+
cmd := base.Cmd("run", "-d",
53+
"--restart=no",
54+
"--name", testContainerName,
55+
"-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort),
56+
testutil.NginxAlpineImage)
57+
containerID := strings.TrimSpace(cmd.Run().Stdout())
58+
assert.Equal(t, iptablesutil.ForwardExists(t, ipt, testutil.Namespace, containerID), true)
59+
60+
base.Cmd("kill", testContainerName).AssertOK()
61+
assert.Equal(t, iptablesutil.ForwardExists(t, ipt, testutil.Namespace, containerID), false)
62+
}

cmd/nerdctl/container_stop_linux_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@ import (
2222
"strings"
2323
"testing"
2424

25+
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
2526
"github.com/containerd/nerdctl/v2/pkg/testutil"
27+
iptablesutil "github.com/containerd/nerdctl/v2/pkg/testutil/iptables"
2628
"github.com/containerd/nerdctl/v2/pkg/testutil/nettestutil"
29+
"github.com/coreos/go-iptables/iptables"
2730

2831
"gotest.tools/v3/assert"
2932
)
@@ -86,3 +89,33 @@ echo "signal quit"`).AssertOK()
8689
base.Cmd("stop", testContainerName).AssertOK()
8790
base.Cmd("logs", "-f", testContainerName).AssertOutContains("signal quit")
8891
}
92+
93+
func TestStopCleanupForwards(t *testing.T) {
94+
const (
95+
hostPort = 9999
96+
testContainerName = "ngx"
97+
)
98+
base := testutil.NewBase(t)
99+
defer func() {
100+
base.Cmd("rm", "-f", testContainerName).Run()
101+
}()
102+
103+
// skip if rootless
104+
if rootlessutil.IsRootless() {
105+
t.Skip("pkg/testutil/iptables does not support rootless")
106+
}
107+
108+
ipt, err := iptables.New()
109+
assert.NilError(t, err)
110+
111+
cmd := base.Cmd("run", "-d",
112+
"--restart=no",
113+
"--name", testContainerName,
114+
"-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort),
115+
testutil.NginxAlpineImage)
116+
containerID := strings.TrimSpace(cmd.Run().Stdout())
117+
assert.Equal(t, iptablesutil.ForwardExists(t, ipt, testutil.Namespace, containerID), true)
118+
119+
base.Cmd("stop", testContainerName).AssertOK()
120+
assert.Equal(t, iptablesutil.ForwardExists(t, ipt, testutil.Namespace, containerID), false)
121+
}

pkg/cmd/container/kill.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package container
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"os"
2324
"strings"
@@ -26,10 +27,15 @@ import (
2627
"github.com/containerd/containerd"
2728
"github.com/containerd/containerd/cio"
2829
"github.com/containerd/containerd/errdefs"
30+
gocni "github.com/containerd/go-cni"
2931
"github.com/containerd/log"
3032
"github.com/containerd/nerdctl/v2/pkg/api/types"
3133
"github.com/containerd/nerdctl/v2/pkg/containerutil"
3234
"github.com/containerd/nerdctl/v2/pkg/idutil/containerwalker"
35+
"github.com/containerd/nerdctl/v2/pkg/labels"
36+
"github.com/containerd/nerdctl/v2/pkg/netutil"
37+
"github.com/containerd/nerdctl/v2/pkg/netutil/nettype"
38+
"github.com/containerd/nerdctl/v2/pkg/portutil"
3339
"github.com/moby/sys/signal"
3440
)
3541

@@ -50,6 +56,9 @@ func Kill(ctx context.Context, client *containerd.Client, reqs []string, options
5056
if found.MatchCount > 1 {
5157
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
5258
}
59+
if err := cleanupNetwork(ctx, found.Container, options.GOptions); err != nil {
60+
return fmt.Errorf("unable to cleanup network for container: %s, %q", found.Req, err)
61+
}
5362
if err := killContainer(ctx, found.Container, parsedSignal); err != nil {
5463
if errdefs.IsNotFound(err) {
5564
fmt.Fprintf(options.Stderr, "No such container: %s\n", found.Req)
@@ -106,3 +115,75 @@ func killContainer(ctx context.Context, container containerd.Container, signal s
106115
}
107116
return nil
108117
}
118+
119+
// cleanupNetwork removes cni network setup, specifically the forwards
120+
func cleanupNetwork(ctx context.Context, container containerd.Container, globalOpts types.GlobalCommandOptions) error {
121+
// retrieve info to get current active port mappings
122+
info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata)
123+
if err != nil {
124+
return err
125+
}
126+
ports, portErr := portutil.ParsePortsLabel(info.Labels)
127+
if portErr != nil {
128+
return fmt.Errorf("no oci spec: %q", portErr)
129+
}
130+
portMappings := []gocni.NamespaceOpts{
131+
gocni.WithCapabilityPortMap(ports),
132+
}
133+
134+
// retrieve info to get cni instance
135+
spec, err := container.Spec(ctx)
136+
if err != nil {
137+
return err
138+
}
139+
networksJSON := spec.Annotations[labels.Networks]
140+
var networks []string
141+
if err := json.Unmarshal([]byte(networksJSON), &networks); err != nil {
142+
return err
143+
}
144+
netType, err := nettype.Detect(networks)
145+
if err != nil {
146+
return err
147+
}
148+
149+
switch netType {
150+
case nettype.Host, nettype.None, nettype.Container:
151+
// NOP
152+
case nettype.CNI:
153+
e, err := netutil.NewCNIEnv(globalOpts.CNIPath, globalOpts.CNINetConfPath, netutil.WithDefaultNetwork())
154+
if err != nil {
155+
return err
156+
}
157+
cniOpts := []gocni.Opt{
158+
gocni.WithPluginDir([]string{globalOpts.CNIPath}),
159+
}
160+
netMap, err := e.NetworkMap()
161+
if err != nil {
162+
return err
163+
}
164+
for _, netstr := range networks {
165+
net, ok := netMap[netstr]
166+
if !ok {
167+
return fmt.Errorf("no such network: %q", netstr)
168+
}
169+
cniOpts = append(cniOpts, gocni.WithConfListBytes(net.Bytes))
170+
}
171+
cni, err := gocni.New(cniOpts...)
172+
if err != nil {
173+
return err
174+
}
175+
176+
var namespaceOpts []gocni.NamespaceOpts
177+
namespaceOpts = append(namespaceOpts, portMappings...)
178+
namespace := spec.Annotations[labels.Namespace]
179+
fullID := namespace + "-" + container.ID()
180+
if err := cni.Remove(ctx, fullID, "", namespaceOpts...); err != nil {
181+
log.L.WithError(err).Errorf("failed to call cni.Remove")
182+
return err
183+
}
184+
return nil
185+
default:
186+
return fmt.Errorf("unexpected network type %v", netType)
187+
}
188+
return nil
189+
}

pkg/cmd/container/stop.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ func Stop(ctx context.Context, client *containerd.Client, reqs []string, opt typ
3535
if found.MatchCount > 1 {
3636
return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
3737
}
38+
if err := cleanupNetwork(ctx, found.Container, opt.GOptions); err != nil {
39+
return fmt.Errorf("unable to cleanup network for container: %s", found.Req)
40+
}
3841
if err := containerutil.Stop(ctx, found.Container, opt.Timeout); err != nil {
3942
if errdefs.IsNotFound(err) {
4043
fmt.Fprintf(opt.Stderr, "No such container: %s\n", found.Req)

0 commit comments

Comments
 (0)