Merge pull request #1019 from docker/ps_containers

This commit is contained in:
Nicolas De loof 2020-12-09 15:04:55 +01:00 committed by GitHub
commit a95a76291b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 321 additions and 235 deletions

View File

@ -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
} }

View File

@ -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,
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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 { for _, container := range containers {
_, _ = fmt.Fprintf(w, "%s\t%s\t%d/%d\t%s\n", service.ID, service.Name, service.Replicas, service.Desired, strings.Join(service.Ports, ", ")) 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") "NAME", "SERVICE", "STATE", "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
} }

View File

@ -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)

View File

@ -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()

View File

@ -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) {

View File

@ -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 { tasks, err := b.aws.DescribeServiceTasks(ctx, cluster, project, service.Name)
ports = append(ports, fmt.Sprintf( if err != nil {
"%s:%d->%d/%s", return nil, err
lb.URL,
lb.PublishedPort,
lb.TargetPort,
strings.ToLower(lb.Protocol)))
} }
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
} }

View File

@ -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)),
}) })
} }

View File

@ -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) {

View File

@ -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)
}

View File

@ -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
local/compose/ls_test.go Normal file
View File

@ -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)")
}

View File

@ -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) { func (s *composeService) Ps(ctx context.Context, projectName string) ([]compose.ContainerSummary, error) {
list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ 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) { var summary []compose.ContainerSummary
containersByLabel, keys, err := groupContainerByLabel(containers, serviceLabel) for _, c := range containers {
if err != nil { var publishers []compose.PortPublisher
return nil, err for _, p := range c.Ports {
} var url string
var services []compose.ServiceStatus if p.PublicPort != 0 {
for _, service := range keys { url = fmt.Sprintf("%s:%d", p.IP, p.PublicPort)
containers := containersByLabel[service]
runnningContainers := []moby.Container{}
for _, container := range containers {
if container.State == convert.ContainerRunning {
runnningContainers = append(runnningContainers, container)
} }
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, summary = append(summary, compose.ContainerSummary{
Name: service, ID: c.ID,
Desired: len(containers), Name: getContainerName(c),
Replicas: len(runnningContainers), 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) {

View File

@ -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
} }

View File

@ -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] name := fields[0]
switch containerID { 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())
}) })

View File

@ -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"))