diff options
-rw-r--r-- | cmd/podman/containers/stats.go | 12 | ||||
-rw-r--r-- | cmd/podman/system/dial_stdio.go | 145 | ||||
-rw-r--r-- | docs/source/markdown/podman-manifest.1.md | 44 | ||||
-rw-r--r-- | pkg/bindings/connection.go | 11 | ||||
-rw-r--r-- | pkg/domain/infra/abi/containers.go | 9 | ||||
-rw-r--r-- | pkg/specgen/specgen.go | 6 | ||||
-rw-r--r-- | pkg/specgen/specgen_test.go | 25 | ||||
-rw-r--r-- | test/e2e/system_dial_stdio_test.go | 53 |
8 files changed, 290 insertions, 15 deletions
diff --git a/cmd/podman/containers/stats.go b/cmd/podman/containers/stats.go index 11e8f6870..d21feaabc 100644 --- a/cmd/podman/containers/stats.go +++ b/cmd/podman/containers/stats.go @@ -11,9 +11,7 @@ import ( "github.com/containers/podman/v3/cmd/podman/registry" "github.com/containers/podman/v3/cmd/podman/validate" "github.com/containers/podman/v3/libpod/define" - "github.com/containers/podman/v3/pkg/cgroups" "github.com/containers/podman/v3/pkg/domain/entities" - "github.com/containers/podman/v3/pkg/rootless" "github.com/containers/podman/v3/utils" "github.com/docker/go-units" "github.com/pkg/errors" @@ -113,16 +111,6 @@ func checkStatOptions(cmd *cobra.Command, args []string) error { } func stats(cmd *cobra.Command, args []string) error { - if rootless.IsRootless() { - unified, err := cgroups.IsCgroup2UnifiedMode() - if err != nil { - return err - } - if !unified { - return errors.New("stats is not supported in rootless mode without cgroups v2") - } - } - // Convert to the entities options. We should not leak CLI-only // options into the backend and separate concerns. opts := entities.ContainerStatsOptions{ diff --git a/cmd/podman/system/dial_stdio.go b/cmd/podman/system/dial_stdio.go new file mode 100644 index 000000000..eae89f38e --- /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/docs/source/markdown/podman-manifest.1.md b/docs/source/markdown/podman-manifest.1.md index 6b82cc1ad..964f89afe 100644 --- a/docs/source/markdown/podman-manifest.1.md +++ b/docs/source/markdown/podman-manifest.1.md @@ -24,5 +24,49 @@ The `podman manifest` command provides subcommands which can be used to: | remove | [podman-manifest-remove(1)](podman-manifest-remove.1.md) | Remove an image from a manifest list or image index. | | rm | [podman-manifest-rme(1)](podman-manifest-rm.1.md) | Remove manifest list or image index from local storage. | +## EXAMPLES + +### Building a multi-arch manifest list from a Containerfile + +Assuming the `Containerfile` uses `RUN` instructions, the host needs +a way to execute non-native binaries. Configuring this is beyond +the scope of this example. Building a multi-arch manifest list +`shazam` in parallel across 4-threads can be done like this: + + $ platarch=linux/amd64,linux/ppc64le,linux/arm64,linux/s390x + $ podman build --jobs=4 --platform=$platarch --manifest shazam . + +**Note:** The `--jobs` argument is optional, and the `-t` or `--tag` +option should *not* be used. + +### Assembling a multi-arch manifest from separately built images + +Assuming `example.com/example/shazam:$arch` images are built separately +on other hosts and pushed to the `example.com` registry. They may +be combined into a manifest list, and pushed using a simple loop: + + $ REPO=example.com/example/shazam + $ podman manifest create $REPO:latest + $ for IMGTAG in amd64 s390x ppc64le arm64; do \ + podman manifest add $REPO:latest docker://$REPO:IMGTAG; \ + done + $ podman manifest push --all $REPO:latest + +**Note:** The `add` instruction argument order is `<manifest>` then `<image>`. +Also, the `--all` push option is required to ensure all contents are +pushed, not just the native platform/arch. + +### Removing and tagging a manifest list before pushing + +Special care is needed when removing and pushing manifest lists, as opposed +to the contents. You almost always want to use the `manifest rm` and +`manifest push --all` subcommands. For example, a rename and push could +be performed like this: + + $ podman tag localhost/shazam example.com/example/shazam + $ podman manifest rm localhost/shazam + $ podman manifest push --all example.com/example/shazam + + ## SEE ALSO podman(1), podman-manifest-add(1), podman-manifest-annotate(1), podman-manifest-create(1), podman-manifest-inspect(1), podman-manifest-push(1), podman-manifest-remove(1) diff --git a/pkg/bindings/connection.go b/pkg/bindings/connection.go index e2c46e481..dc75dac5a 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/pkg/domain/infra/abi/containers.go b/pkg/domain/infra/abi/containers.go index 6ca142618..c30129001 100644 --- a/pkg/domain/infra/abi/containers.go +++ b/pkg/domain/infra/abi/containers.go @@ -1319,6 +1319,15 @@ func (ic *ContainerEngine) ContainerStats(ctx context.Context, namesOrIds []stri if options.Interval < 1 { return nil, errors.New("Invalid interval, must be a positive number greater zero") } + if rootless.IsRootless() { + unified, err := cgroups.IsCgroup2UnifiedMode() + if err != nil { + return nil, err + } + if !unified { + return nil, errors.New("stats is not supported in rootless mode without cgroups v2") + } + } statsChan = make(chan entities.ContainerStatsReport, 1) containerFunc := ic.Libpod.GetRunningContainers diff --git a/pkg/specgen/specgen.go b/pkg/specgen/specgen.go index dbb669291..07995b2df 100644 --- a/pkg/specgen/specgen.go +++ b/pkg/specgen/specgen.go @@ -552,10 +552,10 @@ func NewSpecGenerator(arg string, rootfs bool) *SpecGenerator { if rootfs { csc.Rootfs = arg // check if rootfs is actually overlayed - parts := strings.SplitN(csc.Rootfs, ":", 2) - if len(parts) > 1 && parts[1] == "O" { + lastColonIndex := strings.LastIndex(csc.Rootfs, ":") + if lastColonIndex != -1 && lastColonIndex+1 < len(csc.Rootfs) && csc.Rootfs[lastColonIndex+1:] == "O" { csc.RootfsOverlay = true - csc.Rootfs = parts[0] + csc.Rootfs = csc.Rootfs[:lastColonIndex] } } else { csc.Image = arg diff --git a/pkg/specgen/specgen_test.go b/pkg/specgen/specgen_test.go new file mode 100644 index 000000000..b838d9d30 --- /dev/null +++ b/pkg/specgen/specgen_test.go @@ -0,0 +1,25 @@ +package specgen + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewSpecGeneratorWithRootfs(t *testing.T) { + tests := []struct { + rootfs string + expectedRootfsOverlay bool + expectedRootfs string + }{ + {"/root/a:b:O", true, "/root/a:b"}, + {"/root/a:b/c:O", true, "/root/a:b/c"}, + {"/root/a:b/c:", false, "/root/a:b/c:"}, + {"/root/a/b", false, "/root/a/b"}, + } + for _, args := range tests { + val := NewSpecGenerator(args.rootfs, true) + assert.Equal(t, val.RootfsOverlay, args.expectedRootfsOverlay) + assert.Equal(t, val.Rootfs, args.expectedRootfs) + } +} diff --git a/test/e2e/system_dial_stdio_test.go b/test/e2e/system_dial_stdio_test.go new file mode 100644 index 000000000..afe3d5acd --- /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")) + }) +}) |