Skip to content

Commit 7399297

Browse files
authored
Merge pull request #72 from AkihiroSuda/dev-fix-restart
fix restarting rootless containers
2 parents d03d648 + 7a3493b commit 7399297

File tree

10 files changed

+220
-35
lines changed

10 files changed

+220
-35
lines changed

.github/workflows/test.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: "Prepare test environment"
3232
run: DOCKER_BUILDKIT=1 docker build -t test --target test .
3333
- name: "Test"
34-
run: docker run -t --rm --privileged test
34+
run: docker run -t --rm --privileged test go test -v -test.kill-daemon ./...
3535

3636
cross:
3737
runs-on: ubuntu-20.04
@@ -57,7 +57,7 @@ jobs:
5757
with:
5858
fetch-depth: 1
5959
- name: "Ensure that the test suite is compatible with Docker"
60-
run: go test -v -exec sudo -test.target=docker .
60+
run: go test -v -exec sudo -test.target=docker -test.kill-daemon .
6161

6262
test-cgroup2:
6363
name: "Cgroup2 + rootless"
@@ -83,6 +83,6 @@ jobs:
8383
- name: "Install rootless containerd"
8484
run: ssh default -- containerd-rootless-setuptool.sh install
8585
- name: "Run tests (rootless)"
86-
run: ssh default -- "CONTAINERD_SNAPSHOTTER=native /vagrant/nerdctl.test -test.v"
86+
run: ssh default -- "CONTAINERD_SNAPSHOTTER=native /vagrant/nerdctl.test -test.v -test.kill-daemon"
8787
- name: "Uninstall rootless containerd"
8888
run: ssh default -- containerd-rootless-setuptool.sh uninstall

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,8 @@ In addition to containerd, the following components should be installed (optiona
116116
- [CNI isolation plugin](https://github.com/AkihiroSuda/cni-isolation): for isolating bridge networks (`nerdctl network create`)
117117
- [BuildKit](https://github.com/moby/buildkit): for using `nerdctl build`. BuildKit daemon (`buildkitd`) needs to be running.
118118
- [RootlessKit](https://github.com/rootless-containers/rootlesskit) and [slirp4netns](https://github.com/rootless-containers/slirp4netns): for [Rootless mode](./docs/rootless.md)
119-
- RootlessKit needs to be v0.10.0 or later
120-
- slirp4netns needs toe be v0.4.0 or later
119+
- RootlessKit needs to be v0.10.0 or later. v0.13.2 or later is recommended.
120+
- slirp4netns needs toe be v0.4.0 or later. v1.1.7 or later is recommended.
121121

122122
To run nerdctl inside Docker:
123123
```bash

Vagrantfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ Vagrant.configure("2") do |config|
4545
chmod +x /usr/local/sbin/runc
4646
4747
# Install RootlessKit
48-
ROOTLESSKIT_VERSION=0.13.1
48+
ROOTLESSKIT_VERSION=0.13.2
4949
curl -sSL https://github.com/rootless-containers/rootlesskit/releases/download/v${ROOTLESSKIT_VERSION}/rootlesskit-$(uname -m).tar.gz | tar Cxzv /usr/local/bin
5050
5151
# Delegate cgroup v2 controllers

extras/rootless/containerd-rootless.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
# External dependencies:
2626
# * newuidmap and newgidmap needs to be installed.
2727
# * /etc/subuid and /etc/subgid needs to be configured for the current user.
28-
# * RootlessKit (>= v0.10.0) needs to be installed
29-
# * Either one of slirp4netns (>= v0.4.0), VPNKit, lxc-user-nic needs to be installed.
28+
# * RootlessKit (>= v0.10.0) needs to be installed. RootlessKit >= v0.13.2 is recommended.
29+
# * Either one of slirp4netns (>= v0.4.0), VPNKit, lxc-user-nic needs to be installed. slirp4netns >= v1.1.7 is recommended.
3030
#
3131
# Recognized environment variables:
3232
# * CONTAINERD_ROOTLESS_ROOTLESSKIT_STATE_DIR=DIR: the rootlesskit state dir. Defaults to "$XDG_RUNTIME_DIR/containerd-rootless".

pkg/dnsutil/hostsstore/hostsstore.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,9 @@ func ensureFile(path string) error {
6464
return err
6565
}
6666

67-
// EnsureHostsFile is used for creating mount-bindable /etc/hosts file.
67+
// AllocHostsFile is used for creating mount-bindable /etc/hosts file.
6868
// The file is initialized with no content.
69-
func EnsureHostsFile(dataStore, ns, id string) (string, error) {
69+
func AllocHostsFile(dataStore, ns, id string) (string, error) {
7070
lockDir := filepath.Join(dataStore, hostsDirBasename)
7171
if err := os.MkdirAll(lockDir, 0700); err != nil {
7272
return "", err
@@ -79,6 +79,18 @@ func EnsureHostsFile(dataStore, ns, id string) (string, error) {
7979
return path, err
8080
}
8181

82+
func DeallocHostsFile(dataStore, ns, id string) error {
83+
lockDir := filepath.Join(dataStore, hostsDirBasename)
84+
if err := os.MkdirAll(lockDir, 0700); err != nil {
85+
return err
86+
}
87+
dirToBeRemoved := filepath.Dir(HostsPath(dataStore, ns, id))
88+
fn := func() error {
89+
return os.RemoveAll(dirToBeRemoved)
90+
}
91+
return lockutil.WithDirLock(lockDir, fn)
92+
}
93+
8294
func NewStore(dataStore string) (Store, error) {
8395
store := &store{
8496
dataStore: dataStore,
@@ -128,11 +140,15 @@ func (x *store) Acquire(meta Meta) error {
128140

129141
func (x *store) Release(ns, id string) error {
130142
fn := func() error {
131-
d := filepath.Join(x.hostsD, ns, id)
132-
if _, err := os.Stat(d); errors.Is(err, os.ErrNotExist) {
143+
metaPath := filepath.Join(x.hostsD, ns, id, metaJSON)
144+
if _, err := os.Stat(metaPath); errors.Is(err, os.ErrNotExist) {
133145
return nil
134146
}
135-
if err := os.RemoveAll(d); err != nil {
147+
// We remove "meta.json" but we still retain the "hosts" file
148+
// because it is needed for restarting. The "hosts" is removed on
149+
// `nerdctl rm`.
150+
// https://github.com/rootless-containers/rootlesskit/issues/220#issuecomment-783224610
151+
if err := os.RemoveAll(metaPath); err != nil {
136152
return err
137153
}
138154
return newUpdater(x.hostsD).update()

pkg/testutil/testutil.go

Lines changed: 82 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,19 @@ import (
2323
"os"
2424
"os/exec"
2525
"testing"
26+
"time"
2627

28+
"github.com/pkg/errors"
2729
"gotest.tools/v3/assert"
2830
"gotest.tools/v3/icmd"
2931
)
3032

3133
type Base struct {
32-
T testing.TB
33-
Target Target
34-
Binary string
35-
Args []string
34+
T testing.TB
35+
Target Target
36+
DaemonIsKillable bool
37+
Binary string
38+
Args []string
3639
}
3740

3841
func (b *Base) Cmd(args ...string) *Cmd {
@@ -44,13 +47,75 @@ func (b *Base) Cmd(args ...string) *Cmd {
4447
return cmd
4548
}
4649

50+
func (b *Base) systemctlTarget() string {
51+
switch b.Target {
52+
case Nerdctl:
53+
return "containerd.service"
54+
case Docker:
55+
return "docker.service"
56+
default:
57+
b.T.Fatalf("unexpected target %q", b.Target)
58+
return ""
59+
}
60+
}
61+
62+
func (b *Base) systemctlArgs() []string {
63+
var systemctlArgs []string
64+
if os.Geteuid() != 0 {
65+
systemctlArgs = append(systemctlArgs, "--user")
66+
}
67+
return systemctlArgs
68+
}
69+
70+
func (b *Base) KillDaemon() {
71+
b.T.Helper()
72+
if !b.DaemonIsKillable {
73+
b.T.Skip("daemon is not killable (hint: set \"-test.kill-daemon\")")
74+
}
75+
target := b.systemctlTarget()
76+
b.T.Logf("killing %q", target)
77+
cmdKill := exec.Command("systemctl",
78+
append(b.systemctlArgs(),
79+
[]string{"kill", "-s", "KILL", target}...)...)
80+
if out, err := cmdKill.CombinedOutput(); err != nil {
81+
err = errors.Wrapf(err, "cannot kill %q: %q", target, string(out))
82+
b.T.Fatal(err)
83+
}
84+
// the daemon should restart automatically
85+
}
86+
87+
func (b *Base) EnsureDaemonActive() {
88+
b.T.Helper()
89+
target := b.systemctlTarget()
90+
b.T.Logf("checking activity of %q", target)
91+
systemctlArgs := b.systemctlArgs()
92+
const (
93+
maxRetry = 30
94+
sleep = 3 * time.Second
95+
)
96+
for i := 0; i < maxRetry; i++ {
97+
cmd := exec.Command("systemctl",
98+
append(systemctlArgs,
99+
[]string{"is-active", target}...)...)
100+
out, err := cmd.CombinedOutput()
101+
b.T.Logf("(retry=%d) %s", i, string(out))
102+
if err == nil {
103+
b.T.Logf("daemon %q is now running", target)
104+
return
105+
}
106+
time.Sleep(sleep)
107+
}
108+
b.T.Fatalf("daemon %q not running", target)
109+
}
110+
47111
type Cmd struct {
48112
icmd.Cmd
49113
*Base
50114
DockerIncompatible bool
51115
}
52116

53117
func (c *Cmd) Run() *icmd.Result {
118+
c.Base.T.Helper()
54119
if c.Base.Target == Docker && c.DockerIncompatible {
55120
c.Base.T.Skip("test is incompatible with Docker")
56121
}
@@ -91,10 +156,14 @@ const (
91156
Docker = Target("docker")
92157
)
93158

94-
var flagTestTarget Target
159+
var (
160+
flagTestTarget Target
161+
flagTestKillDaemon bool
162+
)
95163

96164
func M(m *testing.M) {
97165
flag.StringVar(&flagTestTarget, "test.target", Nerdctl, "target to test")
166+
flag.BoolVar(&flagTestKillDaemon, "test.kill-daemon", false, "enable tests that kill the daemon")
98167
flag.Parse()
99168
fmt.Printf("test target: %q\n", flagTestTarget)
100169
os.Exit(m.Run())
@@ -107,12 +176,17 @@ func GetTarget() string {
107176
return flagTestTarget
108177
}
109178

179+
func GetDaemonIsKillable() bool {
180+
return flagTestKillDaemon
181+
}
182+
110183
const Namespace = "nerdctl-test"
111184

112185
func NewBase(t *testing.T) *Base {
113186
base := &Base{
114-
T: t,
115-
Target: GetTarget(),
187+
T: t,
188+
Target: GetTarget(),
189+
DaemonIsKillable: GetDaemonIsKillable(),
116190
}
117191
var err error
118192
switch base.Target {
@@ -136,6 +210,6 @@ func NewBase(t *testing.T) *Base {
136210
// use GCR mirror to avoid hitting Docker Hub rate limit
137211
const (
138212
AlpineImage = "mirror.gcr.io/library/alpine:3.13"
139-
NginxAlpineImage = "mirror.gcr.io/library/nginx:1.19.6-alpine"
213+
NginxAlpineImage = "mirror.gcr.io/library/nginx:1.19-alpine"
140214
NginxAlpineIndexHTMLSnippet = "<title>Welcome to nginx!</title>"
141215
)

rm.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"os"
2424

25+
"github.com/AkihiroSuda/nerdctl/pkg/dnsutil/hostsstore"
2526
"github.com/AkihiroSuda/nerdctl/pkg/idutil/containerwalker"
2627
"github.com/AkihiroSuda/nerdctl/pkg/labels"
2728
"github.com/AkihiroSuda/nerdctl/pkg/namestore"
@@ -65,7 +66,8 @@ func rmAction(clicontext *cli.Context) error {
6566

6667
force := clicontext.Bool("force")
6768

68-
containerNameStore, err := namestore.New(dataStore, clicontext.String("namespace"))
69+
ns := clicontext.String("namespace")
70+
containerNameStore, err := namestore.New(dataStore, ns)
6971
if err != nil {
7072
return err
7173
}
@@ -77,7 +79,7 @@ func rmAction(clicontext *cli.Context) error {
7779
if err != nil {
7880
return err
7981
}
80-
err = removeContainer(clicontext, ctx, client, found.Container.ID(), found.Req, force, stateDir, containerNameStore)
82+
err = removeContainer(clicontext, ctx, client, ns, found.Container.ID(), found.Req, force, dataStore, stateDir, containerNameStore)
8183
return err
8284
},
8385
}
@@ -93,7 +95,8 @@ func rmAction(clicontext *cli.Context) error {
9395
}
9496

9597
// removeContainer returns nil when the container cannot be found
96-
func removeContainer(clicontext *cli.Context, ctx context.Context, client *containerd.Client, id, req string, force bool, stateDir string, namst namestore.NameStore) (retErr error) {
98+
// FIXME: refactoring
99+
func removeContainer(clicontext *cli.Context, ctx context.Context, client *containerd.Client, ns, id, req string, force bool, dataStore, stateDir string, namst namestore.NameStore) (retErr error) {
97100
var name string
98101
defer func() {
99102
if errdefs.IsNotFound(retErr) {
@@ -111,6 +114,11 @@ func removeContainer(clicontext *cli.Context, ctx context.Context, client *conta
111114
} else {
112115
logrus.WithError(retErr).Warnf("failed to remove container %q", id)
113116
}
117+
if retErr == nil {
118+
retErr = hostsstore.DeallocHostsFile(dataStore, ns, id)
119+
} else {
120+
logrus.WithError(retErr).Warnf("failed to release name store for container %q", id)
121+
}
114122
}()
115123
container, err := client.LoadContainer(ctx, id)
116124
if err != nil {

run.go

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ import (
3939
"github.com/AkihiroSuda/nerdctl/pkg/namestore"
4040
"github.com/AkihiroSuda/nerdctl/pkg/netutil"
4141
"github.com/AkihiroSuda/nerdctl/pkg/portutil"
42-
"github.com/AkihiroSuda/nerdctl/pkg/rootlessutil"
4342
"github.com/containerd/console"
4443
"github.com/containerd/containerd"
4544
"github.com/containerd/containerd/cio"
@@ -223,6 +222,8 @@ func runAction(clicontext *cli.Context) error {
223222
return errors.New("image name needs to be specified")
224223
}
225224

225+
ns := clicontext.String("namespace")
226+
226227
client, ctx, cancel, err := newClient(clicontext)
227228
if err != nil {
228229
return err
@@ -376,7 +377,7 @@ func runAction(clicontext *cli.Context) error {
376377
return err
377378
}
378379
// the content of /etc/hosts is created in OCI Hook
379-
etcHostsPath, err := hostsstore.EnsureHostsFile(dataStore, clicontext.String("namespace"), id)
380+
etcHostsPath, err := hostsstore.AllocHostsFile(dataStore, ns, id)
380381
if err != nil {
381382
return err
382383
}
@@ -446,15 +447,15 @@ func runAction(clicontext *cli.Context) error {
446447
var containerNameStore namestore.NameStore
447448
name := clicontext.String("name")
448449
if name != "" {
449-
containerNameStore, err = namestore.New(dataStore, clicontext.String("namespace"))
450+
containerNameStore, err = namestore.New(dataStore, ns)
450451
if err != nil {
451452
return err
452453
}
453454
if err := containerNameStore.Acquire(name, id); err != nil {
454455
return err
455456
}
456457
}
457-
ilOpt, err := withInternalLabels(clicontext.String("namespace"), name, hostname, stateDir, clicontext.StringSlice("network"), ports)
458+
ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, clicontext.StringSlice("network"), ports)
458459
if err != nil {
459460
return err
460461
}
@@ -475,7 +476,7 @@ func runAction(clicontext *cli.Context) error {
475476
return errors.New("flag -d and --rm cannot be specified together")
476477
}
477478
defer func() {
478-
if removeErr := removeContainer(clicontext, ctx, client, id, id, true, stateDir, containerNameStore); removeErr != nil {
479+
if removeErr := removeContainer(clicontext, ctx, client, ns, id, id, true, dataStore, stateDir, containerNameStore); removeErr != nil {
479480
logrus.WithError(removeErr).Warnf("failed to remove container %s", id)
480481
}
481482
}()
@@ -617,12 +618,6 @@ func generateRestartOpts(restartFlag, logURI string) ([]containerd.NewContainerO
617618
case "", "no":
618619
return nil, nil
619620
case "always":
620-
if rootlessutil.IsRootless() {
621-
// $ systemctl --user kill -s KILL containerd
622-
// $ nerdctl info
623-
// FATA[0000] stat /run/user/1001/containerd-rootless/child_pid: no such file or directory
624-
logrus.Warn("FIXME: --restart=always is broken on rootless")
625-
}
626621
opts := []containerd.NewContainerOpts{restart.WithStatus(containerd.Running)}
627622
if logURI != "" {
628623
opts = append(opts, restart.WithLogURIString(logURI))

run_network_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"time"
2727

2828
"github.com/AkihiroSuda/nerdctl/pkg/testutil"
29+
"github.com/containerd/containerd/errdefs"
2930
"github.com/pkg/errors"
3031
"gotest.tools/v3/assert"
3132
)
@@ -165,6 +166,9 @@ func httpGet(urlStr string, attempts int) (*http.Response, error) {
165166
resp *http.Response
166167
err error
167168
)
169+
if attempts < 1 {
170+
return nil, errdefs.ErrInvalidArgument
171+
}
168172
for i := 0; i < attempts; i++ {
169173
resp, err = http.Get(urlStr)
170174
if err == nil {

0 commit comments

Comments
 (0)