From 9271dcd93f8a6213b0a18311f3c8af361fd9de12 Mon Sep 17 00:00:00 2001 From: Jin Dong Date: Thu, 8 Dec 2022 23:57:24 +0000 Subject: [PATCH] Add compose port Signed-off-by: Jin Dong --- README.md | 14 +++- cmd/nerdctl/compose.go | 1 + cmd/nerdctl/compose_port.go | 88 ++++++++++++++++++++++++++ cmd/nerdctl/compose_port_linux_test.go | 85 +++++++++++++++++++++++++ cmd/nerdctl/port.go | 37 +---------- pkg/composer/port.go | 53 ++++++++++++++++ pkg/containerutil/containerutil.go | 55 ++++++++++++++++ 7 files changed, 297 insertions(+), 36 deletions(-) create mode 100644 cmd/nerdctl/compose_port.go create mode 100644 cmd/nerdctl/compose_port_linux_test.go create mode 100644 pkg/composer/port.go create mode 100644 pkg/containerutil/containerutil.go diff --git a/README.md b/README.md index 653875769e5..bd24389d795 100644 --- a/README.md +++ b/README.md @@ -335,6 +335,7 @@ It does not necessarily mean that the corresponding features are missing in cont - [:whale: nerdctl compose down](#whale-nerdctl-compose-down) - [:whale: nerdctl compose images](#whale-nerdctl-compose-images) - [:whale: nerdctl compose stop](#whale-nerdctl-compose-stop) + - [:whale: nerdctl compose port](#whale-nerdctl-compose-port) - [:whale: nerdctl compose ps](#whale-nerdctl-compose-ps) - [:whale: nerdctl compose pull](#whale-nerdctl-compose-pull) - [:whale: nerdctl compose push](#whale-nerdctl-compose-push) @@ -1492,6 +1493,17 @@ Flags: - :whale: `-t, --timeout`: Seconds to wait for stop before killing it (default 10) +### :whale: nerdctl compose port + +Print the public port for a port binding of a service container + +Usage: `nerdctl compose port [OPTIONS] SERVICE PRIVATE_PORT` + +Flags: + +- :whale: `--index`: Index of the container if the service has multiple instances. (default 1) +- :whale: `--protocol`: Protocol of the port (tcp|udp) (default "tcp") + ### :whale: nerdctl compose ps List containers of services @@ -1668,7 +1680,7 @@ Registry: - `docker search` Compose: -- `docker-compose create|events|port|scale|start` +- `docker-compose create|events|scale|start` Others: - `docker system df` diff --git a/cmd/nerdctl/compose.go b/cmd/nerdctl/compose.go index d5540b2e0b2..bf78a49dc63 100644 --- a/cmd/nerdctl/compose.go +++ b/cmd/nerdctl/compose.go @@ -60,6 +60,7 @@ func newComposeCommand() *cobra.Command { newComposeBuildCommand(), newComposeExecCommand(), newComposeImagesCommand(), + newComposePortCommand(), newComposePushCommand(), newComposePullCommand(), newComposeDownCommand(), diff --git a/cmd/nerdctl/compose_port.go b/cmd/nerdctl/compose_port.go new file mode 100644 index 00000000000..e651eeaf6f5 --- /dev/null +++ b/cmd/nerdctl/compose_port.go @@ -0,0 +1,88 @@ +/* + Copyright The containerd 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 main + +import ( + "fmt" + "strconv" + + "github.com/containerd/nerdctl/pkg/composer" + "github.com/spf13/cobra" +) + +func newComposePortCommand() *cobra.Command { + var composePortCommand = &cobra.Command{ + Use: "port [flags] SERVICE PRIVATE_PORT", + Short: "Print the public port for a port binding", + Args: cobra.ExactArgs(2), + RunE: composePortAction, + SilenceUsage: true, + SilenceErrors: true, + } + composePortCommand.Flags().Int("index", 1, "index of the container if the service has multiple instances.") + composePortCommand.Flags().String("protocol", "tcp", "protocol of the port (tcp|udp)") + + return composePortCommand +} + +func composePortAction(cmd *cobra.Command, args []string) error { + index, err := cmd.Flags().GetInt("index") + if err != nil { + return err + } + if index < 1 { + return fmt.Errorf("index starts from 1 and should be equal or greater than 1, given index: %d", index) + } + + protocol, err := cmd.Flags().GetString("protocol") + if err != nil { + return err + } + switch protocol { + case "tcp", "udp": + default: + return fmt.Errorf("unsupported protocol: %s (only tcp and udp are supported)", protocol) + } + + port, err := strconv.Atoi(args[1]) + if err != nil { + return err + } + if port <= 0 { + return fmt.Errorf("unexpected port: %d", port) + } + + client, ctx, cancel, err := newClient(cmd) + if err != nil { + return err + } + defer cancel() + + c, err := getComposer(cmd, client) + if err != nil { + return err + } + + po := composer.PortOptions{ + ServiceName: args[0], + Index: index, + Port: port, + Protocol: protocol, + } + + return c.Port(ctx, cmd.OutOrStdout(), po) +} diff --git a/cmd/nerdctl/compose_port_linux_test.go b/cmd/nerdctl/compose_port_linux_test.go new file mode 100644 index 00000000000..f5240edc55b --- /dev/null +++ b/cmd/nerdctl/compose_port_linux_test.go @@ -0,0 +1,85 @@ +/* + Copyright The containerd 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 main + +import ( + "fmt" + "testing" + + "github.com/containerd/nerdctl/pkg/testutil" +) + +func TestComposePort(t *testing.T) { + base := testutil.NewBase(t) + + var dockerComposeYAML = fmt.Sprintf(` +version: '3.1' + +services: + svc0: + image: %s + command: "sleep infinity" + ports: + - "12345:10000" + - "12346:10001/udp" +`, testutil.CommonImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + projectName := comp.ProjectName() + t.Logf("projectName=%q", projectName) + + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() + defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + + // `port` should work for given port and protocol + base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "svc0", "10000").AssertOutExactly("0.0.0.0:12345\n") + base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "udp", "svc0", "10001").AssertOutExactly("0.0.0.0:12346\n") +} + +func TestComposePortFailure(t *testing.T) { + // when no port mapping is found, docker compose v1 prints `\n` while v2 prints `:0\n` + // both v1 and v2 have exit code 0 (succeess) + // nerdctl compose will fail with error (no public port found). + testutil.DockerIncompatible(t) + base := testutil.NewBase(t) + + var dockerComposeYAML = fmt.Sprintf(` +version: '3.1' + +services: + svc0: + image: %s + command: "sleep infinity" + ports: + - "12345:10000" + - "12346:10001/udp" +`, testutil.CommonImage) + + comp := testutil.NewComposeDir(t, dockerComposeYAML) + defer comp.CleanUp() + projectName := comp.ProjectName() + t.Logf("projectName=%q", projectName) + + base.ComposeCmd("-f", comp.YAMLFullPath(), "up", "-d").AssertOK() + defer base.ComposeCmd("-f", comp.YAMLFullPath(), "down", "-v").AssertOK() + + // `port` should fail if given port and protocol don't exist + base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "svc0", "9999").AssertFail() + base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "udp", "svc0", "10000").AssertFail() + base.ComposeCmd("-f", comp.YAMLFullPath(), "port", "--protocol", "tcp", "svc0", "10001").AssertFail() +} diff --git a/cmd/nerdctl/port.go b/cmd/nerdctl/port.go index 74b7d56acc0..ef39fa14f94 100644 --- a/cmd/nerdctl/port.go +++ b/cmd/nerdctl/port.go @@ -18,15 +18,12 @@ package main import ( "context" - "encoding/json" "fmt" "strconv" "strings" - "github.com/containerd/containerd" - gocni "github.com/containerd/go-cni" + "github.com/containerd/nerdctl/pkg/containerutil" "github.com/containerd/nerdctl/pkg/idutil/containerwalker" - "github.com/containerd/nerdctl/pkg/labels" "github.com/spf13/cobra" ) @@ -85,7 +82,7 @@ func portAction(cmd *cobra.Command, args []string) error { if found.MatchCount > 1 { return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req) } - return printPort(ctx, cmd, found.Container, argPort, argProto) + return containerutil.PrintHostPort(ctx, cmd.OutOrStdout(), found.Container, argPort, argProto) }, } req := args[0] @@ -98,36 +95,6 @@ func portAction(cmd *cobra.Command, args []string) error { return nil } -func printPort(ctx context.Context, cmd *cobra.Command, container containerd.Container, argPort int, argProto string) error { - l, err := container.Labels(ctx) - if err != nil { - return err - } - portsJSON := l[labels.Ports] - if portsJSON == "" { - return nil - } - var ports []gocni.PortMapping - if err := json.Unmarshal([]byte(portsJSON), &ports); err != nil { - return err - } - - if argPort < 0 { - for _, p := range ports { - fmt.Fprintf(cmd.OutOrStdout(), "%d/%s -> %s:%d\n", p.ContainerPort, p.Protocol, p.HostIP, p.HostPort) - } - return nil - } - - for _, p := range ports { - if p.ContainerPort == int32(argPort) && strings.ToLower(p.Protocol) == argProto { - fmt.Fprintf(cmd.OutOrStdout(), "%s:%d\n", p.HostIP, p.HostPort) - return nil - } - } - return fmt.Errorf("no public port %d/%s published for %q", argPort, argProto, container.ID()) -} - func portShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return shellCompleteContainerNames(cmd, nil) } diff --git a/pkg/composer/port.go b/pkg/composer/port.go new file mode 100644 index 00000000000..12676eb981f --- /dev/null +++ b/pkg/composer/port.go @@ -0,0 +1,53 @@ +/* + Copyright The containerd 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 composer + +import ( + "context" + "fmt" + "io" + + "github.com/containerd/nerdctl/pkg/containerutil" +) + +// PortOptions has args for getting the public port of a given private port/protocol +// in a service container. +type PortOptions struct { + ServiceName string + Index int + Port int + Protocol string +} + +// Port gets the corresponding public port of a given private port/protocol +// on a service container. +func (c *Composer) Port(ctx context.Context, writer io.Writer, po PortOptions) error { + containers, err := c.Containers(ctx, po.ServiceName) + if err != nil { + return fmt.Errorf("fail to get containers for service %s: %s", po.ServiceName, err) + } + if len(containers) == 0 { + return fmt.Errorf("no running containers from service %s", po.ServiceName) + } + if po.Index > len(containers) { + return fmt.Errorf("index (%d) out of range: only %d running instances from service %s", + po.Index, len(containers), po.ServiceName) + } + container := containers[po.Index-1] + + return containerutil.PrintHostPort(ctx, writer, container, po.Port, po.Protocol) +} diff --git a/pkg/containerutil/containerutil.go b/pkg/containerutil/containerutil.go new file mode 100644 index 00000000000..257ba90ca41 --- /dev/null +++ b/pkg/containerutil/containerutil.go @@ -0,0 +1,55 @@ +/* + Copyright The containerd 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 containerutil + +import ( + "context" + "fmt" + "io" + "strings" + + "github.com/containerd/containerd" + "github.com/containerd/nerdctl/pkg/portutil" +) + +// PrintHostPort writes to `writer` the public (HostIP:HostPort) of a given `containerPort/protocol` in a container. +// if `containerPort < 0`, it writes all public ports of the container. +func PrintHostPort(ctx context.Context, writer io.Writer, container containerd.Container, containerPort int, proto string) error { + l, err := container.Labels(ctx) + if err != nil { + return err + } + ports, err := portutil.ParsePortsLabel(l) + if err != nil { + return err + } + + if containerPort < 0 { + for _, p := range ports { + fmt.Fprintf(writer, "%d/%s -> %s:%d\n", p.ContainerPort, p.Protocol, p.HostIP, p.HostPort) + } + return nil + } + + for _, p := range ports { + if p.ContainerPort == int32(containerPort) && strings.ToLower(p.Protocol) == proto { + fmt.Fprintf(writer, "%s:%d\n", p.HostIP, p.HostPort) + return nil + } + } + return fmt.Errorf("no public port %d/%s published for %q", containerPort, proto, container.ID()) +}