From f92957314c1b84577a53b9d44d99647264f0cc50 Mon Sep 17 00:00:00 2001 From: Zach Koopmans Date: Thu, 30 Mar 2023 14:13:49 -0700 Subject: [PATCH] Add portforward command to runsc Add portforward comand so that we can use runsc to forward connections to container ports. This will eventually be supported in k8s. PiperOrigin-RevId: 520739913 --- .buildkite/pipeline.yaml | 2 +- Makefile | 9 +- images/basic/redis/Dockerfile | 1 + pkg/test/dockerutil/container.go | 30 +- pkg/test/dockerutil/profile.go | 9 +- runsc/boot/controller.go | 3 + .../boot/portforward/portforward_hostinet.go | 2 +- runsc/cli/main.go | 1 + runsc/cmd/BUILD | 2 + runsc/cmd/portforward.go | 321 ++++++++++++++++++ runsc/container/container.go | 9 + runsc/sandbox/sandbox.go | 18 +- test/root/BUILD | 20 ++ test/root/portforward_test.go | 160 +++++++++ 14 files changed, 574 insertions(+), 13 deletions(-) create mode 100644 images/basic/redis/Dockerfile create mode 100644 runsc/cmd/portforward.go create mode 100644 test/root/portforward_test.go diff --git a/.buildkite/pipeline.yaml b/.buildkite/pipeline.yaml index cc4945910..ff46dac64 100644 --- a/.buildkite/pipeline.yaml +++ b/.buildkite/pipeline.yaml @@ -271,7 +271,7 @@ steps: # See above: not truly a source test. <<: *source_test label: ":docker: Docker tests (cgroupv2)" - command: make docker-tests + command: make portforward-tests docker-tests agents: <<: *ubuntu_agents arch: "amd64" diff --git a/Makefile b/Makefile index a58e935bc..21e9b844e 100644 --- a/Makefile +++ b/Makefile @@ -262,6 +262,13 @@ arm-qemu-smoke-test: $(RUNTIME_BIN) load-arm-qemu simple-tests: unit-tests # Compatibility target. .PHONY: simple-tests +portforward-tests: load-basic_redis $(RUNTIME_BIN) + @$(call install_runtime,$(RUNTIME),--network=sandbox) + @$(call sudo,test/root:portforward_test,--runtime=$(RUNTIME) -test.v) + @$(call install_runtime,$(RUNTIME),--network=host) + @$(call sudo,test/root:portforward_test,--runtime=$(RUNTIME) -test.v) +.PHONY: portforward-test + # Standard integration targets. INTEGRATION_TARGETS := //test/image:image_test //test/e2e:integration_test @@ -324,6 +331,7 @@ fsstress-test: load-basic $(RUNTIME_BIN) @$(call test_runtime,$(RUNTIME),//test/fsstress:fsstress_test) .PHONY: fsstress-test + # Specific containerd version tests. containerd-test-%: load-basic_alpine load-basic_python load-basic_busybox load-basic_symlink-resolv load-basic_httpd load-basic_ubuntu $(RUNTIME_BIN) @$(call install_runtime,$(RUNTIME),) # Clear flags. @@ -335,7 +343,6 @@ else sudo tar -C "$$(dirname $$(which containerd))" -zxvf - containerd-shim-runsc-v1 endif @$(call sudo,test/root:root_test,--runtime=$(RUNTIME) -test.v) - containerd-tests-min: containerd-test-1.4.12 ## diff --git a/images/basic/redis/Dockerfile b/images/basic/redis/Dockerfile new file mode 100644 index 000000000..0f17249af --- /dev/null +++ b/images/basic/redis/Dockerfile @@ -0,0 +1 @@ +FROM redis:5.0.4 diff --git a/pkg/test/dockerutil/container.go b/pkg/test/dockerutil/container.go index 07feecee9..ae5b03941 100644 --- a/pkg/test/dockerutil/container.go +++ b/pkg/test/dockerutil/container.go @@ -45,9 +45,8 @@ import ( // client. Container is backed by the offical golang docker API. // See: https://pkg.go.dev/github.com/docker/docker. type Container struct { - Name string - runtime string - + Name string + runtime string logger testutil.Logger client *client.Client id string @@ -90,6 +89,11 @@ type RunOpts struct { // Privileged enables privileged mode. Privileged bool + // Sets network mode for the container. See container.NetworkMode for types. Several options will + // not work w/ gVisor. For example, you can't set the "sandbox" network option for gVisor using + // this handle. + NetworkMode string + // CapAdd are the extra set of capabilities to add. CapAdd []string @@ -270,6 +274,7 @@ func (c *Container) hostConfig(r RunOpts) *container.HostConfig { CapDrop: r.CapDrop, Privileged: r.Privileged, ReadonlyRootfs: r.ReadOnly, + NetworkMode: container.NetworkMode(r.NetworkMode), Resources: container.Resources{ Memory: int64(r.Memory), // In bytes. CpusetCpus: r.CpusetCpus, @@ -341,6 +346,25 @@ func (c *Container) ID() string { return c.id } +// RootDirectory returns an educated guess about the container's root directory. +func (c *Container) RootDirectory() (string, error) { + // The root directory of this container's runtime. + rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime) + _, err := os.Stat(rootDir) + if err == nil { + return rootDir, nil + } + // In docker v20+, due to https://github.com/moby/moby/issues/42345 the + // rootDir seems to always be the following. + const defaultDir = "/var/run/docker/runtime-runc/moby" + _, derr := os.Stat(defaultDir) + if derr == nil { + return defaultDir, nil + } + + return "", fmt.Errorf("cannot stat %q: %v or %q: %v", rootDir, err, defaultDir, derr) +} + // SandboxPid returns the container's pid. func (c *Container) SandboxPid(ctx context.Context) (int, error) { resp, err := c.client.ContainerInspect(ctx, c.id) diff --git a/pkg/test/dockerutil/profile.go b/pkg/test/dockerutil/profile.go index 12fe98b16..47a051d85 100644 --- a/pkg/test/dockerutil/profile.go +++ b/pkg/test/dockerutil/profile.go @@ -81,12 +81,9 @@ func (p *profile) createProcess(c *Container) error { return fmt.Errorf("failed to get runtime path: %v", err) } - // The root directory of this container's runtime. - rootDir := fmt.Sprintf("/var/run/docker/runtime-%s/moby", c.runtime) - if _, err := os.Stat(rootDir); os.IsNotExist(err) { - // In docker v20+, due to https://github.com/moby/moby/issues/42345 the - // rootDir seems to always be the following. - rootDir = "/var/run/docker/runtime-runc/moby" + rootDir, err := c.RootDirectory() + if err != nil { + return fmt.Errorf("failed to get root directory: %v", err) } // Format is `runsc --root=rootDir debug --profile-*=file --duration=24h containerID`. diff --git a/runsc/boot/controller.go b/runsc/boot/controller.go index 30f948c24..e3a22b952 100644 --- a/runsc/boot/controller.go +++ b/runsc/boot/controller.go @@ -58,6 +58,9 @@ const ( // ContMgrExecuteAsync executes a command in a container. ContMgrExecuteAsync = "containerManager.ExecuteAsync" + // ContMgrPortForward starts port forwarding with the sandbox. + ContMgrPortForward = "containerManager.PortForward" + // ContMgrProcesses lists processes running in a container. ContMgrProcesses = "containerManager.Processes" diff --git a/runsc/boot/portforward/portforward_hostinet.go b/runsc/boot/portforward/portforward_hostinet.go index eea9a293e..40ec37e72 100644 --- a/runsc/boot/portforward/portforward_hostinet.go +++ b/runsc/boot/portforward/portforward_hostinet.go @@ -48,7 +48,7 @@ type hostInetConn struct { // NewHostInetConn creates a hostInetConn backed by a host socket on the localhost address. func NewHostInetConn(port uint16) (proxyConn, error) { // NOTE: Options must match sandbox seccomp filters. See filter/config.go - fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, 0) + fd, err := unix.Socket(unix.AF_INET, unix.SOCK_STREAM|unix.SOCK_NONBLOCK|unix.SOCK_CLOEXEC, unix.IPPROTO_TCP) if err != nil { return nil, err } diff --git a/runsc/cli/main.go b/runsc/cli/main.go index f554e849d..2f764322d 100644 --- a/runsc/cli/main.go +++ b/runsc/cli/main.go @@ -76,6 +76,7 @@ func Main() { subcommands.Register(new(cmd.List), "") subcommands.Register(new(cmd.PS), "") subcommands.Register(new(cmd.Pause), "") + subcommands.Register(new(cmd.PortForward), "") subcommands.Register(new(cmd.Restore), "") subcommands.Register(new(cmd.Resume), "") subcommands.Register(new(cmd.Run), "") diff --git a/runsc/cmd/BUILD b/runsc/cmd/BUILD index 98ece4191..88581f677 100644 --- a/runsc/cmd/BUILD +++ b/runsc/cmd/BUILD @@ -33,6 +33,7 @@ go_library( "path.go", "pause.go", "platforms.go", + "portforward.go", "ps.go", "read_control.go", "restore.go", @@ -70,6 +71,7 @@ go_library( "//pkg/state/statefile", "//pkg/sync", "//pkg/unet", + "//pkg/urpc", "//runsc/boot", "//runsc/cmd/util", "//runsc/config", diff --git a/runsc/cmd/portforward.go b/runsc/cmd/portforward.go new file mode 100644 index 000000000..c2f372177 --- /dev/null +++ b/runsc/cmd/portforward.go @@ -0,0 +1,321 @@ +// Copyright 2023 The gVisor 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 cmd + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "math" + "net" + "os" + "os/signal" + "strconv" + "strings" + "sync" + "syscall" + + "github.com/google/subcommands" + "gvisor.dev/gvisor/pkg/log" + "gvisor.dev/gvisor/pkg/urpc" + "gvisor.dev/gvisor/runsc/boot" + "gvisor.dev/gvisor/runsc/cmd/util" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/container" + "gvisor.dev/gvisor/runsc/flag" +) + +// PortForward implements subcommands.Command for the "portforward" command. +type PortForward struct { + portNum int + stream string +} + +// Name implements subcommands.Command.Name. +func (*PortForward) Name() string { + return "port-forward" +} + +// Synopsis implements subcommands.Command.Synopsis. +func (*PortForward) Synopsis() string { + return "port forward to a secure container" +} + +// Usage implements subcommands.Command.Usage. +func (*PortForward) Usage() string { + return `port-forward CONTAINER_ID [LOCAL_PORT:]REMOTE_PORT - port forward to gvisor container. + +Open a local port and forward connections to another port inside the specified +container. + +EXAMPLES: + +The following will forward connections on local port 8080 to port 80 in the +container named 'nginx': + + # runsc port-forward nginx 8080:80 +` +} + +// SetFlags implements subcommands.Command.SetFlags. +func (p *PortForward) SetFlags(f *flag.FlagSet) {} + +// Execute implements subcommands.Command.Execute. +func (p *PortForward) Execute(ctx context.Context, f *flag.FlagSet, args ...any) subcommands.ExitStatus { + conf := args[0].(*config.Config) + // Requires at least the container id and port. + if f.NArg() != 2 { + f.Usage() + return subcommands.ExitUsageError + } + + id := f.Arg(0) + portStr := f.Arg(1) + + c, err := container.Load(conf.RootDir, container.FullID{ContainerID: id}, container.LoadOpts{}) + if err != nil { + util.Fatalf("loading container: %v", err) + } + + // Allow forwarding to a local port. + ports := strings.Split(portStr, ":") + if len(ports) != 2 { + util.Fatalf("invalid port string %q", portStr) + } + + localPort, err := strconv.Atoi(ports[0]) + if err != nil { + util.Fatalf("invalid port string %q: %v", portStr, err) + } + portNum, err := strconv.Atoi(ports[1]) + if err != nil { + util.Fatalf("invalid port string %q: %v", portStr, err) + } + if portNum <= 0 || portNum > math.MaxUint16 { + util.Fatalf("invalid port %d: %v", portNum, err) + } + + // Start port forwarding with the local port. + var wg sync.WaitGroup + ctx, cancel := context.WithCancel(ctx) + wg.Add(3) + go func(localPort, portNum int) { + defer cancel() + defer wg.Done() + // Print message to local user. + fmt.Printf("Forwarding local port %d to %d...\n", localPort, portNum) + if err := localForward(ctx, c, localPort, uint16(portNum)); err != nil { + log.Warningf("port forwarding: %v", err) + } + }(localPort, portNum) + + // Exit port forwarding if the container exits. + go func() { + defer wg.Done() + // Cancel port forwarding after Wait returns regardless of return + // value as err may indicate sandbox has terminated already. + _, _ = c.Wait() + fmt.Printf("Container %q stopped. Exiting...\n", c.ID) + cancel() + }() + + // Wait for ^C from the user. + go func() { + defer wg.Done() + sig := waitSignal() + fmt.Printf("Got %v, Exiting...\n", sig) + cancel() + }() + + // Wait on a WaitGroup for port forwarding to clean up before exiting. + wg.Wait() + + return subcommands.ExitSuccess +} + +// localForward starts port forwarding from the given local port. +func localForward(ctx context.Context, c *container.Container, localPort int, containerPort uint16) error { + l, err := net.Listen("tcp", ":"+strconv.Itoa(localPort)) + if err != nil { + return err + } + defer l.Close() + + var localConnChan = make(chan net.Conn, 1) + var errChan = make(chan error, 1) + go func() { + for { + if ctx.Err() != nil { + return + } + localConn, err := l.Accept() + if err != nil { + errChan <- err + continue + } + localConnChan <- localConn + } + }() + + for { + // Exit if the context is done. + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-errChan: + if err != nil { + log.Warningf("accepting local connection: %v", err) + } + case localConn := <-localConnChan: + // Dispatch a new goroutine to handle the new connection. + go func() { + defer localConn.Close() + fmt.Println("Forwarding new connection...") + err := portCopy(ctx, c, localConn, containerPort) + if err != nil { + log.Warningf("port forwarding: %v", err) + } + fmt.Println("Finished forwarding connection...") + }() + } + } +} + +// portCopy creates a UDS and begins copying data to and from the local +// connection. +func portCopy(ctx context.Context, c *container.Container, localConn net.Conn, port uint16) error { + // Create a new path address for the UDS. + addr, err := tmpUDSAddr() + if err != nil { + return err + } + + // Create the UDS and Listen on it. + l, err := net.Listen("unix", addr) + if err != nil { + return err + } + defer l.Close() + + // Open the UDS as a File so it can be donated to the sentry. + streamFile, err := openStream(addr) + if err != nil { + return fmt.Errorf("opening uds stream: %v", err) + } + defer streamFile.Close() + + // Request port forwarding from the sentry. This request will return + // immediately after port forwarding is started and connection state is + // handled via the UDS from then on. + if err := c.PortForward(&boot.PortForwardOpts{ + Port: port, + FilePayload: urpc.FilePayload{Files: []*os.File{streamFile}}, + }); err != nil { + return fmt.Errorf("PortForward: %v", err) + } + + // We have already opened a single connection on the UDS and passed the + // client end to the sentry. We accept the connection now in order to get + // the other half of the connection. + conn, err := l.Accept() + if err != nil { + return err + } + + toErrCh := make(chan error) + fromErrCh := make(chan error) + // Copy data from the local port to the UDS. + go func() { + defer conn.Close() + defer localConn.Close() + log.Debugf("Start copying from %q to %q", localConn.LocalAddr().String(), conn.LocalAddr().String()) + _, err := io.Copy(localConn, conn) + log.Debugf("Stopped copying from %q to %q", localConn.LocalAddr().String(), conn.LocalAddr().String()) + toErrCh <- err + close(toErrCh) + }() + + // Copy data from the UDS to the local port. + go func() { + defer conn.Close() + defer localConn.Close() + log.Debugf("Start copying from %q to %q", conn.LocalAddr().String(), localConn.LocalAddr().String()) + _, err := io.Copy(conn, localConn) + log.Debugf("Stopped copying from %q to %q", conn.LocalAddr().String(), localConn.LocalAddr().String()) + fromErrCh <- err + close(fromErrCh) + }() + + errMap := map[string]error{} + for { + if len(errMap) == 2 { + return nil + } + select { + case e := <-toErrCh: + errMap["toChannel"] = e + case e := <-fromErrCh: + errMap["fromChannel"] = e + case <-ctx.Done(): + log.Debugf("Port forwarding connection canceled for %q: %v", localConn.LocalAddr().String(), ctx.Err()) + return ctx.Err() + } + } +} + +// tmpUDS generates a temporary UDS addr. +func tmpUDSAddr() (string, error) { + tmpFile, err := ioutil.TempFile("", "runsc-port-forward") + if err != nil { + return "", err + } + path := tmpFile.Name() + // Remove the tempfile and just use its name. + os.Remove(path) + + return path, nil +} + +// openStream opens a UDS as a socket and returns the file descriptor in an +// os.File object. +func openStream(name string) (*os.File, error) { + // The net package will abstract the fd, so we use raw syscalls. + fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) + if err != nil { + return nil, err + } + + // We are acting as a client so we will connect to the socket. + if err = syscall.Connect(fd, &syscall.SockaddrUnix{Name: name}); err != nil { + syscall.Close(fd) + return nil, err + } + + // Return a File so that we can pass it to the Sentry. + return os.NewFile(uintptr(fd), name), nil +} + +// waitSignal waits for SIGINT, SIGQUIT, or SIGTERM from the user. +func waitSignal() os.Signal { + ch := make(chan os.Signal, 2) + signal.Notify( + ch, + syscall.SIGINT, + syscall.SIGQUIT, + syscall.SIGTERM, + ) + return <-ch +} diff --git a/runsc/container/container.go b/runsc/container/container.go index a82f2255b..baf1d6748 100644 --- a/runsc/container/container.go +++ b/runsc/container/container.go @@ -574,6 +574,15 @@ func (c *Container) Event() (*boot.EventOut, error) { return event, nil } +// PortForward starts port forwarding to the container. +func (c *Container) PortForward(opts *boot.PortForwardOpts) error { + if err := c.requireStatus("port forward", Running); err != nil { + return err + } + opts.ContainerID = c.ID + return c.Sandbox.PortForward(opts) +} + // SandboxPid returns the Getpid of the sandbox the container is running in, or -1 if the // container is not running. func (c *Container) SandboxPid() int { diff --git a/runsc/sandbox/sandbox.go b/runsc/sandbox/sandbox.go index dbb5689d0..a3d1490f6 100644 --- a/runsc/sandbox/sandbox.go +++ b/runsc/sandbox/sandbox.go @@ -567,6 +567,22 @@ func (s *Sandbox) Event(cid string) (*boot.EventOut, error) { return &e, nil } +// PortForward starts port forwarding to the sandbox. +func (s *Sandbox) PortForward(opts *boot.PortForwardOpts) error { + log.Debugf("Requesting port forward for container %q in sandbox %q: %+v", opts.ContainerID, s.ID, opts) + conn, err := s.sandboxConnect() + if err != nil { + return err + } + defer conn.Close() + + if err := conn.Call(boot.ContMgrPortForward, opts, nil); err != nil { + return fmt.Errorf("port forwarding to sandbox: %v", err) + } + + return nil +} + func (s *Sandbox) sandboxConnect() (*urpc.Client, error) { log.Debugf("Connecting to sandbox %q", s.ID) conn, err := client.ConnectTo(s.ControlAddress) @@ -1322,7 +1338,7 @@ func (s *Sandbox) waitForStopped() error { return nil } // The sandbox process is a child of the current process, - // so we can wait it and collect its zombie. + // so we can wait on it to terminate and collect its zombie. if _, err := unix.Wait4(int(pid), &s.status, 0, nil); err != nil { return fmt.Errorf("error waiting the sandbox process: %v", err) } diff --git a/test/root/BUILD b/test/root/BUILD index f7cc6a21e..2e09da3d1 100644 --- a/test/root/BUILD +++ b/test/root/BUILD @@ -47,3 +47,23 @@ go_test( "@org_golang_x_sys//unix:go_default_library", ], ) + +go_test( + name = "portforward_test", + srcs = ["portforward_test.go"], + pure = True, + tags = [ + "local", + "manual", + ], + visibility = ["//:sandbox"], + deps = [ + "//pkg/cleanup", + "//pkg/test/dockerutil", + "//runsc/config", + "//runsc/flag", + "//runsc/specutils", + "@com_github_syndtr_gocapability//capability:go_default_library", + "@org_golang_x_sync//errgroup:go_default_library", + ], +) diff --git a/test/root/portforward_test.go b/test/root/portforward_test.go new file mode 100644 index 000000000..fc8a6fb86 --- /dev/null +++ b/test/root/portforward_test.go @@ -0,0 +1,160 @@ +// Copyright 2023 The gVisor 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 portforward_test holds a docker test for port forward. It is separate +// from other root tests so that both hostinet and netstack can be tested in +// one Makefile target. +package portforward_test + +import ( + "bytes" + "context" + "fmt" + "net" + "os" + "os/exec" + "strings" + "testing" + + "github.com/syndtr/gocapability/capability" + "golang.org/x/sync/errgroup" + "gvisor.dev/gvisor/pkg/cleanup" + "gvisor.dev/gvisor/pkg/test/dockerutil" + "gvisor.dev/gvisor/runsc/config" + "gvisor.dev/gvisor/runsc/flag" + "gvisor.dev/gvisor/runsc/specutils" +) + +func TestPortForward(t *testing.T) { + ctx := context.Background() + server := dockerutil.MakeContainer(ctx, t) + defer server.CleanUp(ctx) + + redisPort := 6379 + if err := server.Spawn(ctx, dockerutil.RunOpts{ + Image: "basic/redis", + }); err != nil { + t.Fatalf("failed to create redis server: %v", err) + } + + localPort, err := getUnusedPort() + if err != nil { + t.Fatalf("failed to pick unused port: %v", err) + } + ctx, cancel := context.WithCancel(ctx) + defer cancel() + pf, err := newPortForwardProcess(ctx, server, localPort, redisPort) + if err != nil { + t.Fatalf("failed to create port forward process: %v", err) + } + + var g errgroup.Group + g.Go(func() error { + pf.Wait() + if pf.Error() != nil { + return fmt.Errorf("portforward command: err: %v out: %s", pf.Error(), pf.Output()) + } + return nil + }) + cu := cleanup.Make(pf.Kill) + defer cu.Clean() + + client := dockerutil.MakeNativeContainer(ctx, t) + defer client.CleanUp(ctx) + + out, err := client.Run(ctx, dockerutil.RunOpts{ + Image: "basic/redis", + NetworkMode: "host", + }, "redis-cli", "--verbose", "-p", fmt.Sprintf("%d", localPort), "-i", "1", "-r", "5", "ping") + + if err != nil { + t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output()) + t.Fatalf("failed to run client: %v out: %s", err, out) + } + + if !strings.Contains(out, "PONG") { + t.Logf("portforward command: err: %v out: %s", pf.Error(), pf.Output()) + t.Fatalf("could not reach redis server: %s", out) + } + + cu.Clean() + if err := g.Wait(); err != nil { + t.Fatalf("failed to kill portforward process: %v", err) + } +} + +func getUnusedPort() (int, error) { + l, err := net.Listen("tcp", ":0") + if err != nil { + return 0, err + } + defer l.Close() + return l.Addr().(*net.TCPAddr).Port, nil +} + +type portForwardProcess struct { + cmd *exec.Cmd + buf bytes.Buffer +} + +func newPortForwardProcess(ctx context.Context, c *dockerutil.Container, localPort, containerPort int) (*portForwardProcess, error) { + rootDir, err := c.RootDirectory() + if err != nil { + return nil, err + } + args := []string{"-root", rootDir, "port-forward", c.ID(), fmt.Sprintf("%d:%d", localPort, containerPort)} + cmd := exec.CommandContext(ctx, specutils.ExePath, args...) + ret := &portForwardProcess{cmd: cmd} + ret.cmd.Stdout = &ret.buf + ret.cmd.Stderr = &ret.buf + if err := cmd.Start(); err != nil { + return nil, err + } + return ret, nil +} + +func (p *portForwardProcess) Close() error { + return p.cmd.Wait() +} + +func (p *portForwardProcess) Wait() { p.cmd.Wait() } + +func (p *portForwardProcess) Kill() { p.cmd.Process.Kill() } + +func (p *portForwardProcess) Output() string { return p.buf.String() } + +func (p *portForwardProcess) Error() error { return p.cmd.Err } + +func TestMain(m *testing.M) { + config.RegisterFlags(flag.CommandLine) + if !flag.CommandLine.Parsed() { + flag.Parse() + } + + if !specutils.HasCapabilities(capability.CAP_SYS_ADMIN, capability.CAP_DAC_OVERRIDE) { + fmt.Println("Test requires sysadmin privileges to run. Try again with sudo.") + os.Exit(1) + } + + dockerutil.EnsureSupportedDockerVersion() + + // Configure exe for tests. + path, err := dockerutil.RuntimePath() + if err != nil { + panic(err.Error()) + } + specutils.ExePath = path + + os.Exit(m.Run()) +}