Browse Source

Merge pull request #1019 from docker/ps_containers

Nicolas De loof 4 years ago
parent
commit
a95a76291b

+ 22 - 3
aci/compose.go

@@ -29,6 +29,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/errdefs"
+	"github.com/docker/compose-cli/utils/formatter"
 )
 
 type aciComposeService struct {
@@ -119,7 +120,7 @@ func (cs *aciComposeService) Down(ctx context.Context, project string) error {
 	return err
 }
 
-func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
+func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.ContainerSummary, error) {
 	groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
 	if err != nil {
 		return nil, err
@@ -134,12 +135,30 @@ func (cs *aciComposeService) Ps(ctx context.Context, project string) ([]compose.
 		return nil, fmt.Errorf("no containers found in ACI container group %s", project)
 	}
 
-	res := []compose.ServiceStatus{}
+	res := []compose.ContainerSummary{}
 	for _, container := range *group.Containers {
 		if isContainerVisible(container, group, false) {
 			continue
 		}
-		res = append(res, convert.ContainerGroupToServiceStatus(getContainerID(group, container), group, container, cs.ctx.Location))
+		var publishers []compose.PortPublisher
+		urls := formatter.PortsToStrings(convert.ToPorts(group.IPAddress, *container.Ports), convert.FQDN(group, cs.ctx.Location))
+		for i, p := range *container.Ports {
+			publishers = append(publishers, compose.PortPublisher{
+				URL:           urls[i],
+				TargetPort:    int(*p.Port),
+				PublishedPort: int(*p.Port),
+				Protocol:      string(p.Protocol),
+			})
+		}
+		id := getContainerID(group, container)
+		res = append(res, compose.ContainerSummary{
+			ID:         id,
+			Name:       id,
+			Project:    project,
+			Service:    *container.Name,
+			State:      convert.GetStatus(container, group),
+			Publishers: publishers,
+		})
 	}
 	return res, nil
 }

+ 4 - 3
aci/convert/convert.go

@@ -314,13 +314,14 @@ func ContainerGroupToServiceStatus(containerID string, group containerinstance.C
 	return compose.ServiceStatus{
 		ID:       containerID,
 		Name:     *container.Name,
-		Ports:    formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), fqdn(group, region)),
+		Ports:    formatter.PortsToStrings(ToPorts(group.IPAddress, *container.Ports), FQDN(group, region)),
 		Replicas: replicas,
 		Desired:  1,
 	}
 }
 
-func fqdn(group containerinstance.ContainerGroup, region string) string {
+// FQDN retrieve the fully qualified domain name for a ContainerGroup
+func FQDN(group containerinstance.ContainerGroup, region string) string {
 	fqdn := ""
 	if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
 		fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
@@ -348,7 +349,7 @@ func ContainerGroupToContainer(containerID string, cg containerinstance.Containe
 
 	hostConfig := ToHostConfig(cc, cg)
 	config := &containers.RuntimeConfig{
-		FQDN: fqdn(cg, region),
+		FQDN: FQDN(cg, region),
 		Env:  envVars,
 	}
 

+ 1 - 1
api/client/compose.go

@@ -60,7 +60,7 @@ func (c *composeService) Logs(context.Context, string, compose.LogConsumer) erro
 	return errdefs.ErrNotImplemented
 }
 
-func (c *composeService) Ps(context.Context, string) ([]compose.ServiceStatus, error) {
+func (c *composeService) Ps(context.Context, string) ([]compose.ContainerSummary, error) {
 	return nil, errdefs.ErrNotImplemented
 }
 

+ 11 - 1
api/compose/api.go

@@ -41,7 +41,7 @@ type Service interface {
 	// Logs executes the equivalent to a `compose logs`
 	Logs(ctx context.Context, projectName string, consumer LogConsumer) error
 	// Ps executes the equivalent to a `compose ps`
-	Ps(ctx context.Context, projectName string) ([]ServiceStatus, error)
+	Ps(ctx context.Context, projectName string) ([]ContainerSummary, error)
 	// List executes the equivalent to a `docker stack ls`
 	List(ctx context.Context, projectName string) ([]Stack, error)
 	// Convert translate compose model into backend's native format
@@ -56,6 +56,16 @@ type PortPublisher struct {
 	Protocol      string
 }
 
+// ContainerSummary hold high-level description of a container
+type ContainerSummary struct {
+	ID         string
+	Name       string
+	Project    string
+	Service    string
+	State      string
+	Publishers []PortPublisher
+}
+
 // ServiceStatus hold status about a service
 type ServiceStatus struct {
 	ID         string

+ 20 - 30
cli/cmd/compose/ps.go

@@ -21,12 +21,12 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"sort"
 	"strings"
 
 	"github.com/spf13/cobra"
 
 	"github.com/docker/compose-cli/api/client"
-	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/formatter"
 )
 
@@ -54,44 +54,34 @@ func runPs(ctx context.Context, opts composeOptions) error {
 	if err != nil {
 		return err
 	}
-	serviceList, err := c.ComposeService().Ps(ctx, projectName)
+	containers, err := c.ComposeService().Ps(ctx, projectName)
 	if err != nil {
 		return err
 	}
 	if opts.Quiet {
-		for _, s := range serviceList {
+		for _, s := range containers {
 			fmt.Println(s.ID)
 		}
 		return nil
 	}
-	view := viewFromServiceStatusList(serviceList)
-	return formatter.Print(view, opts.Format, os.Stdout,
+
+	sort.Slice(containers, func(i, j int) bool {
+		return containers[i].Name < containers[j].Name
+	})
+
+	return formatter.Print(containers, opts.Format, os.Stdout,
 		func(w io.Writer) {
-			for _, service := range view {
-				_, _ = fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", "))
+			for _, container := range containers {
+				var ports []string
+				for _, p := range container.Publishers {
+					if p.URL == "" {
+						ports = append(ports, fmt.Sprintf("%d/%s", p.TargetPort, p.Protocol))
+					} else {
+						ports = append(ports, fmt.Sprintf("%s->%d/%s", p.URL, p.TargetPort, p.Protocol))
+					}
+				}
+				_, _ = fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", container.Name, container.Service, container.State, strings.Join(ports, ", "))
 			}
 		},
-		"ID", "NAME", "REPLICAS", "PORTS")
-}
-
-type serviceStatusView struct {
-	ID       string
-	Name     string
-	Replicas int
-	Desired  int
-	Ports    []string
-}
-
-func viewFromServiceStatusList(serviceStatusList []compose.ServiceStatus) []serviceStatusView {
-	retList := make([]serviceStatusView, len(serviceStatusList))
-	for i, s := range serviceStatusList {
-		retList[i] = serviceStatusView{
-			ID:       s.ID,
-			Name:     s.Name,
-			Replicas: s.Replicas,
-			Desired:  s.Desired,
-			Ports:    s.Ports,
-		}
-	}
-	return retList
+		"NAME", "SERVICE", "STATE", "PORTS")
 }

+ 1 - 0
ecs/aws.go

@@ -63,6 +63,7 @@ type API interface {
 	DeleteSecret(ctx context.Context, id string, recover bool) error
 	GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) error
 	DescribeService(ctx context.Context, cluster string, arn string) (compose.ServiceStatus, error)
+	DescribeServiceTasks(ctx context.Context, cluster string, project string, service string) ([]compose.ContainerSummary, error)
 	getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error)
 	ListTasks(ctx context.Context, cluster string, family string) ([]string, error)
 	GetPublicIPs(ctx context.Context, interfaces ...string) (map[string]string, error)

+ 15 - 0
ecs/aws_mock.go

@@ -224,6 +224,21 @@ func (mr *MockAPIMockRecorder) DescribeService(arg0, arg1, arg2 interface{}) *go
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeService", reflect.TypeOf((*MockAPI)(nil).DescribeService), arg0, arg1, arg2)
 }
 
+// DescribeServiceTasks mocks base method
+func (m *MockAPI) DescribeServiceTasks(arg0 context.Context, arg1, arg2, arg3 string) ([]compose.ContainerSummary, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "DescribeServiceTasks", arg0, arg1, arg2, arg3)
+	ret0, _ := ret[0].([]compose.ContainerSummary)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// DescribeServiceTasks indicates an expected call of DescribeServiceTasks
+func (mr *MockAPIMockRecorder) DescribeServiceTasks(arg0, arg1, arg2, arg3 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeServiceTasks", reflect.TypeOf((*MockAPI)(nil).DescribeServiceTasks), arg0, arg1, arg2, arg3)
+}
+
 // DescribeStackEvents mocks base method
 func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) {
 	m.ctrl.T.Helper()

+ 1 - 1
ecs/local/compose.go

@@ -207,7 +207,7 @@ func (e ecsLocalSimulation) Logs(ctx context.Context, projectName string, consum
 	return cmd.Run()
 }
 
-func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
+func (e ecsLocalSimulation) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
 	return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps")
 }
 func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {

+ 14 - 16
ecs/ps.go

@@ -18,13 +18,11 @@ package ecs
 
 import (
 	"context"
-	"fmt"
-	"strings"
 
 	"github.com/docker/compose-cli/api/compose"
 )
 
-func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
+func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.ContainerSummary, error) {
 	cluster, err := b.aws.GetStackClusterID(ctx, project)
 	if err != nil {
 		return nil, err
@@ -38,23 +36,23 @@ func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.Servi
 		return nil, nil
 	}
 
-	status := []compose.ServiceStatus{}
+	summary := []compose.ContainerSummary{}
 	for _, arn := range servicesARN {
-		state, err := b.aws.DescribeService(ctx, cluster, arn)
+		service, err := b.aws.DescribeService(ctx, cluster, arn)
 		if err != nil {
 			return nil, err
 		}
-		ports := []string{}
-		for _, lb := range state.Publishers {
-			ports = append(ports, fmt.Sprintf(
-				"%s:%d->%d/%s",
-				lb.URL,
-				lb.PublishedPort,
-				lb.TargetPort,
-				strings.ToLower(lb.Protocol)))
+
+		tasks, err := b.aws.DescribeServiceTasks(ctx, cluster, project, service.Name)
+		if err != nil {
+			return nil, err
+		}
+
+		for i, t := range tasks {
+			t.Publishers = service.Publishers
+			tasks[i] = t
 		}
-		state.Ports = ports
-		status = append(status, state)
+		summary = append(summary, tasks...)
 	}
-	return status, nil
+	return summary, nil
 }

+ 65 - 2
ecs/sdk.go

@@ -819,6 +819,69 @@ func (s sdk) DescribeService(ctx context.Context, cluster string, arn string) (c
 	}, nil
 }
 
+func (s sdk) DescribeServiceTasks(ctx context.Context, cluster string, project string, service string) ([]compose.ContainerSummary, error) {
+	var summary []compose.ContainerSummary
+	familly := fmt.Sprintf("%s-%s", project, service)
+	var token *string
+	for {
+		list, err := s.ECS.ListTasks(&ecs.ListTasksInput{
+			Cluster:    aws.String(cluster),
+			Family:     aws.String(familly),
+			LaunchType: nil,
+			MaxResults: nil,
+			NextToken:  token,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		if len(list.TaskArns) == 0 {
+			break
+		}
+		tasks, err := s.ECS.DescribeTasksWithContext(ctx, &ecs.DescribeTasksInput{
+			Cluster: aws.String(cluster),
+			Include: aws.StringSlice([]string{"TAGS"}),
+			Tasks:   list.TaskArns,
+		})
+		if err != nil {
+			return nil, err
+		}
+
+		for _, t := range tasks.Tasks {
+			var project string
+			var service string
+			for _, tag := range t.Tags {
+				switch aws.StringValue(tag.Key) {
+				case compose.ProjectTag:
+					project = aws.StringValue(tag.Value)
+				case compose.ServiceTag:
+					service = aws.StringValue(tag.Value)
+				}
+			}
+
+			id, err := arn.Parse(aws.StringValue(t.TaskArn))
+			if err != nil {
+				return nil, err
+			}
+
+			summary = append(summary, compose.ContainerSummary{
+				ID:      id.String(),
+				Name:    id.Resource,
+				Project: project,
+				Service: service,
+				State:   strings.Title(strings.ToLower(aws.StringValue(t.LastStatus))),
+			})
+		}
+
+		if list.NextToken == token {
+			break
+		}
+		token = list.NextToken
+	}
+
+	return summary, nil
+}
+
 func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
 	if len(targetGroupArns) == 0 {
 		return nil, nil
@@ -861,10 +924,10 @@ func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string
 				continue
 			}
 			loadBalancers = append(loadBalancers, compose.PortPublisher{
-				URL:           aws.StringValue(lb.DNSName),
+				URL:           fmt.Sprintf("%s:%d", aws.StringValue(lb.DNSName), aws.Int64Value(tg.Port)),
 				TargetPort:    int(aws.Int64Value(tg.Port)),
 				PublishedPort: int(aws.Int64Value(tg.Port)),
-				Protocol:      aws.StringValue(tg.Protocol),
+				Protocol:      strings.ToLower(aws.StringValue(tg.Protocol)),
 			})
 
 		}

+ 1 - 1
example/backend.go

@@ -169,7 +169,7 @@ func (cs *composeService) Down(ctx context.Context, project string) error {
 	return nil
 }
 
-func (cs *composeService) Ps(ctx context.Context, project string) ([]compose.ServiceStatus, error) {
+func (cs *composeService) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
 	return nil, errdefs.ErrNotImplemented
 }
 func (cs *composeService) List(ctx context.Context, project string) ([]compose.Stack, error) {

+ 0 - 143
local/compose/compose_test.go

@@ -1,143 +0,0 @@
-/*
-   Copyright 2020 Docker Compose CLI 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 compose
-
-import (
-	"path/filepath"
-	"testing"
-
-	composetypes "github.com/compose-spec/compose-go/types"
-	"github.com/docker/docker/api/types"
-	mountTypes "github.com/docker/docker/api/types/mount"
-	"gotest.tools/v3/assert"
-
-	"github.com/docker/compose-cli/api/compose"
-)
-
-func TestContainersToStacks(t *testing.T) {
-	containers := []types.Container{
-		{
-			ID:     "service1",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project1"},
-		},
-		{
-			ID:     "service2",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project1"},
-		},
-		{
-			ID:     "service3",
-			State:  "running",
-			Labels: map[string]string{projectLabel: "project2"},
-		},
-	}
-	stacks, err := containersToStacks(containers)
-	assert.NilError(t, err)
-	assert.DeepEqual(t, stacks, []compose.Stack{
-		{
-			ID:     "project1",
-			Name:   "project1",
-			Status: "running(2)",
-		},
-		{
-			ID:     "project2",
-			Name:   "project2",
-			Status: "running(1)",
-		},
-	})
-}
-
-func TestContainersToServiceStatus(t *testing.T) {
-	containers := []types.Container{
-		{
-			ID:     "c1",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c2",
-			State:  "exited",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c3",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service1"},
-		},
-		{
-			ID:     "c4",
-			State:  "running",
-			Labels: map[string]string{serviceLabel: "service2"},
-		},
-	}
-	services, err := containersToServiceStatus(containers)
-	assert.NilError(t, err)
-	assert.DeepEqual(t, services, []compose.ServiceStatus{
-		{
-			ID:       "service1",
-			Name:     "service1",
-			Replicas: 2,
-			Desired:  3,
-		},
-		{
-			ID:       "service2",
-			Name:     "service2",
-			Replicas: 1,
-			Desired:  1,
-		},
-	})
-}
-
-func TestStacksMixedStatus(t *testing.T) {
-	assert.Equal(t, combinedStatus([]string{"running"}), "running(1)")
-	assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
-	assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
-}
-
-func TestBuildBindMount(t *testing.T) {
-	project := composetypes.Project{}
-	volume := composetypes.ServiceVolumeConfig{
-		Type:   composetypes.VolumeTypeBind,
-		Source: "e2e/volume-test",
-		Target: "/data",
-	}
-	mount, err := buildMount(project, volume)
-	assert.NilError(t, err)
-	assert.Assert(t, filepath.IsAbs(mount.Source))
-	assert.Equal(t, mount.Type, mountTypes.TypeBind)
-}
-
-func TestBuildVolumeMount(t *testing.T) {
-	project := composetypes.Project{
-		Name: "myProject",
-		Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{
-			"myVolume": {
-				Name: "myProject_myVolume",
-			},
-		}),
-	}
-	volume := composetypes.ServiceVolumeConfig{
-		Type:   composetypes.VolumeTypeVolume,
-		Source: "myVolume",
-		Target: "/data",
-	}
-	mount, err := buildMount(project, volume)
-	assert.NilError(t, err)
-	assert.Equal(t, mount.Source, "myProject_myVolume")
-	assert.Equal(t, mount.Type, mountTypes.TypeVolume)
-}

+ 62 - 0
local/compose/create_test.go

@@ -0,0 +1,62 @@
+/*
+   Copyright 2020 Docker Compose CLI 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 compose
+
+import (
+	"os"
+	"path/filepath"
+	"testing"
+
+	composetypes "github.com/compose-spec/compose-go/types"
+	mountTypes "github.com/docker/docker/api/types/mount"
+	"gotest.tools/v3/assert"
+)
+
+func TestBuildBindMount(t *testing.T) {
+	project := composetypes.Project{}
+	volume := composetypes.ServiceVolumeConfig{
+		Type:   composetypes.VolumeTypeBind,
+		Source: "",
+		Target: "/data",
+	}
+	mount, err := buildMount(project, volume)
+	assert.NilError(t, err)
+	assert.Assert(t, filepath.IsAbs(mount.Source))
+	_, err = os.Stat(mount.Source)
+	assert.NilError(t, err)
+	assert.Equal(t, mount.Type, mountTypes.TypeBind)
+}
+
+func TestBuildVolumeMount(t *testing.T) {
+	project := composetypes.Project{
+		Name: "myProject",
+		Volumes: composetypes.Volumes(map[string]composetypes.VolumeConfig{
+			"myVolume": {
+				Name: "myProject_myVolume",
+			},
+		}),
+	}
+	volume := composetypes.ServiceVolumeConfig{
+		Type:   composetypes.VolumeTypeVolume,
+		Source: "myVolume",
+		Target: "/data",
+	}
+	mount, err := buildMount(project, volume)
+	assert.NilError(t, err)
+	assert.Equal(t, mount.Source, "myProject_myVolume")
+	assert.Equal(t, mount.Type, mountTypes.TypeVolume)
+}

+ 66 - 0
local/compose/ls_test.go

@@ -0,0 +1,66 @@
+/*
+   Copyright 2020 Docker Compose CLI 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 compose
+
+import (
+	"testing"
+
+	"github.com/docker/compose-cli/api/compose"
+
+	moby "github.com/docker/docker/api/types"
+	"gotest.tools/v3/assert"
+)
+
+func TestContainersToStacks(t *testing.T) {
+	containers := []moby.Container{
+		{
+			ID:     "service1",
+			State:  "running",
+			Labels: map[string]string{projectLabel: "project1"},
+		},
+		{
+			ID:     "service2",
+			State:  "running",
+			Labels: map[string]string{projectLabel: "project1"},
+		},
+		{
+			ID:     "service3",
+			State:  "running",
+			Labels: map[string]string{projectLabel: "project2"},
+		},
+	}
+	stacks, err := containersToStacks(containers)
+	assert.NilError(t, err)
+	assert.DeepEqual(t, stacks, []compose.Stack{
+		{
+			ID:     "project1",
+			Name:   "project1",
+			Status: "running(2)",
+		},
+		{
+			ID:     "project2",
+			Name:   "project2",
+			Status: "running(1)",
+		},
+	})
+}
+
+func TestStacksMixedStatus(t *testing.T) {
+	assert.Equal(t, combinedStatus([]string{"running"}), "running(1)")
+	assert.Equal(t, combinedStatus([]string{"running", "running", "running"}), "running(3)")
+	assert.Equal(t, combinedStatus([]string{"running", "exited", "running"}), "exited(1), running(2)")
+}

+ 24 - 24
local/compose/ps.go

@@ -21,15 +21,13 @@ import (
 	"fmt"
 	"sort"
 
-	convert "github.com/docker/compose-cli/local/moby"
-
 	"github.com/docker/compose-cli/api/compose"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 )
 
-func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ServiceStatus, error) {
-	list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
+func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
+	containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{
 		Filters: filters.NewArgs(
 			projectFilter(projectName),
 		),
@@ -37,31 +35,33 @@ func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.
 	if err != nil {
 		return nil, err
 	}
-	return containersToServiceStatus(list)
-}
 
-func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) {
-	containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel)
-	if err != nil {
-		return nil, err
-	}
-	var services []compose.ServiceStatus
-	for _, service := range keys {
-		containers := containersByLabel[service]
-		runnningContainers := []moby.Container{}
-		for _, container := range containers {
-			if container.State == convert.ContainerRunning {
-				runnningContainers = append(runnningContainers, container)
+	var summary []compose.ContainerSummary
+	for _, c := range containers {
+		var publishers []compose.PortPublisher
+		for _, p := range c.Ports {
+			var url string
+			if p.PublicPort != 0 {
+				url = fmt.Sprintf("%s:%d", p.IP, p.PublicPort)
 			}
+			publishers = append(publishers, compose.PortPublisher{
+				URL:           url,
+				TargetPort:    int(p.PrivatePort),
+				PublishedPort: int(p.PublicPort),
+				Protocol:      p.Type,
+			})
 		}
-		services = append(services, compose.ServiceStatus{
-			ID:       service,
-			Name:     service,
-			Desired:  len(containers),
-			Replicas: len(runnningContainers),
+
+		summary = append(summary, compose.ContainerSummary{
+			ID:         c.ID,
+			Name:       getContainerName(c),
+			Project:    c.Labels[projectLabel],
+			Service:    c.Labels[serviceLabel],
+			State:      c.State,
+			Publishers: publishers,
 		})
 	}
-	return services, nil
+	return summary, nil
 }
 
 func groupContainerByLabel(containers []moby.Container, labelName string) (map[string][]moby.Container, []string, error) {

+ 4 - 3
server/proxy/compose.go

@@ -54,11 +54,12 @@ func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServices
 		}
 		projectName = project.Name
 	}
-	services, err := Client(ctx).ComposeService().Ps(ctx, projectName)
+	response := []*composev1.Service{}
+	_, err := Client(ctx).ComposeService().Ps(ctx, projectName)
 	if err != nil {
 		return nil, err
 	}
-	response := []*composev1.Service{}
+	/* FIXME need to create `docker service ls` command to re-introduce this feature
 	for _, service := range services {
 		response = append(response, &composev1.Service{
 			Id:       service.ID,
@@ -67,7 +68,7 @@ func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServices
 			Desired:  uint32(service.Desired),
 			Ports:    service.Ports,
 		})
-	}
+	}*/
 	return &composev1.ComposeServicesResponse{Services: response}, nil
 }
 

+ 9 - 6
tests/aci-e2e/e2e-aci_test.go

@@ -692,7 +692,7 @@ func TestUpUpdate(t *testing.T) {
 		for _, l := range out {
 			if strings.Contains(l, serverContainer) {
 				webRunning = true
-				strings.Contains(l, ":80->80/tcp")
+				assert.Check(t, strings.Contains(l, ":80->80/tcp"))
 			}
 		}
 		assert.Assert(t, webRunning, "web container not running ; ps:\n"+res.Stdout())
@@ -734,20 +734,23 @@ func TestUpUpdate(t *testing.T) {
 		var wordsDisplayed, webDisplayed, dbDisplayed bool
 		for _, line := range l {
 			fields := strings.Fields(line)
-			containerID := fields[0]
-			switch containerID {
+			name := fields[0]
+			switch name {
 			case wordsContainer:
 				wordsDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, "words", "1/1"})
+				assert.Equal(t, fields[2], "Running")
 			case dbContainer:
 				dbDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, "db", "1/1"})
+				assert.Equal(t, fields[2], "Running")
 			case serverContainer:
 				webDisplayed = true
-				assert.Equal(t, fields[1], "web")
+				assert.Equal(t, fields[2], "Running")
 				assert.Check(t, strings.Contains(fields[3], ":80->80/tcp"))
 			}
 		}
+		assert.Check(t, webDisplayed, "webDisplayed"+res.Stdout())
+		assert.Check(t, wordsDisplayed, "wordsDisplayed"+res.Stdout())
+		assert.Check(t, dbDisplayed, "dbDisplayed"+res.Stdout())
 		assert.Check(t, webDisplayed && wordsDisplayed && dbDisplayed, "\n%s\n", res.Stdout())
 	})
 

+ 1 - 1
tests/ecs-e2e/e2e-ecs_test.go

@@ -100,7 +100,7 @@ func TestCompose(t *testing.T) {
 			switch serviceName {
 			case "db":
 				dbDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, serviceName, "1/1"})
+				assert.DeepEqual(t, fields, []string{containerID, serviceName, "Running"})
 			case "words":
 				wordsDisplayed = true
 				assert.Check(t, strings.Contains(fields[3], ":8080->8080/tcp"))