From 79af862613e47134e298f32b7f0adf4a0e671760 Mon Sep 17 00:00:00 2001 From: aiordache Date: Fri, 5 Mar 2021 12:40:56 +0100 Subject: [PATCH] Add `compose top` command Signed-off-by: aiordache --- aci/compose.go | 3 ++ api/client/compose.go | 4 ++ api/compose/api.go | 10 ++++ cli/cmd/compose/compose.go | 1 + cli/cmd/compose/top.go | 95 ++++++++++++++++++++++++++++++++++++++ ecs/local/compose.go | 4 ++ ecs/top.go | 28 +++++++++++ kube/compose.go | 4 ++ local/compose/top.go | 68 +++++++++++++++++++++++++++ 9 files changed, 217 insertions(+) create mode 100644 cli/cmd/compose/top.go create mode 100644 ecs/top.go create mode 100644 local/compose/top.go diff --git a/aci/compose.go b/aci/compose.go index f004ddbd..6ebe9729 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -226,3 +226,6 @@ func (cs *aciComposeService) Remove(ctx context.Context, project *types.Project, func (cs *aciComposeService) Exec(ctx context.Context, project *types.Project, opts compose.RunOptions) error { return errdefs.ErrNotImplemented } +func (cs *aciComposeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/api/client/compose.go b/api/client/compose.go index f68378b8..a04b7c3e 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -99,3 +99,7 @@ func (c *composeService) Pause(ctx context.Context, project *types.Project) erro func (c *composeService) UnPause(ctx context.Context, project *types.Project) error { return errdefs.ErrNotImplemented } + +func (c *composeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/api/compose/api.go b/api/compose/api.go index 02d2ef31..293df867 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -63,6 +63,8 @@ type Service interface { Pause(ctx context.Context, project *types.Project) error // UnPause executes the equivalent to a `compose unpause` UnPause(ctx context.Context, project *types.Project) error + // Top executes the equivalent to a `compose top` + Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error) } // BuildOptions group options of the Build API @@ -214,6 +216,14 @@ type ContainerSummary struct { Publishers []PortPublisher } +// ContainerProcSummary holds container processes top data +type ContainerProcSummary struct { + ID string + Name string + Processes [][]string + Titles []string +} + // ServiceStatus hold status about a service type ServiceStatus struct { ID string diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index 7f01f8c5..b3cfb420 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -135,6 +135,7 @@ func Command(contextType string) *cobra.Command { execCommand(&opts), pauseCommand(&opts), unpauseCommand(&opts), + topCommand(&opts), ) if contextType == store.LocalContextType || contextType == store.DefaultContextType { diff --git a/cli/cmd/compose/top.go b/cli/cmd/compose/top.go new file mode 100644 index 00000000..c7d839cc --- /dev/null +++ b/cli/cmd/compose/top.go @@ -0,0 +1,95 @@ +/* + 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 ( + "context" + "fmt" + "io" + "os" + "sort" + "strings" + "text/tabwriter" + + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/client" +) + +type topOptions struct { + *projectOptions +} + +func topCommand(p *projectOptions) *cobra.Command { + opts := topOptions{ + projectOptions: p, + } + topCmd := &cobra.Command{ + Use: "top", + Short: "Display the running processes", + RunE: func(cmd *cobra.Command, args []string) error { + return runTop(cmd.Context(), opts, args) + }, + } + return topCmd +} + +func runTop(ctx context.Context, opts topOptions, services []string) error { + c, err := client.NewWithDefaultLocalBackend(ctx) + if err != nil { + return err + } + projectName, err := opts.toProjectName() + if err != nil { + return err + } + containers, err := c.ComposeService().Top(ctx, projectName, services) + if err != nil { + return err + } + + sort.Slice(containers, func(i, j int) bool { + return containers[i].Name < containers[j].Name + }) + + for _, container := range containers { + fmt.Printf("%s\n", container.Name) + err := psPrinter(os.Stdout, func(w io.Writer) { + for _, proc := range container.Processes { + info := []interface{}{} + for _, p := range proc { + info = append(info, p) + } + _, _ = fmt.Fprintf(w, strings.Repeat("%s\t", len(info))+"\n", info...) + + } + fmt.Fprintln(w) + }, + container.Titles...) + if err != nil { + return err + } + } + return nil +} + +func psPrinter(out io.Writer, printer func(writer io.Writer), headers ...string) error { + w := tabwriter.NewWriter(out, 5, 1, 3, ' ', 0) + _, _ = fmt.Fprintln(w, strings.Join(headers, "\t")) + printer(w) + return w.Flush() +} diff --git a/ecs/local/compose.go b/ecs/local/compose.go index f8a8da8e..024b4255 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -191,3 +191,7 @@ func (e ecsLocalSimulation) Pause(ctx context.Context, project *types.Project) e func (e ecsLocalSimulation) UnPause(ctx context.Context, project *types.Project) error { return e.compose.UnPause(ctx, project) } + +func (e ecsLocalSimulation) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + return e.compose.Top(ctx, projectName, services) +} diff --git a/ecs/top.go b/ecs/top.go new file mode 100644 index 00000000..569a7c65 --- /dev/null +++ b/ecs/top.go @@ -0,0 +1,28 @@ +/* + 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 ecs + +import ( + "context" + + "github.com/docker/compose-cli/api/compose" + "github.com/docker/compose-cli/api/errdefs" +) + +func (b *ecsAPIService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/kube/compose.go b/kube/compose.go index 08a30851..720bf757 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -254,3 +254,7 @@ func (s *composeService) Pause(ctx context.Context, project *types.Project) erro func (s *composeService) UnPause(ctx context.Context, project *types.Project) error { return errdefs.ErrNotImplemented } + +func (s *composeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + return nil, errdefs.ErrNotImplemented +} diff --git a/local/compose/top.go b/local/compose/top.go new file mode 100644 index 00000000..1a83eeb5 --- /dev/null +++ b/local/compose/top.go @@ -0,0 +1,68 @@ +/* + 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 ( + "context" + + "github.com/docker/compose-cli/api/compose" + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" + "golang.org/x/sync/errgroup" +) + +func (s *composeService) Top(ctx context.Context, projectName string, services []string) ([]compose.ContainerProcSummary, error) { + containers, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs(projectFilter(projectName)), + }) + if err != nil { + return nil, err + } + + ignore := func(string) bool { + return false + } + if len(services) > 0 { + ignore = func(s string) bool { + return !contains(services, s) + } + } + summary := make([]compose.ContainerProcSummary, len(containers)) + eg, ctx := errgroup.WithContext(ctx) + for i, c := range containers { + container := c + service := c.Labels[serviceLabel] + if ignore(service) { + continue + } + i := i + eg.Go(func() error { + topContent, err := s.apiClient.ContainerTop(ctx, container.ID, []string{}) + if err != nil { + return err + } + summary[i] = compose.ContainerProcSummary{ + ID: container.ID, + Name: getCanonicalContainerName(container), + Processes: topContent.Processes, + Titles: topContent.Titles, + } + return nil + }) + } + return summary, eg.Wait() +}