diff --git a/cmd/nerdctl/container_create.go b/cmd/nerdctl/container_create.go index 7b56ba9a02c..3851b07fd28 100644 --- a/cmd/nerdctl/container_create.go +++ b/cmd/nerdctl/container_create.go @@ -51,7 +51,7 @@ func newCreateCommand() *cobra.Command { return createCommand } -func createAction(cmd *cobra.Command, args []string) error { +func createAction(cmd *cobra.Command, args []string) (err error) { globalOptions, err := processRootCmdFlags(cmd) if err != nil { return err @@ -91,6 +91,13 @@ func createAction(cmd *cobra.Command, args []string) error { } return err } + // defer setting `nerdctl/error` label in case of error + defer func() { + if err != nil { + containerutil.UpdateErrorLabel(ctx, container, err) + } + }() + fmt.Fprintln(cmd.OutOrStdout(), container.ID()) return nil } diff --git a/cmd/nerdctl/container_inspect_linux_test.go b/cmd/nerdctl/container_inspect_linux_test.go index fd2d3ec1211..6eb0ba9b2f1 100644 --- a/cmd/nerdctl/container_inspect_linux_test.go +++ b/cmd/nerdctl/container_inspect_linux_test.go @@ -18,6 +18,7 @@ package main import ( "fmt" + "strings" "testing" "github.com/containerd/nerdctl/pkg/inspecttypes/dockercompat" @@ -154,3 +155,56 @@ func TestContainerInspectContainsLabel(t *testing.T) { assert.Equal(base.T, "foo", lbs["foo"]) assert.Equal(base.T, "bar", lbs["bar"]) } + +func TestContainerInspectState(t *testing.T) { + t.Parallel() + testContainer := testutil.Identifier(t) + base := testutil.NewBase(t) + + type testCase struct { + name, containerName, cmd string + want dockercompat.ContainerState + } + // nerdctl: run error produces a nil Task, so the Status is empty because Status comes from Task. + // docker : run error gives => `Status=created` as in docker there is no a separation between container and Task. + errStatus := "" + if base.Target == testutil.Docker { + errStatus = "created" + } + testCases := []testCase{ + { + name: "inspect State with error", + containerName: fmt.Sprintf("%s-fail", testContainer), + cmd: "aa", + want: dockercompat.ContainerState{ + Error: "executable file not found in $PATH", + Status: errStatus, + }, + }, + { + name: "inspect State without error", + containerName: fmt.Sprintf("%s-success", testContainer), + cmd: "ls", + want: dockercompat.ContainerState{ + Error: "", + Status: "exited", + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + defer base.Cmd("rm", "-f", tc.containerName).Run() + if tc.want.Error != "" { + base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertFail() + } else { + base.Cmd("run", "--name", tc.containerName, testutil.AlpineImage, tc.cmd).AssertOK() + } + inspect := base.InspectContainer(tc.containerName) + assert.Assert(t, strings.Contains(inspect.State.Error, tc.want.Error), fmt.Sprintf("expected: %s, actual: %s", tc.want.Error, inspect.State.Error)) + assert.Equal(base.T, inspect.State.Status, tc.want.Status) + }) + } + +} diff --git a/cmd/nerdctl/container_run.go b/cmd/nerdctl/container_run.go index bdcf399bdf1..38706c6f4d9 100644 --- a/cmd/nerdctl/container_run.go +++ b/cmd/nerdctl/container_run.go @@ -293,7 +293,7 @@ func setCreateFlags(cmd *cobra.Command) { // runAction is heavily based on ctr implementation: // https://github.com/containerd/containerd/blob/v1.4.3/cmd/ctr/commands/run/run.go -func runAction(cmd *cobra.Command, args []string) error { +func runAction(cmd *cobra.Command, args []string) (err error) { globalOptions, err := processRootCmdFlags(cmd) if err != nil { return err @@ -346,6 +346,12 @@ func runAction(cmd *cobra.Command, args []string) error { } return err } + // defer setting `nerdctl/error` label in case of error + defer func() { + if err != nil { + containerutil.UpdateErrorLabel(ctx, c, err) + } + }() id := c.ID() if rm && !flagD { diff --git a/pkg/cmd/container/remove.go b/pkg/cmd/container/remove.go index c9e7f8955ce..90c0ef5231e 100644 --- a/pkg/cmd/container/remove.go +++ b/pkg/cmd/container/remove.go @@ -90,6 +90,12 @@ func Remove(ctx context.Context, client *containerd.Client, containers []string, // RemoveContainer removes a container from containerd store. func RemoveContainer(ctx context.Context, c containerd.Container, globalOptions types.GlobalCommandOptions, force bool, removeAnonVolumes bool) (retErr error) { + // defer the storage of remove error in the dedicated label + defer func() { + if retErr != nil { + containerutil.UpdateErrorLabel(ctx, c, retErr) + } + }() ns, err := namespaces.NamespaceRequired(ctx) if err != nil { return err diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go index 5bc2c4e62c9..615c48af785 100644 --- a/pkg/containerutil/containerutil.go +++ b/pkg/containerutil/containerutil.go @@ -114,6 +114,15 @@ func UpdateExplicitlyStoppedLabel(ctx context.Context, container containerd.Cont return container.Update(ctx, containerd.UpdateContainerOpts(opt)) } +// UpdateErrorLabel updates the "nerdctl/error" +// label of the container according to the container error. +func UpdateErrorLabel(ctx context.Context, container containerd.Container, err error) error { + opt := containerd.WithAdditionalContainerLabels(map[string]string{ + labels.Error: err.Error(), + }) + return container.Update(ctx, containerd.UpdateContainerOpts(opt)) +} + // WithBindMountHostProcfs replaces procfs mount with rbind. // Required for --pid=host on rootless. // @@ -189,7 +198,13 @@ func GenerateSharingPIDOpts(ctx context.Context, targetCon containerd.Container) } // Start starts `container` with `attach` flag. If `attach` is true, it will attach to the container's stdio. -func Start(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client) error { +func Start(ctx context.Context, container containerd.Container, flagA bool, client *containerd.Client) (err error) { + // defer the storage of start error in the dedicated label + defer func() { + if err != nil { + UpdateErrorLabel(ctx, container, err) + } + }() lab, err := container.Labels(ctx) if err != nil { return err @@ -272,7 +287,13 @@ func Start(ctx context.Context, container containerd.Container, flagA bool, clie } // Stop stops `container` by sending SIGTERM. If the container is not stopped after `timeout`, it sends a SIGKILL. -func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) error { +func Stop(ctx context.Context, container containerd.Container, timeout *time.Duration) (err error) { + // defer the storage of stop error in the dedicated label + defer func() { + if err != nil { + UpdateErrorLabel(ctx, container, err) + } + }() if err := UpdateExplicitlyStoppedLabel(ctx, container, true); err != nil { return err } diff --git a/pkg/errutil/exit_coder.go b/pkg/errutil/exit_coder.go index ae50613b430..be0044edbd0 100644 --- a/pkg/errutil/exit_coder.go +++ b/pkg/errutil/exit_coder.go @@ -25,7 +25,6 @@ type ExitCoder interface { // ExitCodeError is to allow the program to exit with status code without outputting an error message. type ExitCodeError struct { - error exitCode int } @@ -39,6 +38,10 @@ func (e ExitCodeError) ExitCode() int { return e.exitCode } +func (e ExitCodeError) Error() string { + return "" +} + func HandleExitCoder(err error) { if err == nil { return diff --git a/pkg/inspecttypes/dockercompat/dockercompat.go b/pkg/inspecttypes/dockercompat/dockercompat.go index b01253a5407..f4d16bd6281 100644 --- a/pkg/inspecttypes/dockercompat/dockercompat.go +++ b/pkg/inspecttypes/dockercompat/dockercompat.go @@ -167,7 +167,7 @@ type ContainerState struct { // TODO: Dead bool Pid int ExitCode int - // TODO: Error string + Error string // TODO: StartedAt string FinishedAt string // TODO: Health *Health `json:",omitempty"` @@ -257,23 +257,23 @@ func ContainerFromNative(n *native.Container) (*Container, error) { c.Mounts = mounts } + cs := new(ContainerState) + cs.Restarting = n.Labels[restart.StatusLabel] == string(containerd.Running) + cs.Error = n.Labels[labels.Error] if n.Process != nil { - c.State = &ContainerState{ - Status: statusFromNative(n.Process.Status, n.Labels), - Running: n.Process.Status.Status == containerd.Running, - Paused: n.Process.Status.Status == containerd.Paused, - Restarting: n.Labels[restart.StatusLabel] == string(containerd.Running), - Pid: n.Process.Pid, - ExitCode: int(n.Process.Status.ExitStatus), - FinishedAt: n.Process.Status.ExitTime.Format(time.RFC3339Nano), - } + cs.Status = statusFromNative(n.Process.Status, n.Labels) + cs.Running = n.Process.Status.Status == containerd.Running + cs.Paused = n.Process.Status.Status == containerd.Paused + cs.Pid = n.Process.Pid + cs.ExitCode = int(n.Process.Status.ExitStatus) + cs.FinishedAt = n.Process.Status.ExitTime.Format(time.RFC3339Nano) nSettings, err := networkSettingsFromNative(n.Process.NetNS, n.Spec.(*specs.Spec)) if err != nil { return nil, err } c.NetworkSettings = nSettings } - + c.State = cs c.Config = &Config{ Hostname: n.Labels[labels.Hostname], Labels: n.Labels, diff --git a/pkg/labels/labels.go b/pkg/labels/labels.go index 5d4d5431acd..58c747a559e 100644 --- a/pkg/labels/labels.go +++ b/pkg/labels/labels.go @@ -58,7 +58,6 @@ const ( Ports = Prefix + "ports" // IPAddress is the static IP address of the container assigned by the user - IPAddress = Prefix + "ip" // LogURI is the log URI @@ -90,6 +89,10 @@ const ( // PIDContainer is the `nerdctl run --pid` for restarting PIDContainer = Prefix + "pid-container" + // Error encapsulates a container human-readable string + // that describes container error. + Error = Prefix + "error" + // NerdctlDefaultNetwork indicates whether a network is the default network // created and owned by Nerdctl. // Boolean value which can be parsed with strconv.ParseBool() is required.