From 6f9e9ee9ece7396a2b769c1e240eaa893cfce2ae Mon Sep 17 00:00:00 2001
From: Jake Parks <jamesparks10@gmail.com>
Date: Thu, 30 Sep 2021 19:41:25 +0000
Subject: [PATCH] Fixes #11668

Adding dial-stdio CLI cmd

Signed-off-by: Jake Parks <jamesparks10@gmail.com>

Made dial-stdio URI configurable

Slight refactors

Signed-off-by: Jake Parks <jamesparks10@gmail.com>

Added simple test for existence of `podman system dial-stdio` command

Fix 'system dial-stdio' integration tests

Changed link in comment to permalink
---
 cmd/podman/system/dial_stdio.go    | 145 +++++++++++++++++++++++++++++
 pkg/bindings/connection.go         |  11 +++
 test/e2e/system_dial_stdio_test.go |  53 +++++++++++
 3 files changed, 209 insertions(+)
 create mode 100644 cmd/podman/system/dial_stdio.go
 create mode 100644 test/e2e/system_dial_stdio_test.go

diff --git a/cmd/podman/system/dial_stdio.go b/cmd/podman/system/dial_stdio.go
new file mode 100644
index 0000000000..eae89f38e4
--- /dev/null
+++ b/cmd/podman/system/dial_stdio.go
@@ -0,0 +1,145 @@
+package system
+
+import (
+	"context"
+	"io"
+	"os"
+
+	"github.com/containers/podman/v3/cmd/podman/registry"
+	"github.com/containers/podman/v3/cmd/podman/validate"
+	"github.com/containers/podman/v3/pkg/bindings"
+	"github.com/pkg/errors"
+	"github.com/sirupsen/logrus"
+	"github.com/spf13/cobra"
+)
+
+var (
+	dialStdioCommand = &cobra.Command{
+		Use:    "dial-stdio",
+		Short:  "Proxy the stdio stream to the daemon connection. Should not be invoked manually.",
+		Args:   validate.NoArgs,
+		Hidden: true,
+		RunE: func(cmd *cobra.Command, args []string) error {
+			return runDialStdio()
+		},
+		Example: "podman system dial-stdio",
+	}
+)
+
+func init() {
+	registry.Commands = append(registry.Commands, registry.CliCommand{
+		Command: dialStdioCommand,
+		Parent:  systemCmd,
+	})
+}
+
+func runDialStdio() error {
+	ctx := registry.Context()
+	cfg := registry.PodmanConfig()
+	ctx, cancel := context.WithCancel(ctx)
+	defer cancel()
+	bindCtx, err := bindings.NewConnection(ctx, cfg.URI)
+	if err != nil {
+		return errors.Wrap(err, "failed to open connection to podman")
+	}
+	conn, err := bindings.GetClient(bindCtx)
+	if err != nil {
+		return errors.Wrap(err, "failed to get connection after initialization")
+	}
+	netConn, err := conn.GetDialer(bindCtx)
+	if err != nil {
+		return errors.Wrap(err, "failed to open the raw stream connection")
+	}
+	defer netConn.Close()
+
+	var connHalfCloser halfCloser
+	switch t := netConn.(type) {
+	case halfCloser:
+		connHalfCloser = t
+	case halfReadWriteCloser:
+		connHalfCloser = &nopCloseReader{t}
+	default:
+		return errors.New("the raw stream connection does not implement halfCloser")
+	}
+
+	stdin2conn := make(chan error, 1)
+	conn2stdout := make(chan error, 1)
+	go func() {
+		stdin2conn <- copier(connHalfCloser, &halfReadCloserWrapper{os.Stdin}, "stdin to stream")
+	}()
+	go func() {
+		conn2stdout <- copier(&halfWriteCloserWrapper{os.Stdout}, connHalfCloser, "stream to stdout")
+	}()
+	select {
+	case err = <-stdin2conn:
+		if err != nil {
+			return err
+		}
+		// wait for stdout
+		err = <-conn2stdout
+	case err = <-conn2stdout:
+		// return immediately
+	}
+	return err
+}
+
+// Below portion taken from original docker CLI
+// https://github.com/docker/cli/blob/v20.10.9/cli/command/system/dial_stdio.go
+func copier(to halfWriteCloser, from halfReadCloser, debugDescription string) error {
+	defer func() {
+		if err := from.CloseRead(); err != nil {
+			logrus.Errorf("error while CloseRead (%s): %v", debugDescription, err)
+		}
+		if err := to.CloseWrite(); err != nil {
+			logrus.Errorf("error while CloseWrite (%s): %v", debugDescription, err)
+		}
+	}()
+	if _, err := io.Copy(to, from); err != nil {
+		return errors.Wrapf(err, "error while Copy (%s)", debugDescription)
+	}
+	return nil
+}
+
+type halfReadCloser interface {
+	io.Reader
+	CloseRead() error
+}
+
+type halfWriteCloser interface {
+	io.Writer
+	CloseWrite() error
+}
+
+type halfCloser interface {
+	halfReadCloser
+	halfWriteCloser
+}
+
+type halfReadWriteCloser interface {
+	io.Reader
+	halfWriteCloser
+}
+
+type nopCloseReader struct {
+	halfReadWriteCloser
+}
+
+func (x *nopCloseReader) CloseRead() error {
+	return nil
+}
+
+type halfReadCloserWrapper struct {
+	io.ReadCloser
+}
+
+func (x *halfReadCloserWrapper) CloseRead() error {
+	return x.Close()
+}
+
+type halfWriteCloserWrapper struct {
+	io.WriteCloser
+}
+
+func (x *halfWriteCloserWrapper) CloseWrite() error {
+	return x.Close()
+}
diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go
index e2c46e481b..dc75dac5a1 100644
--- a/pkg/bindings/connection.go
+++ b/pkg/bindings/connection.go
@@ -349,6 +349,17 @@ func (c *Connection) DoRequest(httpBody io.Reader, httpMethod, endpoint string,
 	return &APIResponse{response, req}, err
 }
 
+// Get raw Transport.DialContext from client
+func (c *Connection) GetDialer(ctx context.Context) (net.Conn, error) {
+	client := c.Client
+	transport := client.Transport.(*http.Transport)
+	if transport.DialContext != nil && transport.TLSClientConfig == nil {
+		return transport.DialContext(ctx, c.URI.Scheme, c.URI.String())
+	}
+
+	return nil, errors.New("Unable to get dial context")
+}
+
 // FiltersToString converts our typical filter format of a
 // map[string][]string to a query/html safe string.
 func FiltersToString(filters map[string][]string) (string, error) {
diff --git a/test/e2e/system_dial_stdio_test.go b/test/e2e/system_dial_stdio_test.go
new file mode 100644
index 0000000000..afe3d5acd6
--- /dev/null
+++ b/test/e2e/system_dial_stdio_test.go
@@ -0,0 +1,53 @@
+package integration
+
+import (
+	"fmt"
+	"os"
+
+	. "github.com/containers/podman/v3/test/utils"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/gexec"
+)
+
+var _ = Describe("podman system dial-stdio", func() {
+	var (
+		tempdir    string
+		err        error
+		podmanTest *PodmanTestIntegration
+	)
+
+	BeforeEach(func() {
+		tempdir, err = CreateTempDirInTempDir()
+		if err != nil {
+			os.Exit(1)
+		}
+		podmanTest = PodmanTestCreate(tempdir)
+		podmanTest.Setup()
+		podmanTest.SeedImages()
+	})
+
+	AfterEach(func() {
+		podmanTest.Cleanup()
+		f := CurrentGinkgoTestDescription()
+		timedResult := fmt.Sprintf("Test: %s completed in %f seconds", f.TestText, f.Duration.Seconds())
+		GinkgoWriter.Write([]byte(timedResult))
+	})
+
+	It("podman system dial-stdio help", func() {
+		session := podmanTest.Podman([]string{"system", "dial-stdio", "--help"})
+		session.WaitWithDefaultTimeout()
+		Expect(session).Should(Exit(0))
+		Expect(session.OutputToString()).To(ContainSubstring("Examples: podman system dial-stdio"))
+	})
+
+	It("podman system dial-stdio while service is not running", func() {
+		if IsRemote() {
+			Skip("this test is only for non-remote")
+		}
+		session := podmanTest.Podman([]string{"system", "dial-stdio"})
+		session.WaitWithDefaultTimeout()
+		Expect(session).Should(Exit(125))
+		Expect(session.ErrorToString()).To(ContainSubstring("Error: failed to open connection to podman"))
+	})
+})