aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cirrus.yml45
-rw-r--r--cmd/podman/cliconfig/config.go3
-rw-r--r--cmd/podman/commands.go2
-rw-r--r--cmd/podman/container.go1
-rw-r--r--cmd/podman/logs.go58
-rw-r--r--cmd/podman/main.go1
-rw-r--r--cmd/podman/runlabel.go28
-rw-r--r--cmd/podman/search.go19
-rw-r--r--cmd/podman/shared/container.go80
-rw-r--r--cmd/podman/tree.go2
-rw-r--r--cmd/podman/varlink/io.podman.varlink10
-rw-r--r--completions/bash/podman1
-rw-r--r--contrib/cirrus/lib.sh13
-rwxr-xr-xcontrib/cirrus/notice_master_failure.sh19
-rwxr-xr-xcontrib/cirrus/setup_environment.sh2
-rw-r--r--docs/podman-container-runlabel.1.md6
-rw-r--r--docs/podman-image.1.md2
-rw-r--r--docs/podman-load.1.md2
-rw-r--r--docs/podman-logs.1.md6
-rw-r--r--docs/podman-run.1.md9
-rw-r--r--libpod/container_internal.go4
-rw-r--r--libpod/container_internal_linux.go17
-rw-r--r--libpod/container_internal_unsupported.go4
-rw-r--r--libpod/container_log.go208
-rw-r--r--libpod/events.go2
-rw-r--r--libpod/networking_linux.go5
-rw-r--r--libpod/oci.go7
-rw-r--r--libpod/runtime_ctr.go2
-rw-r--r--pkg/adapter/containers.go27
-rw-r--r--pkg/adapter/containers_remote.go47
-rw-r--r--pkg/varlinkapi/containers.go54
-rw-r--r--test/e2e/logs_test.go28
-rw-r--r--test/e2e/ps_test.go14
33 files changed, 647 insertions, 81 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
index 8e26ce72e..14ba3fc6e 100644
--- a/.cirrus.yml
+++ b/.cirrus.yml
@@ -140,6 +140,10 @@ gating_task:
- '/usr/local/bin/entrypoint.sh clean podman BUILDTAGS="exclude_graphdriver_devicemapper selinux seccomp"'
- '/usr/local/bin/entrypoint.sh clean podman-remote-darwin'
+ on_failure:
+ master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh'
+
+
build_each_commit_task:
depends_on:
@@ -160,9 +164,12 @@ build_each_commit_task:
timeout_in: 30m
script:
- - $SCRIPT_BASE/setup_environment.sh
- - git fetch --depth $CIRRUS_CLONE_DEPTH origin $CIRRUS_BASE_BRANCH
- - env GOPATH=/var/tmp/go/ make build-all-new-commits GIT_BASE_BRANCH=origin/$CIRRUS_BASE_BRANCH
+ - '$SCRIPT_BASE/setup_environment.sh'
+ - 'git fetch --depth $CIRRUS_CLONE_DEPTH origin $CIRRUS_BASE_BRANCH'
+ - 'env GOPATH=/var/tmp/go/ make build-all-new-commits GIT_BASE_BRANCH=origin/$CIRRUS_BASE_BRANCH'
+
+ on_failure:
+ master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh'
# Update metadata on VM images referenced by this repository state
@@ -186,7 +193,7 @@ meta_task:
GCPPROJECT: ENCRYPTED[7c80e728e046b1c76147afd156a32c1c57d4a1ac1eab93b7e68e718c61ca8564fc61fef815952b8ae0a64e7034b8fe4f]
CIRRUS_CLONE_DEPTH: 1 # source not used
- script: /usr/local/bin/entrypoint.sh
+ script: '/usr/local/bin/entrypoint.sh'
# This task does the unit and integration testing for every platform
@@ -219,14 +226,12 @@ testing_task:
# Every *_script runs in sequence, for each task. The name prefix is for
# WebUI reference. The values may be strings...
- setup_environment_script: $SCRIPT_BASE/setup_environment.sh
+ setup_environment_script: '$SCRIPT_BASE/setup_environment.sh'
+ unit_test_script: '$SCRIPT_BASE/unit_test.sh'
+ integration_test_script: '$SCRIPT_BASE/integration_test.sh'
- # ...or lists of strings
- unit_test_script:
- - go version
- - $SCRIPT_BASE/unit_test.sh
-
- integration_test_script: $SCRIPT_BASE/integration_test.sh
+ on_failure:
+ master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh'
# This task executes tests as a regular user on a system
@@ -252,12 +257,15 @@ rootless_testing_task:
timeout_in: 120m
- setup_environment_script: $SCRIPT_BASE/setup_environment.sh
+ setup_environment_script: '$SCRIPT_BASE/setup_environment.sh'
rootless_test_script: >-
ssh $ROOTLESS_USER@localhost
-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -o CheckHostIP=no
$CIRRUS_WORKING_DIR/$SCRIPT_BASE/rootless_test.sh
+ on_failure:
+ master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh'
+
# Because system tests are stored within the repository, it is sometimes
# necessary to execute them within a PR to validate changes.
@@ -283,8 +291,8 @@ optional_testing_task:
timeout_in: 60m
- setup_environment_script: $SCRIPT_BASE/setup_environment.sh
- system_test_script: $SCRIPT_BASE/system_test.sh
+ setup_environment_script: '$SCRIPT_BASE/setup_environment.sh'
+ system_test_script: '$SCRIPT_BASE/system_test.sh'
# Build new cache-images for future PR testing, but only after a PR merge.
@@ -317,8 +325,8 @@ cache_images_task:
scopes:
- compute
- devstorage.full_control
- environment_script: $SCRIPT_BASE/setup_environment.sh
- build_vm_images_script: $SCRIPT_BASE/build_vm_images.sh
+ environment_script: '$SCRIPT_BASE/setup_environment.sh'
+ build_vm_images_script: '$SCRIPT_BASE/build_vm_images.sh'
# TODO,Continuous Delivery: Automatically open a libpod PR after using 'sed' to replace
# the image_names with the new (just build) images. That will
@@ -331,6 +339,9 @@ cache_images_task:
# - modify_cirrus_yaml_image_names.sh
# - commit_and_create_upstream_pr.sh
+ on_failure:
+ master_script: '$CIRRUS_WORKING_DIR/$SCRIPT_BASE/notice_master_failure.sh'
+
# Post message to IRC if everything passed
success_task:
@@ -350,4 +361,4 @@ success_task:
cpu: 1
memory: 1
- success_script: $SCRIPT_BASE/success.sh
+ success_script: '$SCRIPT_BASE/success.sh'
diff --git a/cmd/podman/cliconfig/config.go b/cmd/podman/cliconfig/config.go
index cb9d9a338..1461c9f03 100644
--- a/cmd/podman/cliconfig/config.go
+++ b/cmd/podman/cliconfig/config.go
@@ -421,14 +421,15 @@ type RmiValues struct {
type RunlabelValues struct {
PodmanCommand
Authfile string
- Display bool
CertDir string
Creds string
+ Display bool
Name string
Opt1 string
Opt2 string
Opt3 string
Quiet bool
+ Replace bool
SignaturePolicy string
TlsVerify bool
}
diff --git a/cmd/podman/commands.go b/cmd/podman/commands.go
index d37af70c1..810c5a6f6 100644
--- a/cmd/podman/commands.go
+++ b/cmd/podman/commands.go
@@ -21,7 +21,6 @@ func getMainCommands() []*cobra.Command {
&_psCommand,
_loginCommand,
_logoutCommand,
- _logsCommand,
_mountCommand,
_pauseCommand,
_portCommand,
@@ -63,7 +62,6 @@ func getContainerSubCommands() []*cobra.Command {
_execCommand,
_exportCommand,
_killCommand,
- _logsCommand,
_mountCommand,
_pauseCommand,
_portCommand,
diff --git a/cmd/podman/container.go b/cmd/podman/container.go
index ce6ad8883..2e9cedbaa 100644
--- a/cmd/podman/container.go
+++ b/cmd/podman/container.go
@@ -53,6 +53,7 @@ var (
_containerExistsCommand,
_contInspectSubCommand,
_listSubCommand,
+ _logsCommand,
}
)
diff --git a/cmd/podman/logs.go b/cmd/podman/logs.go
index c3416fe57..a1b5fb4cc 100644
--- a/cmd/podman/logs.go
+++ b/cmd/podman/logs.go
@@ -1,27 +1,24 @@
package main
import (
- "os"
"time"
"github.com/containers/libpod/cmd/podman/cliconfig"
- "github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/libpod"
- "github.com/containers/libpod/pkg/logs"
+ "github.com/containers/libpod/pkg/adapter"
"github.com/containers/libpod/pkg/util"
"github.com/pkg/errors"
- "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
var (
logsCommand cliconfig.LogsValues
- logsDescription = `Retrieves logs for a container.
+ logsDescription = `Retrieves logs for one or more containers.
This does not guarantee execution order when combined with podman run (i.e. your run may not have generated any logs at the time you execute podman logs.
`
_logsCommand = &cobra.Command{
- Use: "logs [flags] CONTAINER",
+ Use: "logs [flags] CONTAINER [CONTAINER...]",
Short: "Fetch the logs of a container",
Long: logsDescription,
RunE: func(cmd *cobra.Command, args []string) error {
@@ -29,9 +26,19 @@ var (
logsCommand.GlobalFlags = MainGlobalOpts
return logsCmd(&logsCommand)
},
+ Args: func(cmd *cobra.Command, args []string) error {
+ if len(args) > 0 && logsCommand.Latest {
+ return errors.New("no containers can be specified when using 'latest'")
+ }
+ if !logsCommand.Latest && len(args) < 1 {
+ return errors.New("specify at least one container name or ID to log")
+ }
+ return nil
+ },
Example: `podman logs ctrID
podman logs --tail 2 mywebserver
- podman logs --follow=true --since 10m ctrID`,
+ podman logs --follow=true --since 10m ctrID
+ podman logs mywebserver mydbserver`,
}
)
@@ -54,20 +61,14 @@ func init() {
}
func logsCmd(c *cliconfig.LogsValues) error {
- var ctr *libpod.Container
var err error
- runtime, err := libpodruntime.GetRuntime(&c.PodmanCommand)
+ runtime, err := adapter.GetRuntime(&c.PodmanCommand)
if err != nil {
return errors.Wrapf(err, "could not get runtime")
}
defer runtime.Shutdown(false)
- args := c.InputArgs
- if len(args) != 1 && !c.Latest {
- return errors.Errorf("'podman logs' requires exactly one container name/ID")
- }
-
sinceTime := time.Time{}
if c.Flag("since").Changed {
// parse time, error out if something is wrong
@@ -78,7 +79,7 @@ func logsCmd(c *cliconfig.LogsValues) error {
sinceTime = since
}
- opts := &logs.LogOptions{
+ opts := &libpod.LogOptions{
Details: c.Details,
Follow: c.Follow,
Since: sinceTime,
@@ -86,30 +87,5 @@ func logsCmd(c *cliconfig.LogsValues) error {
Timestamps: c.Timestamps,
}
- if c.Latest {
- ctr, err = runtime.GetLatestContainer()
- } else {
- ctr, err = runtime.LookupContainer(args[0])
- }
- if err != nil {
- return err
- }
-
- logPath := ctr.LogPath()
-
- state, err := ctr.State()
- if err != nil {
- return err
- }
-
- // If the log file does not exist yet and the container is in the
- // Configured state, it has never been started before and no logs exist
- // Exit cleanly in this case
- if _, err := os.Stat(logPath); err != nil {
- if state == libpod.ContainerStateConfigured {
- logrus.Debugf("Container has not been started, no logs exist yet")
- return nil
- }
- }
- return logs.ReadLogs(logPath, ctr, opts)
+ return runtime.Log(c, opts)
}
diff --git a/cmd/podman/main.go b/cmd/podman/main.go
index fa98dc53c..ef300ef75 100644
--- a/cmd/podman/main.go
+++ b/cmd/podman/main.go
@@ -45,6 +45,7 @@ var mainCommands = []*cobra.Command{
&_inspectCommand,
_killCommand,
_loadCommand,
+ _logsCommand,
podCommand.Command,
_pullCommand,
_pushCommand,
diff --git a/cmd/podman/runlabel.go b/cmd/podman/runlabel.go
index 229ff1201..f79aa8b0e 100644
--- a/cmd/podman/runlabel.go
+++ b/cmd/podman/runlabel.go
@@ -10,9 +10,11 @@ import (
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/libpodruntime"
"github.com/containers/libpod/cmd/podman/shared"
+ "github.com/containers/libpod/libpod"
"github.com/containers/libpod/libpod/image"
"github.com/containers/libpod/utils"
"github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
@@ -45,6 +47,7 @@ func init() {
flags.StringVar(&runlabelCommand.CertDir, "cert-dir", "", "`Pathname` of a directory containing TLS certificates and keys")
flags.StringVar(&runlabelCommand.Creds, "creds", "", "`Credentials` (USERNAME:PASSWORD) to use for authenticating to a registry")
flags.BoolVar(&runlabelCommand.Display, "display", false, "Preview the command that the label would run")
+ flags.BoolVar(&runlabelCommand.Replace, "replace", false, "Replace existing container with a new one from the image")
flags.StringVar(&runlabelCommand.Name, "name", "", "Assign a name to the container")
flags.StringVar(&runlabelCommand.Opt1, "opt1", "", "Optional parameter to pass for install")
@@ -146,10 +149,33 @@ func runlabelCmd(c *cliconfig.RunlabelValues) error {
return err
}
if !c.Quiet {
- fmt.Printf("Command: %s\n", strings.Join(cmd, " "))
+ fmt.Printf("command: %s\n", strings.Join(cmd, " "))
if c.Display {
return nil
}
}
+
+ // If container already exists && --replace given -- Nuke it
+ if c.Replace {
+ for i, entry := range cmd {
+ if entry == "--name" {
+ name := cmd[i+1]
+ ctr, err := runtime.LookupContainer(name)
+ if err != nil {
+ if errors.Cause(err) != libpod.ErrNoSuchCtr {
+ logrus.Debugf("Error occurred searching for container %s: %s", name, err.Error())
+ return err
+ }
+ } else {
+ logrus.Debugf("Runlabel --replace option given. Container %s will be deleted. The new container will be named %s", ctr.ID(), name)
+ if err := runtime.RemoveContainer(ctx, ctr, true, false); err != nil {
+ return err
+ }
+ }
+ break
+ }
+ }
+ }
+
return utils.ExecCmdWithStdStreams(stdIn, stdOut, stdErr, env, cmd[0], cmd[1:]...)
}
diff --git a/cmd/podman/search.go b/cmd/podman/search.go
index 5997e144a..a10b9d419 100644
--- a/cmd/podman/search.go
+++ b/cmd/podman/search.go
@@ -1,6 +1,7 @@
package main
import (
+ "reflect"
"strings"
"github.com/containers/buildah/pkg/formats"
@@ -79,7 +80,10 @@ func searchCmd(c *cliconfig.SearchValues) error {
return err
}
format := genSearchFormat(c.Format)
- out := formats.StdoutTemplateArray{Output: searchToGeneric(results), Template: format, Fields: results[0].HeaderMap()}
+ if len(results) == 0 {
+ return nil
+ }
+ out := formats.StdoutTemplateArray{Output: searchToGeneric(results), Template: format, Fields: genSearchOutputMap()}
formats.Writer(out).Out()
return nil
}
@@ -99,3 +103,16 @@ func searchToGeneric(params []image.SearchResult) (genericParams []interface{})
}
return genericParams
}
+
+func genSearchOutputMap() map[string]string {
+ io := image.SearchResult{}
+ v := reflect.Indirect(reflect.ValueOf(io))
+ values := make(map[string]string)
+
+ for i := 0; i < v.NumField(); i++ {
+ key := v.Type().Field(i).Name
+ value := key
+ values[key] = strings.ToUpper(splitCamelCase(value))
+ }
+ return values
+}
diff --git a/cmd/podman/shared/container.go b/cmd/podman/shared/container.go
index 41950928e..6826191c5 100644
--- a/cmd/podman/shared/container.go
+++ b/cmd/podman/shared/container.go
@@ -3,11 +3,11 @@ package shared
import (
"context"
"fmt"
- "github.com/google/shlex"
"io"
"os"
"path/filepath"
"regexp"
+ "sort"
"strconv"
"strings"
"sync"
@@ -21,6 +21,7 @@ import (
"github.com/containers/libpod/pkg/util"
"github.com/cri-o/ocicni/pkg/ocicni"
"github.com/docker/go-units"
+ "github.com/google/shlex"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -583,18 +584,93 @@ func getCgroup(spec *specs.Spec) string {
return cgroup
}
+func comparePorts(i, j ocicni.PortMapping) bool {
+ if i.ContainerPort != j.ContainerPort {
+ return i.ContainerPort < j.ContainerPort
+ }
+
+ if i.HostIP != j.HostIP {
+ return i.HostIP < j.HostIP
+ }
+
+ if i.HostPort != j.HostPort {
+ return i.HostPort < j.HostPort
+ }
+
+ return i.Protocol < j.Protocol
+}
+
+// returns the group as <IP:startPort:lastPort->startPort:lastPort/Proto>
+// e.g 0.0.0.0:1000-1006->1000-1006/tcp
+func formatGroup(key string, start, last int32) string {
+ parts := strings.Split(key, "/")
+ groupType := parts[0]
+ var ip string
+ if len(parts) > 1 {
+ ip = parts[0]
+ groupType = parts[1]
+ }
+ group := strconv.Itoa(int(start))
+ if start != last {
+ group = fmt.Sprintf("%s-%d", group, last)
+ }
+ if ip != "" {
+ group = fmt.Sprintf("%s:%s->%s", ip, group, group)
+ }
+ return fmt.Sprintf("%s/%s", group, groupType)
+}
+
// portsToString converts the ports used to a string of the from "port1, port2"
+// also groups continuous list of ports in readable format.
func portsToString(ports []ocicni.PortMapping) string {
+ type portGroup struct {
+ first int32
+ last int32
+ }
var portDisplay []string
if len(ports) == 0 {
return ""
}
+ //Sort the ports, so grouping continuous ports become easy.
+ sort.Slice(ports, func(i, j int) bool {
+ return comparePorts(ports[i], ports[j])
+ })
+
+ // portGroupMap is used for grouping continuous ports
+ portGroupMap := make(map[string]*portGroup)
+ var groupKeyList []string
+
for _, v := range ports {
+
hostIP := v.HostIP
if hostIP == "" {
hostIP = "0.0.0.0"
}
- portDisplay = append(portDisplay, fmt.Sprintf("%s:%d->%d/%s", hostIP, v.HostPort, v.ContainerPort, v.Protocol))
+ // if hostPort and containerPort are not same, consider as individual port.
+ if v.ContainerPort != v.HostPort {
+ portDisplay = append(portDisplay, fmt.Sprintf("%s:%d->%d/%s", hostIP, v.HostPort, v.ContainerPort, v.Protocol))
+ continue
+ }
+
+ portMapKey := fmt.Sprintf("%s/%s", hostIP, v.Protocol)
+
+ portgroup, ok := portGroupMap[portMapKey]
+ if !ok {
+ portGroupMap[portMapKey] = &portGroup{first: v.ContainerPort, last: v.ContainerPort}
+ // this list is required to travese portGroupMap
+ groupKeyList = append(groupKeyList, portMapKey)
+ continue
+ }
+
+ if portgroup.last == (v.ContainerPort - 1) {
+ portgroup.last = v.ContainerPort
+ continue
+ }
+ }
+ // for each portMapKey, format group list and appned to output string
+ for _, portKey := range groupKeyList {
+ group := portGroupMap[portKey]
+ portDisplay = append(portDisplay, formatGroup(portKey, group.first, group.last))
}
return strings.Join(portDisplay, ", ")
}
diff --git a/cmd/podman/tree.go b/cmd/podman/tree.go
index ebda18cdb..c56e35aef 100644
--- a/cmd/podman/tree.go
+++ b/cmd/podman/tree.go
@@ -23,7 +23,7 @@ var (
treeDescription = "Prints layer hierarchy of an image in a tree format"
_treeCommand = &cobra.Command{
- Use: "tree",
+ Use: "tree [flags] IMAGE",
Short: treeDescription,
Long: treeDescription,
RunE: func(cmd *cobra.Command, args []string) error {
diff --git a/cmd/podman/varlink/io.podman.varlink b/cmd/podman/varlink/io.podman.varlink
index 791790e2e..517a7a2a1 100644
--- a/cmd/podman/varlink/io.podman.varlink
+++ b/cmd/podman/varlink/io.podman.varlink
@@ -19,6 +19,14 @@ type StringResponse (
message: string
)
+type LogLine (
+ device: string,
+ parseLogType : string,
+ time: string,
+ msg: string,
+ cid: string
+)
+
# ContainerChanges describes the return struct for ListContainerChanges
type ContainerChanges (
changed: []string,
@@ -522,6 +530,8 @@ method ListContainerProcesses(name: string, opts: []string) -> (container: []str
# capability of varlink if the client invokes it.
method GetContainerLogs(name: string) -> (container: []string)
+method GetContainersLogs(names: []string, follow: bool, latest: bool, since: string, tail: int, timestamps: bool) -> (log: LogLine)
+
# ListContainerChanges takes a name or ID of a container and returns changes between the container and
# its base image. It returns a struct of changed, deleted, and added path names.
method ListContainerChanges(name: string) -> (container: ContainerChanges)
diff --git a/completions/bash/podman b/completions/bash/podman
index d8354fa80..1976bff44 100644
--- a/completions/bash/podman
+++ b/completions/bash/podman
@@ -2469,6 +2469,7 @@ _podman_container_runlabel() {
-h
-q
--quiet
+ --replace
--tls-verify
"
diff --git a/contrib/cirrus/lib.sh b/contrib/cirrus/lib.sh
index 9419dad05..773f4f484 100644
--- a/contrib/cirrus/lib.sh
+++ b/contrib/cirrus/lib.sh
@@ -103,6 +103,15 @@ clean_env() {
unset -v UNSET_ENV_VARS $UNSET_ENV_VARS || true # don't fail on read-only
}
+die() {
+ req_env_var "
+ 1 $1
+ 2 $2
+ "
+ echo "$2"
+ exit $1
+}
+
# Return a GCE image-name compatible string representation of distribution name
os_release_id() {
eval "$(egrep -m 1 '^ID=' /etc/os-release | tr -d \' | tr -d \")"
@@ -136,14 +145,14 @@ stub() {
ircmsg() {
req_env_var "
CIRRUS_TASK_ID $CIRRUS_TASK_ID
- 1 $1
+ @ $@
"
# Sometimes setup_environment.sh didn't run
SCRIPT="$(dirname $0)/podbot.py"
NICK="podbot_$CIRRUS_TASK_ID"
NICK="${NICK:0:15}" # Any longer will break things
set +e
- $SCRIPT $NICK $1
+ $SCRIPT $NICK $@
echo "Ignoring exit($?)"
set -e
}
diff --git a/contrib/cirrus/notice_master_failure.sh b/contrib/cirrus/notice_master_failure.sh
new file mode 100755
index 000000000..4b09331d3
--- /dev/null
+++ b/contrib/cirrus/notice_master_failure.sh
@@ -0,0 +1,19 @@
+#!/bin/bash
+
+set -e
+
+source $(dirname $0)/lib.sh
+
+# mIRC "escape" codes are the most standard, for a non-standard client-side interpretation.
+ETX="$(echo -n -e '\x03')"
+RED="${ETX}4"
+NOR="$(echo -n -e '\x0f')"
+
+if [[ "$CIRRUS_BRANCH" =~ "master" ]]
+then
+ BURL="https://cirrus-ci.com/build/$CIRRUS_BUILD_ID"
+ ircmsg "${RED}[Action Recommended]: ${NOR}Post-merge testing ${RED}$CIRRUS_BRANCH failed${NOR} in $CIRRUS_TASK_NAME on $(os_release_id)-$(os_release_ver): $BURL. Please investigate, and re-run if appropriate."
+fi
+
+# This script assumed to be executed on failure
+die 1 "Testing Failed"
diff --git a/contrib/cirrus/setup_environment.sh b/contrib/cirrus/setup_environment.sh
index ead2f7343..618027ecd 100755
--- a/contrib/cirrus/setup_environment.sh
+++ b/contrib/cirrus/setup_environment.sh
@@ -89,4 +89,6 @@ then
fi
fi
+show_env_vars
+
record_timestamp "env. setup end"
diff --git a/docs/podman-container-runlabel.1.md b/docs/podman-container-runlabel.1.md
index 7547f7187..7fa9805e6 100644
--- a/docs/podman-container-runlabel.1.md
+++ b/docs/podman-container-runlabel.1.md
@@ -12,6 +12,7 @@ podman-container-runlabel - Execute Image Label Method
[**--rootfs**=*ROOTFS*]
[**--set**=*NAME*=*VALUE*]
[**--storage**]
+[**--replace**]
LABEL IMAGE [ARG...]
# DESCRIPTION
@@ -85,6 +86,11 @@ Print usage statement
Suppress output information when pulling images
+**--replace**
+
+If a container exists of the default or given name, as needed it will be stopped, deleted and a new container will be
+created from this image.
+
**--signature-policy="PATHNAME"**
Pathname of a signature policy file to use. It is not recommended that this
diff --git a/docs/podman-image.1.md b/docs/podman-image.1.md
index 54960045f..333a75b69 100644
--- a/docs/podman-image.1.md
+++ b/docs/podman-image.1.md
@@ -27,8 +27,8 @@ The image command allows you to manage images
| save | [podman-save(1)](podman-save.1.md) | Save an image to docker-archive or oci. |
| sign | [podman-image-sign(1)](podman-image-sign.1.md) | Sign an image. |
| tag | [podman-tag(1)](podman-tag.1.md) | Add an additional name to a local image. |
+| tree | [podman-image-tree(1)](podman-image-tree.1.md) | Prints layer hierarchy of an image in a tree format. |
| trust | [podman-image-trust(1)](podman-image-trust.1.md)| Manage container image trust policy. |
-| tree | [podman-image-tree(1)](podman-image-tree.1.md) | Prints layer hierarchy of an image in a tree format |
## SEE ALSO
podman
diff --git a/docs/podman-load.1.md b/docs/podman-load.1.md
index 8b6501a5c..5363f3f1e 100644
--- a/docs/podman-load.1.md
+++ b/docs/podman-load.1.md
@@ -4,7 +4,7 @@
podman\-load - Load an image from docker archive
## SYNOPSIS
-**podman load** *name*[:*tag*|@*digest*]
+**podman load** [ARCHIVE]
## DESCRIPTION
**podman load** copies an image from either **docker-archive** or **oci-archive** stored
diff --git a/docs/podman-logs.1.md b/docs/podman-logs.1.md
index bc02df954..8cd6ad5e7 100644
--- a/docs/podman-logs.1.md
+++ b/docs/podman-logs.1.md
@@ -1,13 +1,13 @@
% podman-logs(1)
## NAME
-podman\-logs - Fetch the logs of a container
+podman\-logs - Fetch the logs of one or more containers
## SYNOPSIS
-**podman** **logs** [*options*] *container*
+**podman** **logs** [*options*] *container* [*container...*]
## DESCRIPTION
-The podman logs command batch-retrieves whatever logs are present for a container at the time of execution.
+The podman logs command batch-retrieves whatever logs are present for one or more containers at the time of execution.
This does not guarantee execution order when combined with podman run (i.e. your run may not have generated
any logs at the time you execute podman logs
diff --git a/docs/podman-run.1.md b/docs/podman-run.1.md
index fe98e43ca..cf385717e 100644
--- a/docs/podman-run.1.md
+++ b/docs/podman-run.1.md
@@ -1129,6 +1129,15 @@ KillMode=process
WantedBy=multi-user.target
```
+### Configuring Storage Options from the command line
+
+Podman allows for the configuration of storage by changing the values
+in the /etc/container/storage.conf or by using global options. This
+shows how to setup and use fuse-overlayfs for a one time run of busybox
+using global options.
+
+podman --log-level=debug --storage-driver overlay --storage-opt "overlay.mount_program=/usr/bin/fuse-overlayfs" run busybox /bin/sh
+
### Rootless Containers
Podman runs as a non root user on most systems. This feature requires that a new enough version of shadow-utils
diff --git a/libpod/container_internal.go b/libpod/container_internal.go
index 872802016..ac2d65342 100644
--- a/libpod/container_internal.go
+++ b/libpod/container_internal.go
@@ -1429,5 +1429,9 @@ func (c *Container) copyWithTarFromImage(src, dest string) error {
}
a := archive.NewDefaultArchiver()
source := filepath.Join(mountpoint, src)
+
+ if err = c.copyOwnerAndPerms(source, dest); err != nil {
+ return err
+ }
return a.CopyWithTar(source, dest)
}
diff --git a/libpod/container_internal_linux.go b/libpod/container_internal_linux.go
index a7b4aed9f..2a7808bdf 100644
--- a/libpod/container_internal_linux.go
+++ b/libpod/container_internal_linux.go
@@ -982,3 +982,20 @@ func (c *Container) generatePasswd() (string, error) {
}
return passwdFile, nil
}
+
+func (c *Container) copyOwnerAndPerms(source, dest string) error {
+ info, err := os.Stat(source)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil
+ }
+ return errors.Wrapf(err, "cannot stat `%s`", dest)
+ }
+ if err := os.Chmod(dest, info.Mode()); err != nil {
+ return errors.Wrapf(err, "cannot chmod `%s`", dest)
+ }
+ if err := os.Chown(dest, int(info.Sys().(*syscall.Stat_t).Uid), int(info.Sys().(*syscall.Stat_t).Gid)); err != nil {
+ return errors.Wrapf(err, "cannot chown `%s`", dest)
+ }
+ return nil
+}
diff --git a/libpod/container_internal_unsupported.go b/libpod/container_internal_unsupported.go
index 4af0cd56c..f707b350c 100644
--- a/libpod/container_internal_unsupported.go
+++ b/libpod/container_internal_unsupported.go
@@ -35,3 +35,7 @@ func (c *Container) checkpoint(ctx context.Context, options ContainerCheckpointO
func (c *Container) restore(ctx context.Context, options ContainerCheckpointOptions) error {
return ErrNotImplemented
}
+
+func (c *Container) copyOwnerAndPerms(source, dest string) error {
+ return nil
+}
diff --git a/libpod/container_log.go b/libpod/container_log.go
new file mode 100644
index 000000000..7964e4022
--- /dev/null
+++ b/libpod/container_log.go
@@ -0,0 +1,208 @@
+package libpod
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/hpcloud/tail"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+)
+
+const (
+ // logTimeFormat is the time format used in the log.
+ // It is a modified version of RFC3339Nano that guarantees trailing
+ // zeroes are not trimmed, taken from
+ // https://github.com/golang/go/issues/19635
+ logTimeFormat = "2006-01-02T15:04:05.000000000Z07:00"
+)
+
+// LogOptions is the options you can use for logs
+type LogOptions struct {
+ Details bool
+ Follow bool
+ Since time.Time
+ Tail uint64
+ Timestamps bool
+ Multi bool
+ WaitGroup *sync.WaitGroup
+}
+
+// LogLine describes the information for each line of a log
+type LogLine struct {
+ Device string
+ ParseLogType string
+ Time time.Time
+ Msg string
+ CID string
+}
+
+// Log is a runtime function that can read one or more container logs.
+func (r *Runtime) Log(containers []*Container, options *LogOptions, logChannel chan *LogLine) error {
+ for _, ctr := range containers {
+ if err := ctr.ReadLog(options, logChannel); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// ReadLog reads a containers log based on the input options and returns loglines over a channel
+func (c *Container) ReadLog(options *LogOptions, logChannel chan *LogLine) error {
+ t, tailLog, err := getLogFile(c.LogPath(), options)
+ if err != nil {
+ return errors.Wrapf(err, "unable to read log file %s for %s ", c.ID(), c.LogPath())
+ }
+ options.WaitGroup.Add(1)
+ if len(tailLog) > 0 {
+ for _, nll := range tailLog {
+ nll.CID = c.ID()
+ if nll.Since(options.Since) {
+ logChannel <- nll
+ }
+ }
+ }
+
+ go func() {
+ var partial string
+ for line := range t.Lines {
+ nll, err := newLogLine(line.Text)
+ if err != nil {
+ logrus.Error(err)
+ continue
+ }
+ if nll.Partial() {
+ partial = partial + nll.Msg
+ continue
+ } else if !nll.Partial() && len(partial) > 1 {
+ nll.Msg = partial
+ partial = ""
+ }
+ nll.CID = c.ID()
+ if nll.Since(options.Since) {
+ logChannel <- nll
+ }
+ }
+ options.WaitGroup.Done()
+ }()
+ return nil
+}
+
+// getLogFile returns an hp tail for a container given options
+func getLogFile(path string, options *LogOptions) (*tail.Tail, []*LogLine, error) {
+ var (
+ whence int
+ err error
+ logTail []*LogLine
+ )
+ // whence 0=origin, 2=end
+ if options.Tail > 0 {
+ whence = 2
+ logTail, err = getTailLog(path, int(options.Tail))
+ if err != nil {
+ return nil, nil, err
+ }
+ }
+ seek := tail.SeekInfo{
+ Offset: 0,
+ Whence: whence,
+ }
+
+ t, err := tail.TailFile(path, tail.Config{Poll: true, Follow: options.Follow, Location: &seek, Logger: tail.DiscardingLogger})
+ return t, logTail, err
+}
+
+func getTailLog(path string, tail int) ([]*LogLine, error) {
+ var (
+ tailLog []*LogLine
+ nlls []*LogLine
+ tailCounter int
+ partial string
+ )
+ content, err := ioutil.ReadFile(path)
+ if err != nil {
+ return nil, err
+ }
+ splitContent := strings.Split(string(content), "\n")
+ // We read the content in reverse and add each nll until we have the same
+ // number of F type messages as the desired tail
+ for i := len(splitContent) - 1; i >= 0; i-- {
+ if len(splitContent[i]) == 0 {
+ continue
+ }
+ nll, err := newLogLine(splitContent[i])
+ if err != nil {
+ return nil, err
+ }
+ nlls = append(nlls, nll)
+ if !nll.Partial() {
+ tailCounter = tailCounter + 1
+ }
+ if tailCounter == tail {
+ break
+ }
+ }
+ // Now we iterate the results and assemble partial messages to become full messages
+ for _, nll := range nlls {
+ if nll.Partial() {
+ partial = partial + nll.Msg
+ } else {
+ nll.Msg = nll.Msg + partial
+ tailLog = append(tailLog, nll)
+ partial = ""
+ }
+ }
+ return tailLog, nil
+}
+
+// String converts a logline to a string for output given whether a detail
+// bool is specified.
+func (l *LogLine) String(options *LogOptions) string {
+ var out string
+ if options.Multi {
+ cid := l.CID
+ if len(cid) > 12 {
+ cid = cid[:12]
+ }
+ out = fmt.Sprintf("%s ", cid)
+ }
+ if options.Timestamps {
+ out = out + fmt.Sprintf("%s ", l.Time.Format(logTimeFormat))
+ }
+ return out + l.Msg
+}
+
+// Since returns a bool as to whether a log line occurred after a given time
+func (l *LogLine) Since(since time.Time) bool {
+ return l.Time.After(since)
+}
+
+// newLogLine creates a logLine struct from a container log string
+func newLogLine(line string) (*LogLine, error) {
+ splitLine := strings.Split(line, " ")
+ if len(splitLine) < 4 {
+ return nil, errors.Errorf("'%s' is not a valid container log line", line)
+ }
+ logTime, err := time.Parse(time.RFC3339Nano, splitLine[0])
+ if err != nil {
+ return nil, errors.Wrapf(err, "unable to convert time %s from container log", splitLine[0])
+ }
+ l := LogLine{
+ Time: logTime,
+ Device: splitLine[1],
+ ParseLogType: splitLine[2],
+ Msg: strings.Join(splitLine[3:], " "),
+ }
+ return &l, nil
+}
+
+// Partial returns a bool if the log line is a partial log type
+func (l *LogLine) Partial() bool {
+ if l.ParseLogType == "P" {
+ return true
+ }
+ return false
+}
diff --git a/libpod/events.go b/libpod/events.go
index f09529a05..139600982 100644
--- a/libpod/events.go
+++ b/libpod/events.go
@@ -92,5 +92,5 @@ func (r *Runtime) getTail(fromStart, stream bool) (*tail.Tail, error) {
seek.Whence = 0
reopen = false
}
- return tail.TailFile(r.config.EventsLogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek})
+ return tail.TailFile(r.config.EventsLogFilePath, tail.Config{ReOpen: reopen, Follow: stream, Location: &seek, Logger: tail.DiscardingLogger})
}
diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go
index d8b0cffcb..2450bd6b1 100644
--- a/libpod/networking_linux.go
+++ b/libpod/networking_linux.go
@@ -215,9 +215,12 @@ func (r *Runtime) setupRootlessNetNS(ctr *Container) (err error) {
if pid != cmd.Process.Pid {
continue
}
- if status.Exited() || status.Signaled() {
+ if status.Exited() {
return errors.New("slirp4netns failed")
}
+ if status.Signaled() {
+ return errors.New("slirp4netns killed by signal")
+ }
continue
}
return errors.Wrapf(err, "failed to read from slirp4netns sync pipe")
diff --git a/libpod/oci.go b/libpod/oci.go
index 30360d289..69cff6d3c 100644
--- a/libpod/oci.go
+++ b/libpod/oci.go
@@ -183,6 +183,7 @@ func waitPidsStop(pids []int, timeout time.Duration) error {
func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) {
var files []*os.File
+ notifySCTP := false
for _, i := range ports {
switch i.Protocol {
case "udp":
@@ -218,6 +219,12 @@ func bindPorts(ports []ocicni.PortMapping) ([]*os.File, error) {
}
files = append(files, f)
break
+ case "sctp":
+ if !notifySCTP {
+ notifySCTP = true
+ logrus.Warnf("port reservation for SCTP is not supported")
+ }
+ break
default:
return nil, fmt.Errorf("unknown protocol %s", i.Protocol)
diff --git a/libpod/runtime_ctr.go b/libpod/runtime_ctr.go
index c6f119913..3b74a65dd 100644
--- a/libpod/runtime_ctr.go
+++ b/libpod/runtime_ctr.go
@@ -171,7 +171,7 @@ func (r *Runtime) newContainer(ctx context.Context, rSpec *spec.Spec, options ..
}()
if rootless.IsRootless() && ctr.config.ConmonPidFile == "" {
- ctr.config.ConmonPidFile = filepath.Join(ctr.state.RunDir, "conmon.pid")
+ ctr.config.ConmonPidFile = filepath.Join(ctr.config.StaticDir, "conmon.pid")
}
// Go through the volume mounts and check for named volumes
diff --git a/pkg/adapter/containers.go b/pkg/adapter/containers.go
index 756369196..932d209cd 100644
--- a/pkg/adapter/containers.go
+++ b/pkg/adapter/containers.go
@@ -4,7 +4,9 @@ package adapter
import (
"context"
+ "fmt"
"strconv"
+ "sync"
"syscall"
"time"
@@ -127,3 +129,28 @@ func (r *LocalRuntime) WaitOnContainers(ctx context.Context, cli *cliconfig.Wait
}
return ok, failures, err
}
+
+// Log logs one or more containers
+func (r *LocalRuntime) Log(c *cliconfig.LogsValues, options *libpod.LogOptions) error {
+ var wg sync.WaitGroup
+ options.WaitGroup = &wg
+ if len(c.InputArgs) > 1 {
+ options.Multi = true
+ }
+ logChannel := make(chan *libpod.LogLine, int(c.Tail)*len(c.InputArgs)+1)
+ containers, err := shortcuts.GetContainersByContext(false, c.Latest, c.InputArgs, r.Runtime)
+ if err != nil {
+ return err
+ }
+ if err := r.Runtime.Log(containers, options, logChannel); err != nil {
+ return err
+ }
+ go func() {
+ wg.Wait()
+ close(logChannel)
+ }()
+ for line := range logChannel {
+ fmt.Println(line.String(options))
+ }
+ return nil
+}
diff --git a/pkg/adapter/containers_remote.go b/pkg/adapter/containers_remote.go
index 5646d2297..a8146567a 100644
--- a/pkg/adapter/containers_remote.go
+++ b/pkg/adapter/containers_remote.go
@@ -5,18 +5,19 @@ package adapter
import (
"context"
"encoding/json"
- "errors"
+ "fmt"
"strconv"
"syscall"
"time"
"github.com/containers/libpod/cmd/podman/cliconfig"
"github.com/containers/libpod/cmd/podman/shared"
- "github.com/sirupsen/logrus"
-
- iopodman "github.com/containers/libpod/cmd/podman/varlink"
+ "github.com/containers/libpod/cmd/podman/varlink"
"github.com/containers/libpod/libpod"
"github.com/containers/libpod/pkg/inspect"
+ "github.com/pkg/errors"
+ "github.com/sirupsen/logrus"
+ "github.com/varlink/go/varlink"
)
// Inspect returns an inspect struct from varlink
@@ -223,3 +224,41 @@ func BatchContainerOp(ctr *Container, opts shared.PsOptions) (shared.BatchContai
}
return bcs, nil
}
+
+// Logs one or more containers over a varlink connection
+func (r *LocalRuntime) Log(c *cliconfig.LogsValues, options *libpod.LogOptions) error {
+ //GetContainersLogs
+ reply, err := iopodman.GetContainersLogs().Send(r.Conn, uint64(varlink.More), c.InputArgs, c.Follow, c.Latest, options.Since.Format(time.RFC3339Nano), int64(c.Tail), c.Timestamps)
+ if err != nil {
+ return errors.Wrapf(err, "failed to get container logs")
+ }
+ if len(c.InputArgs) > 1 {
+ options.Multi = true
+ }
+ for {
+ log, flags, err := reply()
+ if err != nil {
+ return err
+ }
+ if log.Time == "" && log.Msg == "" {
+ // We got a blank log line which can signal end of stream
+ break
+ }
+ lTime, err := time.Parse(time.RFC3339Nano, log.Time)
+ if err != nil {
+ return errors.Wrapf(err, "unable to parse time of log %s", log.Time)
+ }
+ logLine := libpod.LogLine{
+ Device: log.Device,
+ ParseLogType: log.ParseLogType,
+ Time: lTime,
+ Msg: log.Msg,
+ CID: log.Cid,
+ }
+ fmt.Println(logLine.String(options))
+ if flags&varlink.Continues == 0 {
+ break
+ }
+ }
+ return nil
+}
diff --git a/pkg/varlinkapi/containers.go b/pkg/varlinkapi/containers.go
index fe38a7cdc..3185ba0e9 100644
--- a/pkg/varlinkapi/containers.go
+++ b/pkg/varlinkapi/containers.go
@@ -7,6 +7,7 @@ import (
"io"
"io/ioutil"
"os"
+ "sync"
"syscall"
"time"
@@ -602,3 +603,56 @@ func ContainerStatsToLibpodContainerStats(stats iopodman.ContainerStats) libpod.
}
return cstats
}
+
+// GetContainersLogs is the varlink endpoint to obtain one or more container logs
+func (i *LibpodAPI) GetContainersLogs(call iopodman.VarlinkCall, names []string, follow, latest bool, since string, tail int64, timestamps bool) error {
+ var wg sync.WaitGroup
+ if call.WantsMore() {
+ call.Continues = true
+ }
+ sinceTime, err := time.Parse(time.RFC3339Nano, since)
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ options := libpod.LogOptions{
+ Follow: follow,
+ Since: sinceTime,
+ Tail: uint64(tail),
+ Timestamps: timestamps,
+ }
+
+ options.WaitGroup = &wg
+ if len(names) > 1 {
+ options.Multi = true
+ }
+ logChannel := make(chan *libpod.LogLine, int(tail)*len(names)+1)
+ containers, err := shortcuts.GetContainersByContext(false, latest, names, i.Runtime)
+ if err != nil {
+ return call.ReplyErrorOccurred(err.Error())
+ }
+ if err := i.Runtime.Log(containers, &options, logChannel); err != nil {
+ return err
+ }
+ go func() {
+ wg.Wait()
+ close(logChannel)
+ }()
+ for line := range logChannel {
+ call.ReplyGetContainersLogs(newPodmanLogLine(line))
+ if !call.Continues {
+ break
+ }
+
+ }
+ return call.ReplyGetContainersLogs(iopodman.LogLine{})
+}
+
+func newPodmanLogLine(line *libpod.LogLine) iopodman.LogLine {
+ return iopodman.LogLine{
+ Device: line.Device,
+ ParseLogType: line.ParseLogType,
+ Time: line.Time.Format(time.RFC3339Nano),
+ Msg: line.Msg,
+ Cid: line.CID,
+ }
+}
diff --git a/test/e2e/logs_test.go b/test/e2e/logs_test.go
index b7d959de9..d383a83b3 100644
--- a/test/e2e/logs_test.go
+++ b/test/e2e/logs_test.go
@@ -4,6 +4,7 @@ package integration
import (
"os"
+ "strings"
. "github.com/containers/libpod/test/utils"
. "github.com/onsi/ginkgo"
@@ -34,7 +35,6 @@ var _ = Describe("Podman logs", func() {
})
- //sudo bin/podman run -it --rm fedora-minimal bash -c 'for a in `seq 5`; do echo hello; done'
It("podman logs for container", func() {
logc := podmanTest.Podman([]string{"run", "-dt", ALPINE, "sh", "-c", "echo podman; echo podman; echo podman"})
logc.WaitWithDefaultTimeout()
@@ -106,4 +106,30 @@ var _ = Describe("Podman logs", func() {
Expect(results.ExitCode()).To(Equal(0))
Expect(len(results.OutputToStringArray())).To(Equal(3))
})
+
+ It("podman logs latest and container name should fail", func() {
+ results := podmanTest.Podman([]string{"logs", "-l", "foobar"})
+ results.WaitWithDefaultTimeout()
+ Expect(results.ExitCode()).ToNot(Equal(0))
+ })
+
+ It("podman logs two containers and should display short container IDs", func() {
+ log1 := podmanTest.Podman([]string{"run", "-dt", ALPINE, "sh", "-c", "echo podman; echo podman; echo podman"})
+ log1.WaitWithDefaultTimeout()
+ Expect(log1.ExitCode()).To(Equal(0))
+ cid1 := log1.OutputToString()
+
+ log2 := podmanTest.Podman([]string{"run", "-dt", ALPINE, "sh", "-c", "echo podman; echo podman; echo podman"})
+ log2.WaitWithDefaultTimeout()
+ Expect(log2.ExitCode()).To(Equal(0))
+ cid2 := log2.OutputToString()
+
+ results := podmanTest.Podman([]string{"logs", cid1, cid2})
+ results.WaitWithDefaultTimeout()
+ Expect(results.ExitCode()).To(Equal(0))
+
+ output := results.OutputToStringArray()
+ Expect(len(output)).To(Equal(6))
+ Expect(strings.Contains(output[0], cid1[:12]) || strings.Contains(output[0], cid2[:12])).To(BeTrue())
+ })
})
diff --git a/test/e2e/ps_test.go b/test/e2e/ps_test.go
index 58697acde..92ca538f0 100644
--- a/test/e2e/ps_test.go
+++ b/test/e2e/ps_test.go
@@ -303,4 +303,18 @@ var _ = Describe("Podman ps", func() {
Expect(session.OutputToString()).To(ContainSubstring(podid))
})
+
+ It("podman ps test with port range", func() {
+ session := podmanTest.RunTopContainer("")
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"run", "-dt", "-p", "1000-1006:1000-1006", ALPINE, "top"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.ExitCode()).To(Equal(0))
+
+ session = podmanTest.Podman([]string{"ps", "--format", "{{.Ports}}"})
+ session.WaitWithDefaultTimeout()
+ Expect(session.OutputToString()).To(ContainSubstring("0.0.0.0:1000-1006"))
+ })
})