From 1d859dc8074c00d2073741df60ad202d0b48ecb3 Mon Sep 17 00:00:00 2001 From: Nicolas De Loof Date: Mon, 7 Dec 2020 14:46:36 +0100 Subject: [PATCH] `Ps` return ContainerSummary, not Services Signed-off-by: Nicolas De Loof --- aci/compose.go | 24 +++++++++++-- aci/convert/convert.go | 7 ++-- api/client/compose.go | 2 +- api/compose/api.go | 12 ++++++- cli/cmd/compose/ps.go | 50 +++++++++++--------------- ecs/aws.go | 1 + ecs/aws_mock.go | 15 ++++++++ ecs/local/compose.go | 2 +- ecs/ps.go | 30 ++++++++-------- ecs/sdk.go | 67 +++++++++++++++++++++++++++++++++-- example/backend.go | 2 +- local/compose/create_test.go | 1 + local/compose/ls_test.go | 1 + local/compose/ps.go | 32 +++++++++++++++-- server/proxy/compose.go | 7 ++-- tests/aci-e2e/e2e-aci_test.go | 6 ++-- 16 files changed, 192 insertions(+), 67 deletions(-) create mode 100644 local/compose/create_test.go create mode 100644 local/compose/ls_test.go diff --git a/aci/compose.go b/aci/compose.go index 93976e7ba..5fe89e62f 100644 --- a/aci/compose.go +++ b/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,29 @@ 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), + }) + } + res = append(res, compose.ContainerSummary{ + ID: *container.Name, + Name: *container.Name, + Project: project, + Service: *container.Name, + State: convert.GetStatus(container, group), + Publishers: publishers, + }) } return res, nil } diff --git a/aci/convert/convert.go b/aci/convert/convert.go index 733547cad..a3f72b58f 100644 --- a/aci/convert/convert.go +++ b/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, } diff --git a/api/client/compose.go b/api/client/compose.go index 3503e83a4..e139e20f2 100644 --- a/api/client/compose.go +++ b/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 } diff --git a/api/compose/api.go b/api/compose/api.go index 95cbb5b98..e1badf3da 100644 --- a/api/compose/api.go +++ b/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 diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index 235443d0d..e9ffe2951 100644 --- a/cli/cmd/compose/ps.go +++ b/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\n", container.Name, 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", "STATE", "PORTS") } diff --git a/ecs/aws.go b/ecs/aws.go index 782c25c8b..5cf50fbfa 100644 --- a/ecs/aws.go +++ b/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) diff --git a/ecs/aws_mock.go b/ecs/aws_mock.go index 428bc8c80..9d438bf29 100644 --- a/ecs/aws_mock.go +++ b/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() diff --git a/ecs/local/compose.go b/ecs/local/compose.go index f1ad7b58d..9bf64a5f9 100644 --- a/ecs/local/compose.go +++ b/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) { diff --git a/ecs/ps.go b/ecs/ps.go index e283668fc..a1c4ea7b6 100644 --- a/ecs/ps.go +++ b/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 } - state.Ports = ports - status = append(status, state) + + for i, t := range tasks { + t.Publishers = service.Publishers + tasks[i] = t + } + summary = append(summary, tasks...) } - return status, nil + return summary, nil } diff --git a/ecs/sdk.go b/ecs/sdk.go index d12e61710..e9b7abacc 100644 --- a/ecs/sdk.go +++ b/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: 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)), }) } diff --git a/example/backend.go b/example/backend.go index 540849505..23274f11d 100644 --- a/example/backend.go +++ b/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) { diff --git a/local/compose/create_test.go b/local/compose/create_test.go new file mode 100644 index 000000000..cfbe186c0 --- /dev/null +++ b/local/compose/create_test.go @@ -0,0 +1 @@ +package compose diff --git a/local/compose/ls_test.go b/local/compose/ls_test.go new file mode 100644 index 000000000..cfbe186c0 --- /dev/null +++ b/local/compose/ls_test.go @@ -0,0 +1 @@ +package compose diff --git a/local/compose/ps.go b/local/compose/ps.go index 39b1ecfaf..dafacda23 100644 --- a/local/compose/ps.go +++ b/local/compose/ps.go @@ -28,8 +28,8 @@ import ( "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,7 +37,33 @@ func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose. if err != nil { return nil, err } - return containersToServiceStatus(list) + + 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, + }) + } + + 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 summary, nil } func containersToServiceStatus(containers []moby.Container) ([]compose.ServiceStatus, error) { diff --git a/server/proxy/compose.go b/server/proxy/compose.go index 59afcf556..3c584d201 100644 --- a/server/proxy/compose.go +++ b/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 } diff --git a/tests/aci-e2e/e2e-aci_test.go b/tests/aci-e2e/e2e-aci_test.go index ccc950ee1..ee0702d6d 100644 --- a/tests/aci-e2e/e2e-aci_test.go +++ b/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()) @@ -738,10 +738,10 @@ func TestUpUpdate(t *testing.T) { switch containerID { case wordsContainer: wordsDisplayed = true - assert.DeepEqual(t, fields, []string{containerID, "words", "1/1"}) + assert.DeepEqual(t, fields, []string{containerID, "words", "Running"}) case dbContainer: dbDisplayed = true - assert.DeepEqual(t, fields, []string{containerID, "db", "1/1"}) + assert.DeepEqual(t, fields, []string{containerID, "db", "Running"}) case serverContainer: webDisplayed = true assert.Equal(t, fields[1], "web")