瀏覽代碼

Merge pull request #1019 from docker/ps_containers

Nicolas De loof 5 年之前
父節點
當前提交
a95a76291b

+ 22 - 3
aci/compose.go

@@ -29,6 +29,7 @@ import (
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/context/store"
 	"github.com/docker/compose-cli/errdefs"
 	"github.com/docker/compose-cli/errdefs"
+	"github.com/docker/compose-cli/utils/formatter"
 )
 )
 
 
 type aciComposeService struct {
 type aciComposeService struct {
@@ -119,7 +120,7 @@ func (cs *aciComposeService) Down(ctx context.Context, project string) error {
 	return err
 	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)
 	groupsClient, err := login.NewContainerGroupsClient(cs.ctx.SubscriptionID)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		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)
 		return nil, fmt.Errorf("no containers found in ACI container group %s", project)
 	}
 	}
 
 
-	res := []compose.ServiceStatus{}
+	res := []compose.ContainerSummary{}
 	for _, container := range *group.Containers {
 	for _, container := range *group.Containers {
 		if isContainerVisible(container, group, false) {
 		if isContainerVisible(container, group, false) {
 			continue
 			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
 	return res, nil
 }
 }

+ 4 - 3
aci/convert/convert.go

@@ -314,13 +314,14 @@ func ContainerGroupToServiceStatus(containerID string, group containerinstance.C
 	return compose.ServiceStatus{
 	return compose.ServiceStatus{
 		ID:       containerID,
 		ID:       containerID,
 		Name:     *container.Name,
 		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,
 		Replicas: replicas,
 		Desired:  1,
 		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 := ""
 	fqdn := ""
 	if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
 	if group.IPAddress != nil && group.IPAddress.DNSNameLabel != nil && *group.IPAddress.DNSNameLabel != "" {
 		fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
 		fqdn = *group.IPAddress.DNSNameLabel + "." + region + ".azurecontainer.io"
@@ -348,7 +349,7 @@ func ContainerGroupToContainer(containerID string, cg containerinstance.Containe
 
 
 	hostConfig := ToHostConfig(cc, cg)
 	hostConfig := ToHostConfig(cc, cg)
 	config := &containers.RuntimeConfig{
 	config := &containers.RuntimeConfig{
-		FQDN: fqdn(cg, region),
+		FQDN: FQDN(cg, region),
 		Env:  envVars,
 		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
 	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
 	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 executes the equivalent to a `compose logs`
 	Logs(ctx context.Context, projectName string, consumer LogConsumer) error
 	Logs(ctx context.Context, projectName string, consumer LogConsumer) error
 	// Ps executes the equivalent to a `compose ps`
 	// 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 executes the equivalent to a `docker stack ls`
 	List(ctx context.Context, projectName string) ([]Stack, error)
 	List(ctx context.Context, projectName string) ([]Stack, error)
 	// Convert translate compose model into backend's native format
 	// Convert translate compose model into backend's native format
@@ -56,6 +56,16 @@ type PortPublisher struct {
 	Protocol      string
 	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
 // ServiceStatus hold status about a service
 type ServiceStatus struct {
 type ServiceStatus struct {
 	ID         string
 	ID         string

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

@@ -21,12 +21,12 @@ import (
 	"fmt"
 	"fmt"
 	"io"
 	"io"
 	"os"
 	"os"
+	"sort"
 	"strings"
 	"strings"
 
 
 	"github.com/spf13/cobra"
 	"github.com/spf13/cobra"
 
 
 	"github.com/docker/compose-cli/api/client"
 	"github.com/docker/compose-cli/api/client"
-	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/formatter"
 	"github.com/docker/compose-cli/formatter"
 )
 )
 
 
@@ -54,44 +54,34 @@ func runPs(ctx context.Context, opts composeOptions) error {
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
-	serviceList, err := c.ComposeService().Ps(ctx, projectName)
+	containers, err := c.ComposeService().Ps(ctx, projectName)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
 	if opts.Quiet {
 	if opts.Quiet {
-		for _, s := range serviceList {
+		for _, s := range containers {
 			fmt.Println(s.ID)
 			fmt.Println(s.ID)
 		}
 		}
 		return nil
 		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) {
 		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
 	DeleteSecret(ctx context.Context, id string, recover bool) error
 	GetLogs(ctx context.Context, name string, consumer func(service, container, message string)) 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)
 	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)
 	getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error)
 	ListTasks(ctx context.Context, cluster string, family string) ([]string, error)
 	ListTasks(ctx context.Context, cluster string, family string) ([]string, error)
 	GetPublicIPs(ctx context.Context, interfaces ...string) (map[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)
 	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
 // DescribeStackEvents mocks base method
 func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) {
 func (m *MockAPI) DescribeStackEvents(arg0 context.Context, arg1 string) ([]*cloudformation.StackEvent, error) {
 	m.ctrl.T.Helper()
 	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()
 	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")
 	return nil, errors.Wrap(errdefs.ErrNotImplemented, "use docker-compose ps")
 }
 }
 func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {
 func (e ecsLocalSimulation) List(ctx context.Context, projectName string) ([]compose.Stack, error) {

+ 14 - 16
ecs/ps.go

@@ -18,13 +18,11 @@ package ecs
 
 
 import (
 import (
 	"context"
 	"context"
-	"fmt"
-	"strings"
 
 
 	"github.com/docker/compose-cli/api/compose"
 	"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)
 	cluster, err := b.aws.GetStackClusterID(ctx, project)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -38,23 +36,23 @@ func (b *ecsAPIService) Ps(ctx context.Context, project string) ([]compose.Servi
 		return nil, nil
 		return nil, nil
 	}
 	}
 
 
-	status := []compose.ServiceStatus{}
+	summary := []compose.ContainerSummary{}
 	for _, arn := range servicesARN {
 	for _, arn := range servicesARN {
-		state, err := b.aws.DescribeService(ctx, cluster, arn)
+		service, err := b.aws.DescribeService(ctx, cluster, arn)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			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
 	}, 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) {
 func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string) ([]compose.PortPublisher, error) {
 	if len(targetGroupArns) == 0 {
 	if len(targetGroupArns) == 0 {
 		return nil, nil
 		return nil, nil
@@ -861,10 +924,10 @@ func (s sdk) getURLWithPortMapping(ctx context.Context, targetGroupArns []string
 				continue
 				continue
 			}
 			}
 			loadBalancers = append(loadBalancers, compose.PortPublisher{
 			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)),
 				TargetPort:    int(aws.Int64Value(tg.Port)),
 				PublishedPort: 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
 	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
 	return nil, errdefs.ErrNotImplemented
 }
 }
 func (cs *composeService) List(ctx context.Context, project string) ([]compose.Stack, error) {
 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"
 	"fmt"
 	"sort"
 	"sort"
 
 
-	convert "github.com/docker/compose-cli/local/moby"
-
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/compose"
 	moby "github.com/docker/docker/api/types"
 	moby "github.com/docker/docker/api/types"
 	"github.com/docker/docker/api/types/filters"
 	"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(
 		Filters: filters.NewArgs(
 			projectFilter(projectName),
 			projectFilter(projectName),
 		),
 		),
@@ -37,31 +35,33 @@ func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.
 	if err != nil {
 	if err != nil {
 		return nil, err
 		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) {
 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
 		projectName = project.Name
 	}
 	}
-	services, err := Client(ctx).ComposeService().Ps(ctx, projectName)
+	response := []*composev1.Service{}
+	_, err := Client(ctx).ComposeService().Ps(ctx, projectName)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	response := []*composev1.Service{}
+	/* FIXME need to create `docker service ls` command to re-introduce this feature
 	for _, service := range services {
 	for _, service := range services {
 		response = append(response, &composev1.Service{
 		response = append(response, &composev1.Service{
 			Id:       service.ID,
 			Id:       service.ID,
@@ -67,7 +68,7 @@ func (p *proxy) Services(ctx context.Context, request *composev1.ComposeServices
 			Desired:  uint32(service.Desired),
 			Desired:  uint32(service.Desired),
 			Ports:    service.Ports,
 			Ports:    service.Ports,
 		})
 		})
-	}
+	}*/
 	return &composev1.ComposeServicesResponse{Services: response}, nil
 	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 {
 		for _, l := range out {
 			if strings.Contains(l, serverContainer) {
 			if strings.Contains(l, serverContainer) {
 				webRunning = true
 				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())
 		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
 		var wordsDisplayed, webDisplayed, dbDisplayed bool
 		for _, line := range l {
 		for _, line := range l {
 			fields := strings.Fields(line)
 			fields := strings.Fields(line)
-			containerID := fields[0]
-			switch containerID {
+			name := fields[0]
+			switch name {
 			case wordsContainer:
 			case wordsContainer:
 				wordsDisplayed = true
 				wordsDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, "words", "1/1"})
+				assert.Equal(t, fields[2], "Running")
 			case dbContainer:
 			case dbContainer:
 				dbDisplayed = true
 				dbDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, "db", "1/1"})
+				assert.Equal(t, fields[2], "Running")
 			case serverContainer:
 			case serverContainer:
 				webDisplayed = true
 				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, 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())
 		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 {
 			switch serviceName {
 			case "db":
 			case "db":
 				dbDisplayed = true
 				dbDisplayed = true
-				assert.DeepEqual(t, fields, []string{containerID, serviceName, "1/1"})
+				assert.DeepEqual(t, fields, []string{containerID, serviceName, "Running"})
 			case "words":
 			case "words":
 				wordsDisplayed = true
 				wordsDisplayed = true
 				assert.Check(t, strings.Contains(fields[3], ":8080->8080/tcp"))
 				assert.Check(t, strings.Contains(fields[3], ":8080->8080/tcp"))