Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 5 additions & 9 deletions cmd/nerdctl/compose_create_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,6 @@ import (
)

func TestComposeCreate(t *testing.T) {
// docker-compose v1 depecreated this command
// docker-compose v2 reimplemented this command
testutil.DockerIncompatible(t)

base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
Expand All @@ -46,7 +42,7 @@ services:

// 1.1 `compose create` should create service container (in `created` status)
base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DockerIncompatible() can be now probably removed

// 1.2 created container can be started by `compose start`
base.ComposeCmd("-f", comp.YAMLFullPath(), "start").AssertOK()
}
Expand Down Expand Up @@ -78,8 +74,8 @@ services:

// `compose create` should create containers for both services and their dependencies
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "svc0").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1", "-a").AssertOutContainsAny("Created", "created")
}

func TestComposeCreatePull(t *testing.T) {
Expand Down Expand Up @@ -111,7 +107,7 @@ services:
base.ComposeCmd("-f", comp.YAMLFullPath(), "create").AssertOK()
base.Cmd("rmi", "-f", testutil.AlpineImage).Run()
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--pull", "always").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
}

func TestComposeCreateBuild(t *testing.T) {
Expand Down Expand Up @@ -148,5 +144,5 @@ services:
// `compose create --build` should succeed: image is built and container is created
base.ComposeCmd("-f", comp.YAMLFullPath(), "create", "--build").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "images", "svc0").AssertOutContains(imageSvc0)
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Created", "created")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Created", "created")
}
2 changes: 1 addition & 1 deletion cmd/nerdctl/compose_down_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,5 @@ services:
defer base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "down", "-v").Run()

base.ComposeCmd("-p", projectName, "-f", compOrphan.YAMLFullPath(), "down", "--remove-orphans").AssertOK()
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps").AssertOutNotContains(orphanContainer)
base.ComposeCmd("-p", projectName, "-f", compFull.YAMLFullPath(), "ps", "-a").AssertOutNotContains(orphanContainer)
}
2 changes: 1 addition & 1 deletion cmd/nerdctl/compose_kill_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,6 @@ volumes:
base.ComposeCmd("-f", comp.YAMLFullPath(), "kill", "db").AssertOK()
time.Sleep(3 * time.Second)
// Docker Compose v1: "Exit 137", v2: "exited (137)"
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny(" 137", "(137)")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny(" 137", "(137)")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")
}
2 changes: 1 addition & 1 deletion cmd/nerdctl/compose_pause_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ services:

// pause a service should (only) pause its own container
base.ComposeCmd("-f", comp.YAMLFullPath(), "pause", "svc0").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0").AssertOutContainsAny("Paused", "paused")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc0", "-a").AssertOutContainsAny("Paused", "paused")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "svc1").AssertOutContainsAny("Up", "running")

// unpause should be able to recover the paused service container
Expand Down
130 changes: 126 additions & 4 deletions cmd/nerdctl/compose_ps.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,13 @@ package main
import (
"context"
"fmt"
"strings"
"text/tabwriter"
"time"

"github.com/containerd/containerd"
"github.com/containerd/containerd/errdefs"
"github.com/containerd/containerd/runtime/restart"
gocni "github.com/containerd/go-cni"
"github.com/containerd/nerdctl/pkg/clientutil"
"github.com/containerd/nerdctl/pkg/cmd/compose"
Expand All @@ -42,7 +46,12 @@ func newComposePsCommand() *cobra.Command {
SilenceUsage: true,
SilenceErrors: true,
}
composePsCommand.Flags().String("format", "", "Format the output. Supported values: [json]")
composePsCommand.Flags().String("format", "table", "Format the output. Supported values: [table|json]")
composePsCommand.Flags().String("filter", "", "Filter matches containers based on given conditions")
composePsCommand.Flags().StringArray("status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]")
composePsCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs")
composePsCommand.Flags().Bool("services", false, "Display services")
composePsCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)")
return composePsCommand
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

Expand Down Expand Up @@ -71,8 +80,40 @@ func composePsAction(cmd *cobra.Command, args []string) error {
if err != nil {
return err
}
if format != "json" && format != "" {
return fmt.Errorf("unsupported format %s, supported formats are: [json]", format)
if format != "json" && format != "table" {
return fmt.Errorf("unsupported format %s, supported formats are: [table|json]", format)
}
status, err := cmd.Flags().GetStringArray("status")
if err != nil {
return err
}
quiet, err := cmd.Flags().GetBool("quiet")
if err != nil {
return err
}
displayServices, err := cmd.Flags().GetBool("services")
if err != nil {
return err
}
filter, err := cmd.Flags().GetString("filter")
if err != nil {
return err
}
if filter != "" {
splited := strings.SplitN(filter, "=", 2)
if len(splited) != 2 {
return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter)
}
// currently only the 'status' filter is supported
if splited[0] != "status" {
return fmt.Errorf("invalid filter '%s'", splited[0])
}
status = append(status, splited[1])
}

all, err := cmd.Flags().GetBool("all")
if err != nil {
return err
}

client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address)
Expand All @@ -97,6 +138,41 @@ func composePsAction(cmd *cobra.Command, args []string) error {
return err
}

if !all {
var upContainers []containerd.Container
for _, container := range containers {
// cStatus := formatter.ContainerStatus(ctx, c)
cStatus, err := containerutil.ContainerStatus(ctx, container)
if err != nil {
continue
}
if cStatus.Status == containerd.Running {
upContainers = append(upContainers, container)
}
}
containers = upContainers
}

if len(status) != 0 {
var filterdContainers []containerd.Container
for _, container := range containers {
cStatus := statusForFilter(ctx, container)
for _, s := range status {
if cStatus == s {
filterdContainers = append(filterdContainers, container)
}
}
}
containers = filterdContainers
}

if quiet {
for _, c := range containers {
fmt.Fprintln(cmd.OutOrStdout(), c.ID())
}
return nil
}

containersPrintable := make([]composeContainerPrintable, len(containers))
eg, ctx := errgroup.WithContext(ctx)
for i, container := range containers {
Expand All @@ -121,6 +197,12 @@ func composePsAction(cmd *cobra.Command, args []string) error {
return err
}

if displayServices {
for _, p := range containersPrintable {
fmt.Fprintln(cmd.OutOrStdout(), p.Service)
}
return nil
}
if format == "json" {
outJSON, err := formatter.ToJSON(containersPrintable, "", "")
if err != nil {
Expand Down Expand Up @@ -198,9 +280,11 @@ func composeContainerPrintableJSON(ctx context.Context, container containerd.Con
if err == nil {
// show exitCode only when container is exited/stopped
if status.Status == containerd.Stopped {
state = "exited"
exitCode = status.ExitStatus
} else {
state = string(status.Status)
}
state = string(status.Status)
} else {
state = string(containerd.Unknown)
}
Expand Down Expand Up @@ -255,3 +339,41 @@ func formatPublishers(labelMap map[string]string) []PortPublisher {
}
return dockerPorts
}

// statusForFilter returns the status value to be matched with the 'status' filter
func statusForFilter(ctx context.Context, c containerd.Container) string {
// Just in case, there is something wrong in server.
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()

task, err := c.Task(ctx, nil)
if err != nil {
// NOTE: NotFound doesn't mean that container hasn't started.
// In docker/CRI-containerd plugin, the task will be deleted
// when it exits. So, the status will be "created" for this
// case.
if errdefs.IsNotFound(err) {
return string(containerd.Created)
}
return string(containerd.Unknown)
}

status, err := task.Status(ctx)
if err != nil {
return string(containerd.Unknown)
}
labels, err := c.Labels(ctx)
if err != nil {
return string(containerd.Unknown)
}

switch s := status.Status; s {
case containerd.Stopped:
if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(status, labels) {
return "restarting"
}
return "exited"
default:
return string(s)
}
}
87 changes: 83 additions & 4 deletions cmd/nerdctl/compose_ps_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,92 @@ import (
"fmt"
"strings"
"testing"
"time"

"github.com/containerd/nerdctl/pkg/tabutil"
"github.com/containerd/nerdctl/pkg/testutil"
"gotest.tools/v3/assert"
)

func TestComposePs(t *testing.T) {
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'

services:
wordpress:
image: %s
container_name: wordpress_container
ports:
- 8080:80
environment:
WORDPRESS_DB_HOST: db
WORDPRESS_DB_USER: exampleuser
WORDPRESS_DB_PASSWORD: examplepass
WORDPRESS_DB_NAME: exampledb
volumes:
- wordpress:/var/www/html
db:
image: %s
container_name: db_container
environment:
MYSQL_DATABASE: exampledb
MYSQL_USER: exampleuser
MYSQL_PASSWORD: examplepass
MYSQL_RANDOM_ROOT_PASSWORD: '1'
volumes:
- db:/var/lib/mysql
alpine:
image: %s
container_name: alpine_container

volumes:
wordpress:
db:
`, testutil.WordpressImage, testutil.MariaDBImage, testutil.AlpineImage)
comp := testutil.NewComposeDir(t, dockerComposeYAML)
defer comp.CleanUp()
projectName := comp.ProjectName()
t.Logf("projectName=%q", projectName)

base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK()
defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").Run()

assertHandler := func(expectedName, expectedImage string) func(stdout string) error {
return func(stdout string) error {
lines := strings.Split(strings.TrimSpace(stdout), "\n")
if len(lines) < 2 {
return fmt.Errorf("expected at least 2 lines, got %d", len(lines))
}

tab := tabutil.NewReader("NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS")
err := tab.ParseHeader(lines[0])
if err != nil {
return fmt.Errorf("failed to parse header: %v", err)
}

container, _ := tab.ReadRow(lines[1], "NAME")
assert.Equal(t, container, expectedName)

image, _ := tab.ReadRow(lines[1], "IMAGE")
assert.Equal(t, image, expectedImage)

return nil
}

}

time.Sleep(3 * time.Second)
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutWithFunc(assertHandler("wordpress_container", testutil.WordpressImage))
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutWithFunc(assertHandler("db_container", testutil.MariaDBImage))
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps").AssertOutNotContains(testutil.AlpineImage)
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "alpine", "-a").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage))
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "-a", "--filter", "status=exited").AssertOutWithFunc(assertHandler("alpine_container", testutil.AlpineImage))
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--services", "-a").AssertOutContainsAll("wordpress\n", "db\n", "alpine\n")
}

func TestComposePsJSON(t *testing.T) {
// `--format` is only supported in docker compose v2.
// Currently, CI is using docker compose v1.
// docker parses unknown 'format' as a Go template and won't output an error
testutil.DockerIncompatible(t)

base := testutil.NewBase(t)
Expand Down Expand Up @@ -101,8 +180,8 @@ volumes:
AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"running"`, `"TargetPort":80`, `"PublishedPort":8080`))
// check wordpress is stopped
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "wordpress").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress").
AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"stopped"`))
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress", "-a").
AssertOutWithFunc(assertHandler("wordpress", 1, `"Service":"wordpress"`, `"State":"exited"`))
// check wordpress is removed
base.ComposeCmd("-f", comp.YAMLFullPath(), "rm", "-f", "wordpress").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "--format", "json", "wordpress").
Expand Down
7 changes: 2 additions & 5 deletions cmd/nerdctl/compose_restart_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import (
)

func TestComposeRestart(t *testing.T) {
// docker-compose v2 hides exited containers in `compose ps`, and shows
// them if `-a` is passed, which is not supported yet by `nerdctl compose`.
testutil.DockerIncompatible(t)
base := testutil.NewBase(t)
var dockerComposeYAML = fmt.Sprintf(`
version: '3.1'
Expand Down Expand Up @@ -68,13 +65,13 @@ volumes:

// stop and restart a single service.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
Copy link
Copy Markdown
Member

@AkihiroSuda AkihiroSuda Sep 16, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DockerIncompatible() can be now probably removed

base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "db").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running")

// stop one service and restart all (also check `--timeout` arg).
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "restart", "--timeout", "5").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Up", "running")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")
Expand Down
4 changes: 2 additions & 2 deletions cmd/nerdctl/compose_stop_linux_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,11 @@ volumes:

// stop should (only) stop the given service.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "db").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "db", "-a").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Up", "running")

// `--timeout` arg should work properly.
base.ComposeCmd("-f", comp.YAMLFullPath(), "stop", "--timeout", "5", "wordpress").AssertOK()
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress").AssertOutContainsAny("Exit", "exited")
base.ComposeCmd("-f", comp.YAMLFullPath(), "ps", "wordpress", "-a").AssertOutContainsAny("Exit", "exited")

}
Loading