diff --git a/aci/compose.go b/aci/compose.go index 653436701..a3d4de65d 100644 --- a/aci/compose.go +++ b/aci/compose.go @@ -233,3 +233,7 @@ func (cs *aciComposeService) Top(ctx context.Context, projectName string, servic func (cs *aciComposeService) Events(ctx context.Context, project string, options compose.EventsOptions) error { return errdefs.ErrNotImplemented } + +func (cs *aciComposeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} diff --git a/api/client/compose.go b/api/client/compose.go index c5b1dca36..37107f654 100644 --- a/api/client/compose.go +++ b/api/client/compose.go @@ -107,3 +107,7 @@ func (c *composeService) Top(ctx context.Context, projectName string, services [ func (c *composeService) Events(ctx context.Context, project string, options compose.EventsOptions) error { return errdefs.ErrNotImplemented } + +func (c *composeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} diff --git a/api/compose/api.go b/api/compose/api.go index c6269328f..1952e979a 100644 --- a/api/compose/api.go +++ b/api/compose/api.go @@ -68,6 +68,8 @@ type Service interface { Top(ctx context.Context, projectName string, services []string) ([]ContainerProcSummary, error) // Events executes the equivalent to a `compose events` Events(ctx context.Context, project string, options EventsOptions) error + // Port executes the equivalent to a `compose port` + Port(ctx context.Context, project string, service string, port int, options PortOptions) (string, int, error) } // BuildOptions group options of the Build API @@ -199,6 +201,12 @@ type Event struct { Attributes map[string]string } +// PortOptions group options of the Port API +type PortOptions struct { + Protocol string + Index int +} + func (e Event) String() string { t := e.Timestamp.Format("2006-01-02 15:04:05.000000") var attr []string diff --git a/cli/cmd/compose/compose.go b/cli/cmd/compose/compose.go index c6b180397..29d8ec46d 100644 --- a/cli/cmd/compose/compose.go +++ b/cli/cmd/compose/compose.go @@ -140,6 +140,7 @@ func Command(contextType string) *cobra.Command { unpauseCommand(&opts), topCommand(&opts), eventsCommand(&opts), + portCommand(&opts), ) if contextType == store.LocalContextType || contextType == store.DefaultContextType { diff --git a/cli/cmd/compose/port.go b/cli/cmd/compose/port.go new file mode 100644 index 000000000..c556893ca --- /dev/null +++ b/cli/cmd/compose/port.go @@ -0,0 +1,77 @@ +/* + 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" + "strconv" + + "github.com/spf13/cobra" + + "github.com/docker/compose-cli/api/client" + "github.com/docker/compose-cli/api/compose" +) + +type portOptions struct { + *projectOptions + protocol string + index int +} + +func portCommand(p *projectOptions) *cobra.Command { + opts := portOptions{ + projectOptions: p, + } + cmd := &cobra.Command{ + Use: "port [options] [--] SERVICE PRIVATE_PORT", + Short: "Print the public port for a port binding.", + Args: cobra.MinimumNArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + port, err := strconv.Atoi(args[1]) + if err != nil { + return err + } + return runPort(cmd.Context(), opts, args[0], port) + }, + } + cmd.Flags().StringVar(&opts.protocol, "protocol", "tcp", "tcp or udp") + cmd.Flags().IntVar(&opts.index, "index", 1, "index of the container if service has multiple replicas") + return cmd +} + +func runPort(ctx context.Context, opts portOptions, service string, port int) error { + c, err := client.New(ctx) + if err != nil { + return err + } + + projectName, err := opts.toProjectName() + if err != nil { + return err + } + ip, port, err := c.ComposeService().Port(ctx, projectName, service, port, compose.PortOptions{ + Protocol: opts.protocol, + Index: opts.index, + }) + if err != nil { + return err + } + + fmt.Printf("%s:%d\n", ip, port) + return nil +} diff --git a/cli/cmd/compose/ps.go b/cli/cmd/compose/ps.go index be0f0a104..5e91b5be3 100644 --- a/cli/cmd/compose/ps.go +++ b/cli/cmd/compose/ps.go @@ -29,6 +29,7 @@ import ( "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/compose" "github.com/docker/compose-cli/cli/formatter" + "github.com/docker/compose-cli/utils" ) type psOptions struct { @@ -77,7 +78,7 @@ func runPs(ctx context.Context, opts psOptions) error { if opts.Services { services := []string{} for _, s := range containers { - if !contains(services, s.Service) { + if !utils.StringContains(services, s.Service) { services = append(services, s.Service) } } @@ -115,12 +116,3 @@ func runPs(ctx context.Context, opts psOptions) error { }, "NAME", "SERVICE", "STATUS", "PORTS") } - -func contains(slice []string, item string) bool { - for _, v := range slice { - if v == item { - return true - } - } - return false -} diff --git a/cli/cmd/compose/pull.go b/cli/cmd/compose/pull.go index 44ccbae37..e735d355c 100644 --- a/cli/cmd/compose/pull.go +++ b/cli/cmd/compose/pull.go @@ -23,6 +23,7 @@ import ( "github.com/docker/compose-cli/api/client" "github.com/docker/compose-cli/api/progress" + "github.com/docker/compose-cli/utils" ) type pullOptions struct { @@ -65,7 +66,7 @@ func runPull(ctx context.Context, opts pullOptions, services []string) error { return err } for _, s := range project.Services { - if !contains(services, s.Name) { + if !utils.StringContains(services, s.Name) { project.DisabledServices = append(project.DisabledServices, s) } } diff --git a/cli/cmd/compose/up.go b/cli/cmd/compose/up.go index 280bb4ddb..5c11d2a94 100644 --- a/cli/cmd/compose/up.go +++ b/cli/cmd/compose/up.go @@ -28,6 +28,7 @@ import ( "time" "github.com/compose-spec/compose-go/types" + "github.com/docker/compose-cli/utils" "github.com/sirupsen/logrus" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" @@ -106,7 +107,7 @@ func (opts upOptions) apply(project *types.Project, services []string) error { return err } for _, s := range project.Services { - if !contains(services, s.Name) { + if !utils.StringContains(services, s.Name) { project.DisabledServices = append(project.DisabledServices, s) } } diff --git a/ecs/local/compose.go b/ecs/local/compose.go index 69958af98..0d6ac4ae5 100644 --- a/ecs/local/compose.go +++ b/ecs/local/compose.go @@ -199,3 +199,7 @@ func (e ecsLocalSimulation) Top(ctx context.Context, projectName string, service func (e ecsLocalSimulation) Events(ctx context.Context, project string, options compose.EventsOptions) error { return e.compose.Events(ctx, project, options) } + +func (e ecsLocalSimulation) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} diff --git a/ecs/up.go b/ecs/up.go index aa937f512..cb93a6120 100644 --- a/ecs/up.go +++ b/ecs/up.go @@ -67,6 +67,10 @@ func (b *ecsAPIService) Events(ctx context.Context, project string, options comp return errdefs.ErrNotImplemented } +func (b *ecsAPIService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} + func (b *ecsAPIService) Up(ctx context.Context, project *types.Project, options compose.UpOptions) error { logrus.Debugf("deploying on AWS with region=%q", b.Region) err := b.aws.CheckRequirements(ctx, b.Region) diff --git a/kube/compose.go b/kube/compose.go index adf4992f4..52ba0ae82 100644 --- a/kube/compose.go +++ b/kube/compose.go @@ -262,3 +262,7 @@ func (s *composeService) Top(ctx context.Context, projectName string, services [ func (s *composeService) Events(ctx context.Context, project string, options compose.EventsOptions) error { return errdefs.ErrNotImplemented } + +func (s *composeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + return "", 0, errdefs.ErrNotImplemented +} diff --git a/local/compose/port.go b/local/compose/port.go new file mode 100644 index 000000000..c10235156 --- /dev/null +++ b/local/compose/port.go @@ -0,0 +1,50 @@ +/* + 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" + + "github.com/docker/compose-cli/api/compose" + + moby "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/filters" +) + +func (s *composeService) Port(ctx context.Context, project string, service string, port int, options compose.PortOptions) (string, int, error) { + list, err := s.apiClient.ContainerList(ctx, moby.ContainerListOptions{ + Filters: filters.NewArgs( + projectFilter(project), + serviceFilter(service), + filters.Arg("label", fmt.Sprintf("%s=%d", containerNumberLabel, options.Index)), + ), + }) + if err != nil { + return "", 0, err + } + if len(list) == 0 { + return "", 0, fmt.Errorf("no container found for %s_%d", service, options.Index) + } + container := list[0] + for _, p := range container.Ports { + if p.PrivatePort == uint16(port) && p.Type == options.Protocol { + return p.IP, int(p.PublicPort), nil + } + } + return "", 0, err +} diff --git a/local/e2e/compose/networks_test.go b/local/e2e/compose/networks_test.go index f90d5796f..b1d9301e7 100644 --- a/local/e2e/compose/networks_test.go +++ b/local/e2e/compose/networks_test.go @@ -55,6 +55,11 @@ func TestNetworks(t *testing.T) { res.Assert(t, icmd.Expected{Out: "microservices"}) }) + t.Run("port", func(t *testing.T) { + res := c.RunDockerCmd("compose", "--project-name", projectName, "port", "words", "8080") + res.Assert(t, icmd.Expected{Out: `0.0.0.0:8080`}) + }) + t.Run("down", func(t *testing.T) { _ = c.RunDockerCmd("compose", "--project-name", projectName, "down") })