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")) + }) +})