Skip to content

Commit f86baac

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

File tree

6 files changed

+312
-90
lines changed

6 files changed

+312
-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: 84 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,16 @@ 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"
39+
"github.com/containerd/nerdctl/v2/pkg/rootlessutil"
3340
"github.com/moby/sys/signal"
3441
)
3542

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

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)