From 38397c0bafbc945aebc73ffb2ce0b15837c31cd0 Mon Sep 17 00:00:00 2001 From: Akihiro Suda Date: Tue, 9 Mar 2021 17:17:22 +0900 Subject: [PATCH] add `nerdctl start` Signed-off-by: Akihiro Suda --- README.md | 5 ++- container.go | 1 + kill.go | 7 ++- main.go | 1 + pkg/labels/labels.go | 3 ++ run.go | 7 ++- run_network_test.go | 5 ++- start.go | 100 +++++++++++++++++++++++++++++++++++++++++++ stop_test.go | 69 +++++++++++++++++++++++++++++ 9 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 start.go create mode 100644 stop_test.go diff --git a/README.md b/README.md index 3c4e8986ae3..db0c598fbdb 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl port](#whale-nerdctl-port) - [:whale: nerdctl rm](#whale-nerdctl-rm) - [:whale: nerdctl stop](#whale-nerdctl-stop) + - [:whale: nerdctl start](#whale-nerdctl-start) - [:whale: nerdctl kill](#whale-nerdctl-kill) - [:whale: nerdctl pause](#whale-nerdctl-pause) - [:whale: nerdctl unpause](#whale-nerdctl-unpause) @@ -306,6 +307,9 @@ Flags: ### :whale: nerdctl stop Stop one or more running containers. +### :whale: nerdctl start +Start one or more running containers. + ### :whale: nerdctl kill Kill one or more running containers. @@ -468,7 +472,6 @@ Container management: - `docker cp` - `docker diff` - `docker rename` -- `docker start` - `docker wait` - `docker container prune` diff --git a/container.go b/container.go index aebc828ed26..1887a497ad7 100644 --- a/container.go +++ b/container.go @@ -34,6 +34,7 @@ var containerCommand = &cli.Command{ portCommand, rmCommand, stopCommand, + startCommand, killCommand, pauseCommand, unpauseCommand, diff --git a/kill.go b/kill.go index dd39a8f0e2a..64299266fe8 100644 --- a/kill.go +++ b/kill.go @@ -72,7 +72,7 @@ func killAction(clicontext *cli.Context) error { walker := &containerwalker.ContainerWalker{ Client: client, OnFound: func(ctx context.Context, found containerwalker.Found) error { - if err := killContainer(ctx, clicontext, found.Container, signal); err != nil { + if err := killContainer(ctx, found.Container, signal); err != nil { if errdefs.IsNotFound(err) { fmt.Fprintf(clicontext.App.ErrWriter, "Error response from daemon: Cannot kill container: %s: No such container: %s\n", found.Req, found.Req) os.Exit(1) @@ -94,7 +94,7 @@ func killAction(clicontext *cli.Context) error { return nil } -func killContainer(ctx context.Context, clicontext *cli.Context, container containerd.Container, signal syscall.Signal) error { +func killContainer(ctx context.Context, container containerd.Container, signal syscall.Signal) error { task, err := container.Task(ctx, cio.Load) if err != nil { return err @@ -109,8 +109,7 @@ func killContainer(ctx context.Context, clicontext *cli.Context, container conta switch status.Status { case containerd.Created, containerd.Stopped: - fmt.Fprintf(clicontext.App.ErrWriter, "Error response from daemon: Cannot kill container: %s: Container %s is not running\n", container.ID(), container.ID()) - os.Exit(1) + return errors.Errorf("cannot kill container: %s: Container %s is not running\n", container.ID(), container.ID()) case containerd.Paused, containerd.Pausing: paused = true default: diff --git a/main.go b/main.go index c5af8d28734..5bef53d9fb4 100644 --- a/main.go +++ b/main.go @@ -142,6 +142,7 @@ func newApp() *cli.App { logsCommand, portCommand, stopCommand, + startCommand, killCommand, rmCommand, pauseCommand, diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 0708a25eef3..a319da50ec7 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -42,4 +42,7 @@ const ( // Ports is a JSON-marshalled string of []gocni.PortMapping . Ports = Prefix + "ports" + + // LogURI is the log URI + LogURI = Prefix + "log-uri" ) diff --git a/run.go b/run.go index 11984a9a972..c71f9fa328e 100644 --- a/run.go +++ b/run.go @@ -435,7 +435,7 @@ func runAction(clicontext *cli.Context) error { return err } } - ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, clicontext.StringSlice("network"), ports) + ilOpt, err := withInternalLabels(ns, name, hostname, stateDir, clicontext.StringSlice("network"), ports, logURI) if err != nil { return err } @@ -689,7 +689,7 @@ func withContainerLabels(clicontext *cli.Context) ([]containerd.NewContainerOpts return []containerd.NewContainerOpts{o}, nil } -func withInternalLabels(ns, name, hostname, containerStateDir string, networks []string, ports []gocni.PortMapping) (containerd.NewContainerOpts, error) { +func withInternalLabels(ns, name, hostname, containerStateDir string, networks []string, ports []gocni.PortMapping, logURI string) (containerd.NewContainerOpts, error) { m := make(map[string]string) m[labels.Namespace] = ns if name != "" { @@ -709,6 +709,9 @@ func withInternalLabels(ns, name, hostname, containerStateDir string, networks [ } m[labels.Ports] = string(portsJSON) } + if logURI != "" { + m[labels.LogURI] = logURI + } return containerd.WithAdditionalContainerLabels(m), nil } diff --git a/run_network_test.go b/run_network_test.go index 35ed7768829..739e59566f0 100644 --- a/run_network_test.go +++ b/run_network_test.go @@ -212,8 +212,11 @@ func httpGet(urlStr string, attempts int) (*http.Response, error) { if attempts < 1 { return nil, errdefs.ErrInvalidArgument } + client := &http.Client{ + Timeout: 3 * time.Second, + } for i := 0; i < attempts; i++ { - resp, err = http.Get(urlStr) + resp, err = client.Get(urlStr) if err == nil { return resp, nil } diff --git a/start.go b/start.go new file mode 100644 index 00000000000..6b2d2785825 --- /dev/null +++ b/start.go @@ -0,0 +1,100 @@ +/* + Copyright (C) nerdctl authors. + Copyright (C) containerd authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "context" + "fmt" + "net/url" + "syscall" + + "github.com/AkihiroSuda/nerdctl/pkg/idutil/containerwalker" + "github.com/AkihiroSuda/nerdctl/pkg/labels" + "github.com/containerd/containerd" + "github.com/containerd/containerd/cio" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +var startCommand = &cli.Command{ + Name: "start", + Usage: "Start one or more running containers", + ArgsUsage: "[flags] CONTAINER [CONTAINER, ...]", + Action: startAction, +} + +func startAction(clicontext *cli.Context) error { + if clicontext.NArg() == 0 { + return errors.Errorf("requires at least 1 argument") + } + + client, ctx, cancel, err := newClient(clicontext) + if err != nil { + return err + } + defer cancel() + + walker := &containerwalker.ContainerWalker{ + Client: client, + OnFound: func(ctx context.Context, found containerwalker.Found) error { + if err := startContainer(ctx, found.Container); err != nil { + return err + } + _, err := fmt.Fprintf(clicontext.App.Writer, "%s\n", found.Req) + return err + }, + } + for _, req := range clicontext.Args().Slice() { + n, err := walker.Walk(ctx, req) + if err != nil { + return err + } else if n == 0 { + return errors.Errorf("no such container %s", req) + } + } + return nil +} + +func startContainer(ctx context.Context, container containerd.Container) error { + lab, err := container.Labels(ctx) + if err != nil { + return err + } + taskCIO := cio.NullIO + if logURIStr := lab[labels.LogURI]; logURIStr != "" { + logURI, err := url.Parse(logURIStr) + if err != nil { + return err + } + taskCIO = cio.LogURI(logURI) + } + if err = killContainer(ctx, container, syscall.SIGKILL); err != nil { + logrus.WithError(err).Debug("failed to kill container (negligible in most case)") + } + if oldTask, err := container.Task(ctx, nil); err == nil { + if _, err := oldTask.Delete(ctx); err != nil { + logrus.WithError(err).Debug("failed to delete old task") + } + } + task, err := container.NewTask(ctx, taskCIO) + if err != nil { + return err + } + return task.Start(ctx) +} diff --git a/stop_test.go b/stop_test.go new file mode 100644 index 00000000000..c77a697d0e8 --- /dev/null +++ b/stop_test.go @@ -0,0 +1,69 @@ +/* + Copyright (C) nerdctl authors. + Copyright (C) containerd authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package main + +import ( + "fmt" + "io/ioutil" + "strings" + "testing" + + "github.com/AkihiroSuda/nerdctl/pkg/testutil" + "github.com/pkg/errors" + "gotest.tools/v3/assert" +) + +func TestStopStart(t *testing.T) { + const ( + hostPort = 8080 + testContainerName = "nerdctl-test-stop-start-nginx" + ) + base := testutil.NewBase(t) + defer base.Cmd("rm", "-f", testContainerName).Run() + + base.Cmd("run", "-d", + "--restart=no", + "--name", testContainerName, + "-p", fmt.Sprintf("127.0.0.1:%d:80", hostPort), + testutil.NginxAlpineImage).AssertOK() + + check := func(httpGetRetry int) error { + resp, err := httpGet(fmt.Sprintf("http://127.0.0.1:%d", hostPort), httpGetRetry) + if err != nil { + return err + } + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + if !strings.Contains(string(respBody), testutil.NginxAlpineIndexHTMLSnippet) { + return errors.Errorf("expected contain %q, got %q", + testutil.NginxAlpineIndexHTMLSnippet, string(respBody)) + } + return nil + } + + assert.NilError(t, check(30)) + base.Cmd("stop", testContainerName).AssertOK() + base.Cmd("exec", testContainerName, "ps").AssertFail() + if check(1) == nil { + t.Fatal("expected to get an error") + } + base.Cmd("start", testContainerName).AssertOK() + assert.NilError(t, check(30)) +}