diff --git a/cmd/compose/attach.go b/cmd/compose/attach.go new file mode 100644 index 00000000000..3bc17134df6 --- /dev/null +++ b/cmd/compose/attach.go @@ -0,0 +1,82 @@ +/* + 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/cli/cli/command" + "github.com/docker/cli/cli/command/container" + "github.com/docker/compose/v2/pkg/api" + "github.com/spf13/cobra" +) + +func attachCommand(p *ProjectOptions, dockerCli command.Cli, backend api.Service) *cobra.Command { + // uses NewAttachCommand from `docker container attach`: + // - lookup container by service name (and optionally index) + // - invoke underlying `container attach` with container ID + + // FYI changes to the underlying NewAttachCommand could require changes here, i.e. help messages or options added that don't make sense with `compose attach` + // - That said, the command flags/behavior are pretty stable + + cmd := container.NewAttachCommand(dockerCli) + containerAttachRunE := cmd.RunE + + // Options: + // inherits `container attach` options: --detach-keys, --no-stdin, --sig-proxy, --dry-run + // additional options specific to compose (in this case to lookup a container): + opts := api.LookupContainerOptions{} + cmd.Flags().IntVar(&opts.Index, "index", 1, "index of the container if service has multiple replicas") + + // Help messages: (override to name first arg SERVICE instead of CONTAINER): + cmd.Use = "attach [OPTIONS] SERVICE" + cmd.Short = "Attach local standard input, output, and error streams to a service's running container" + + // clear alias for `docker attach` + cmd.Annotations = map[string]string{} + + // todo is this for completions or validation? or both? + cmd.ValidArgsFunction = completeServiceNames(dockerCli, p) + + // Args: why require a service name? + // - It's possible to make `compose attach` work without specifying a service (i.e. just use first container). + // - But, I don't see that convention used in other similar compose commands, i.e. `compose exec` which targets a single container too and requires the service name. + // - Also, the underlying `container attach` command requires 1 arg so it would have to be patched to ignore no args + // - And, other compose commands optionally take a service name, i.e. `compose logs`, but the behavior there is to view logs of all containers not just one. + + cmd.RunE = Adapt(func(ctx context.Context, args []string) error { + projectName, err := p.toProjectName(dockerCli) + if err != nil { + return err + } + + if len(args) > 0 { + opts.Service = args[0] + } + + containerID, err := backend.LookupContainer(ctx, projectName, opts) + if err != nil { + return err + } + + // TODO? --dry-run? would it be helpful to print the matching container name/ID and not attach? + + return containerAttachRunE(cmd, []string{containerID}) + }) + + return cmd +} diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index a5acef5ffe1..fd68bae80f4 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -460,6 +460,7 @@ func RootCommand(dockerCli command.Cli, backend api.Service) *cobra.Command { // scaleCommand(&opts, dockerCli, backend), watchCommand(&opts, dockerCli, backend), alphaCommand(&opts, dockerCli, backend), + attachCommand(&opts, dockerCli, backend), ) c.Flags().SetInterspersed(false) diff --git a/pkg/api/api.go b/pkg/api/api.go index 42578638a57..e2757740497 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -74,6 +74,8 @@ type Service interface { Events(ctx context.Context, projectName string, options EventsOptions) error // Port executes the equivalent to a `compose port` Port(ctx context.Context, projectName string, service string, port uint16, options PortOptions) (string, int, error) + // Lookup container by service and index + LookupContainer(ctx context.Context, projectName string, options LookupContainerOptions) (string, error) // Publish executes the equivalent to a `compose publish` Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error // Images executes the equivalent of a `compose images` @@ -361,6 +363,11 @@ type PortOptions struct { Index int } +type LookupContainerOptions struct { + Service string + Index int +} + // PublishOptions group options of the Publish API type PublishOptions struct { ResolveImageDigests bool diff --git a/pkg/api/proxy.go b/pkg/api/proxy.go index b08059a3229..60a50f5d616 100644 --- a/pkg/api/proxy.go +++ b/pkg/api/proxy.go @@ -49,6 +49,7 @@ type ServiceProxy struct { TopFn func(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error) EventsFn func(ctx context.Context, project string, options EventsOptions) error PortFn func(ctx context.Context, project string, service string, port uint16, options PortOptions) (string, int, error) + LookupContainerFn func(ctx context.Context, project string, options LookupContainerOptions) (string, error) ImagesFn func(ctx context.Context, projectName string, options ImagesOptions) ([]ImageSummary, error) WatchFn func(ctx context.Context, project *types.Project, services []string, options WatchOptions) error MaxConcurrencyFn func(parallel int) @@ -93,6 +94,7 @@ func (s *ServiceProxy) WithService(service Service) *ServiceProxy { s.TopFn = service.Top s.EventsFn = service.Events s.PortFn = service.Port + s.LookupContainerFn = service.LookupContainer s.PublishFn = service.Publish s.ImagesFn = service.Images s.WatchFn = service.Watch @@ -315,6 +317,13 @@ func (s *ServiceProxy) Port(ctx context.Context, projectName string, service str return s.PortFn(ctx, projectName, service, port, options) } +func (s *ServiceProxy) LookupContainer(ctx context.Context, projectName string, options LookupContainerOptions) (string, error) { + if s.LookupContainerFn == nil { + return "", ErrNotImplemented + } + return s.LookupContainerFn(ctx, projectName, options) +} + func (s *ServiceProxy) Publish(ctx context.Context, project *types.Project, repository string, options PublishOptions) error { return s.PublishFn(ctx, project, repository, options) } diff --git a/pkg/compose/lookup_container.go b/pkg/compose/lookup_container.go new file mode 100644 index 00000000000..a1d7c036ab4 --- /dev/null +++ b/pkg/compose/lookup_container.go @@ -0,0 +1,33 @@ +/* + 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" + "strings" + + "github.com/docker/compose/v2/pkg/api" +) + +func (s *composeService) LookupContainer(ctx context.Context, projectName string, options api.LookupContainerOptions) (string, error) { + projectName = strings.ToLower(projectName) + container, err := s.getSpecifiedContainer(ctx, projectName, oneOffExclude, false, options.Service, options.Index) + if err != nil { + return "", err + } + return container.ID, nil +}