Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/source/markdown/options/network.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ Valid _mode_ values are:

Any other options will be passed through to netavark without validation. This can be useful to pass arguments to netavark plugins.

For rootless bridge networks, port forwarding uses `rootlessport` by default. Setting `rootless_port_forwarder="pasta"` in the `[network]` section of **[containers.conf(5)](https://github.com/containers/container-libs/blob/main/common/docs/containers.conf.5.md)** switches to pasta's kernel-level forwarding (via `pesto`), which preserves the original client source IP address inside the container. This option is experimental and its behavior is subject to change.

For example, to set a static IPv4 address and a static mac address, use `--network bridge:ip=10.88.0.10,mac=44:33:22:11:00:99`.

- _\<network name or ID\>_**[:OPTIONS,...]**: Connect to a user-defined network; this is the network name or ID from a network created by **[podman network create](podman-network-create.1.md)**. It is possible to specify the same options described under the bridge mode above. Use the **--network** option multiple times to specify additional networks. \
Expand Down
8 changes: 8 additions & 0 deletions docs/source/markdown/options/publish.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,11 @@ Use **podman port** to see the actual mapping: `podman port $CONTAINER $CONTAINE

Port publishing is only supported for containers utilizing their own network namespace
through `bridge` networks, or the `pasta` network mode.

For rootless bridge networks, port forwarding uses `rootlessport` by default, which
is a userspace proxy that does not preserve client source IPs. Setting
`rootless_port_forwarder="pasta"` in the `[network]` section of
**[containers.conf(5)](https://github.com/containers/container-libs/blob/main/common/docs/containers.conf.5.md)**
switches to pasta's kernel-level forwarding via `pesto`, preserving the original
client IP address inside the container. This option is experimental and its
behavior is subject to change.
2 changes: 2 additions & 0 deletions docs/source/markdown/podman-info.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ host:
spec: 1.0.0
+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL
os: linux
rootlessPortForwarder: rootlessport
pasta:
executable: /usr/bin/passt
package: passt-0^20221116.gace074c-1.fc34.x86_64
Expand Down Expand Up @@ -237,6 +238,7 @@ $ podman info --format json
"version": "crun version 1.0\ncommit: 139dc6971e2f1d931af520188763e984d6cdfbf8\nspec: 1.0.0\n+SYSTEMD +SELINUX +APPARMOR +CAP +SECCOMP +EBPF +CRIU +YAJL"
},
"os": "linux",
"rootlessPortForwarder": "rootlessport",
"remoteSocket": {
"path": "/run/user/3267/podman/podman.sock"
},
Expand Down
8 changes: 8 additions & 0 deletions docs/tutorials/basic_networking.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ The user experience of rootless netavark is very akin to a rootful netavark, exc
there is no default network configuration provided. You simply need to create a
network, and the one will be created as a bridge network.

By default, rootless bridge networks use `rootlessport` for port forwarding, which
is a userspace proxy that does not preserve client source IPs. To preserve source
IPs, set `rootless_port_forwarder="pasta"` in the `[network]` section of
`containers.conf`. This uses `pesto` to configure pasta's kernel-level forwarding,
so containers see the real client IP address instead of the bridge gateway.
This option is experimental and its behavior is subject to change.
It requires a version of passt (`>= passt-0^20260507.g1afd4ed`) that ships the `pesto` binary.

```
$ podman network create
```
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ require (
github.com/vishvananda/netlink v1.3.1
go.etcd.io/bbolt v1.4.3
go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b
go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598
go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598
go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598
go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844
go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844
go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844
golang.org/x/crypto v0.51.0
golang.org/x/net v0.54.0
golang.org/x/sync v0.20.0
Expand Down
16 changes: 8 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ github.com/disiqueira/gotree/v3 v3.0.2 h1:ik5iuLQQoufZBNPY518dXhiO5056hyNBIK9lWh
github.com/disiqueira/gotree/v3 v3.0.2/go.mod h1:ZuyjE4+mUQZlbpkI24AmruZKhg3VHEgPLDY8Qk+uUu8=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/cli v29.5.0+incompatible h1:FPUvKJoKpeP4Njz8NrQdeUN8o247P7ndTiILtaP5/l4=
github.com/docker/cli v29.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/cli v29.5.1+incompatible h1:NiufLAJoRcPauFoBNYthfuM4REFwM8H2h9xnLABNHGs=
github.com/docker/cli v29.5.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=
github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker-credential-helpers v0.9.7 h1:jaPIxEIDz5bQeghNAdzz0ETwMMnM4vzjZlxz3pWP4JA=
Expand Down Expand Up @@ -431,12 +431,12 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b h1:i8ntFzITajbJA3ojnA0ZdpbC+I+ccweZvZaGIhQb4i8=
go.podman.io/buildah v1.42.1-0.20260501153811-377cf64e213b/go.mod h1:hPvgsjBU09C+15fKoIZJvKvNaxR+c0QvMg/n4NgBS7A=
go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598 h1:siUEt+Q/qN+1TI4/dem6kaI4x4hnanj0CPKIYLAA5x8=
go.podman.io/common v0.67.2-0.20260515151312-e2c14667a598/go.mod h1:Zw3/xJw/PY5FERQykTNIWFAJzL9ism2LaWFrmvFlBjo=
go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598 h1:nHqODDlrwo33mxXf5qH7b7rjcc2ZVmR8xSTdvAApOrg=
go.podman.io/image/v5 v5.39.3-0.20260515151312-e2c14667a598/go.mod h1:PehFFa+D1wGt+GL5PCm0BupwarWcFFnOpZyQkF0eGdw=
go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598 h1:HtvnEpmKdzedYRDIBFIrVJummv3WnflKYEhK+3twZs0=
go.podman.io/storage v1.62.1-0.20260515151312-e2c14667a598/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs=
go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844 h1:Un2Wz6Ni/QmkVC528gXOVvVKAM/vOi/c7kLK8gWvmvI=
go.podman.io/common v0.67.2-0.20260519201413-7e9ee2072844/go.mod h1:bhfqGXJ/cMC6CYubcmENInEQlemgClO0ea9+e1Mz/8k=
go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844 h1:wbh4wP38Ba13JjvHtNS2RZrI+L2/n2pIhQY36dUaebw=
go.podman.io/image/v5 v5.39.3-0.20260519201413-7e9ee2072844/go.mod h1:Jg91tpyyYgFAQLG1YjWbaZs4NHRzLWpDgCZyiOvDZyY=
go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844 h1:s3bBIQVRqZ4uusgEMaVmUfXXphj4tEsg7BwiQB4COkQ=
go.podman.io/storage v1.63.1-0.20260519201413-7e9ee2072844/go.mod h1:z4Z9K+7GhKjWL/Y1O17+4f8a1KGijVeC9hr3tymhSOs=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
Expand Down
7 changes: 5 additions & 2 deletions libpod/define/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,11 @@ type HostInfo struct {
// RemoteSocket returns the UNIX domain socket the Podman service is listening on
RemoteSocket *RemoteSocket `json:"remoteSocket,omitempty"`
// RootlessNetworkCmd returns the default rootless network command (pasta)
RootlessNetworkCmd string `json:"rootlessNetworkCmd"`
RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"`
RootlessNetworkCmd string `json:"rootlessNetworkCmd"`
// RootlessPortForwarder returns the port forwarding mechanism for rootless
// bridge networks: "rootlessport" (default) or "pasta" (experimental)
RootlessPortForwarder string `json:"rootlessPortForwarder"`
RuntimeInfo map[string]any `json:"runtimeInfo,omitempty"`
// ServiceIsRemote is true when the podman/libpod service is remote to the client
ServiceIsRemote bool `json:"serviceIsRemote"`
Security SecurityInfo `json:"security"`
Expand Down
41 changes: 21 additions & 20 deletions libpod/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,27 @@ func (r *Runtime) hostInfo() (*define.HostInfo, error) {
}

info := define.HostInfo{
Arch: runtime.GOARCH,
BuildahVersion: buildah.Version,
DatabaseBackend: r.state.Name(),
Linkmode: linkmode.Linkmode(),
CPUs: runtime.NumCPU(),
CPUUtilization: cpuUtil,
Distribution: hostDistributionInfo,
LogDriver: r.config.Containers.LogDriver,
EventLogger: r.eventer.String(),
FreeLocks: locksFree,
Hostname: host,
Kernel: kv,
MemFree: mi.MemFree,
MemTotal: mi.MemTotal,
NetworkBackend: r.config.Network.NetworkBackend,
NetworkBackendInfo: r.network.NetworkInfo(),
OS: runtime.GOOS,
RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd,
SwapFree: mi.SwapFree,
SwapTotal: mi.SwapTotal,
Arch: runtime.GOARCH,
BuildahVersion: buildah.Version,
DatabaseBackend: r.state.Name(),
Linkmode: linkmode.Linkmode(),
CPUs: runtime.NumCPU(),
CPUUtilization: cpuUtil,
Distribution: hostDistributionInfo,
LogDriver: r.config.Containers.LogDriver,
EventLogger: r.eventer.String(),
FreeLocks: locksFree,
Hostname: host,
Kernel: kv,
MemFree: mi.MemFree,
MemTotal: mi.MemTotal,
NetworkBackend: r.config.Network.NetworkBackend,
NetworkBackendInfo: r.network.NetworkInfo(),
OS: runtime.GOOS,
RootlessNetworkCmd: r.config.Network.DefaultRootlessNetworkCmd,
RootlessPortForwarder: r.config.Network.RootlessPortForwarder,
SwapFree: mi.SwapFree,
SwapTotal: mi.SwapTotal,
}
platform := parse.DefaultPlatform()
pArr := strings.Split(platform, "/")
Expand Down
28 changes: 22 additions & 6 deletions libpod/networking_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,11 +110,27 @@ func (r *Runtime) teardownNetwork(ctr *Container) error {
return err
}

if !ctr.config.NetMode.IsPasta() && len(networks) > 0 {
netOpts := ctr.getNetworkOptions(networks)
return r.teardownNetworkBackend(ctr.state.NetNS, netOpts)
if len(networks) == 0 {
return nil
}
return nil

// --net=pasta: per-container pasta cleans up when it exits, nothing to tear down.
if ctr.config.NetMode.IsPasta() {
return nil
}

// Pasta forwarding mode: remove port forwarding rules (via pesto) before
// netavark tears down bridge/nftables so pasta stops forwarding first.
// Rootlessport mode: no explicit teardown needed (exits with conmon).
if rootless.IsRootless() && ctr.config.NetMode.IsBridge() && len(ctr.config.PortMappings) > 0 &&
r.config.Network.RootlessPortForwarder == config.RootlessPortForwarderPasta {
if err := r.teardownRootlessPortMappingViaPesto(ctr); err != nil {
logrus.Warnf("pesto port cleanup failed for container %s: %v", ctr.ID(), err)
}
}

netOpts := ctr.getNetworkOptions(networks)
return r.teardownNetworkBackend(ctr.state.NetNS, netOpts)
}

// isBridgeNetMode checks if the given network mode is bridge.
Expand Down Expand Up @@ -439,7 +455,7 @@ func (c *Container) NetworkDisconnect(nameOrID, netName string, _ bool) error {

// Reload ports when there are still connected networks, maybe we removed the network interface with the child ip.
// Reloading without connected networks does not make sense, so we can skip this step.
if rootless.IsRootless() && len(networkStatus) > 0 {
if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networkStatus) > 0 {
if err := c.reloadRootlessRLKPortMapping(); err != nil {
return err
}
Expand Down Expand Up @@ -595,7 +611,7 @@ func (c *Container) NetworkConnect(nameOrID, netName string, netOpts types.PerNe

// The first network needs a port reload to set the correct child ip for the rootlessport process.
// Adding a second network does not require a port reload because the child ip is still valid.
if rootless.IsRootless() && len(networks) == 0 {
if rootless.IsRootless() && c.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport && len(networks) == 0 {
if err := c.reloadRootlessRLKPortMapping(); err != nil {
return err
}
Expand Down
4 changes: 4 additions & 0 deletions libpod/networking_freebsd.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,3 +224,7 @@ func (c *Container) inspectJoinedNetworkNS(_ string) (q types.StatusBlock, retEr
func (c *Container) reloadRootlessRLKPortMapping() error {
return errors.New("unsupported (*Container).reloadRootlessRLKPortMapping")
}

func (r *Runtime) teardownRootlessPortMappingViaPesto(_ *Container) error {
return errors.New("unsupported teardownRootlessPortMappingViaPesto on FreeBSD")
}
23 changes: 14 additions & 9 deletions libpod/networking_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/sirupsen/logrus"
"github.com/vishvananda/netlink"
"go.podman.io/common/libnetwork/types"
"go.podman.io/common/pkg/config"
"go.podman.io/common/pkg/netns"
"go.podman.io/podman/v6/libpod/define"
"go.podman.io/podman/v6/pkg/rootless"
Expand Down Expand Up @@ -59,15 +60,19 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS string) (status map[strin
}
}()

// set up rootless port forwarder when rootless with ports and the network status is empty,
// if this is called from network reload the network status will not be empty and we should
// not set up port because they are still active
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 && ctr.getNetworkStatus() == nil {
// set up port forwarder for rootless netns
// make sure to fix this in container.handleRestartPolicy() as well
// Important we have to call this after r.setUpNetwork() so that
// we can use the proper netStatus
err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus)
// Set up port forwarding for rootless bridge networks.
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 {
switch r.config.Network.RootlessPortForwarder {
case config.RootlessPortForwarderPasta:
err = r.setupRootlessPortMappingViaPesto(ctr)
case config.RootlessPortForwarderRootlessport, "":
if ctr.getNetworkStatus() == nil {
err = r.setupRootlessPortMappingViaRLK(ctr, ctrNS, netStatus)
}
default:
err = fmt.Errorf("invalid rootless_port_forwarder value %q, must be %q or %q",
r.config.Network.RootlessPortForwarder, config.RootlessPortForwarderRootlessport, config.RootlessPortForwarderPasta)
}
}
return netStatus, err
}
Expand Down
52 changes: 52 additions & 0 deletions libpod/networking_pesto_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//go:build !remote

// Pesto integration for rootless bridge network port forwarding.
//
// A shared pasta instance in the rootless netns (-c pasta.sock) handles
// host-side port forwarding. On container start/stop, pesto incrementally
// adds or deletes port forwarding rules for that container. Pasta forwards
// via kernel splice (localhost) or TAP (external), preserving source IPs.
// The container sees the real client's address instead of a proxy or bridge
// gateway address.
//
// Container start:
// - netavark sets up bridge + DNAT
// - pesto --add: adds this container's ports to pasta
//
// Container stop:
// - pesto --delete: removes this container's ports from pasta
// - netavark tears down bridge/DNAT

package libpod

import (
"go.podman.io/common/libnetwork/pasta"
)

func (r *Runtime) pestoSocketPath() string {
info, err := r.network.RootlessNetnsInfo()
if err != nil || info == nil {
return ""
}
return info.PestoSocketPath
}

// setupRootlessPortMappingViaPesto adds this container's port forwarding
// rules to the shared pasta instance.
func (r *Runtime) setupRootlessPortMappingViaPesto(ctr *Container) error {
ports := ctr.convertPortMappings()
if len(ports) == 0 {
return nil
}
return pasta.PestoAddPorts(r.config, r.pestoSocketPath(), ports)
}

// teardownRootlessPortMappingViaPesto removes this container's port
// forwarding rules from the shared pasta instance.
func (r *Runtime) teardownRootlessPortMappingViaPesto(ctr *Container) error {
ports := ctr.convertPortMappings()
if len(ports) == 0 {
return nil
}
return pasta.PestoDeletePorts(r.config, r.pestoSocketPath(), ports)
}
7 changes: 5 additions & 2 deletions libpod/oci_conmon_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -1188,8 +1188,11 @@ func (r *ConmonOCIRuntime) createOCIContainer(ctr *Container, restoreOptions *Co
// process cannot use them.
cmd.ExtraFiles = append(cmd.ExtraFiles, ports...)

// For rootless port forwarding, create sync pipe and leak write end to conmon
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 {
// For rootless port forwarding via rootlessport, create sync pipe and
// leak write end to conmon. Pasta forwarding mode does not use
// rootlessport, so no pipe is needed.
if rootless.IsRootless() && len(ctr.config.PortMappings) > 0 &&
ctr.runtime.config.Network.RootlessPortForwarder == config.RootlessPortForwarderRootlessport {
ctr.rootlessPortSyncR, ctr.rootlessPortSyncW, err = os.Pipe()
if err != nil {
return 0, fmt.Errorf("failed to create rootless port sync pipe: %w", err)
Expand Down
1 change: 1 addition & 0 deletions rootless.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The following list categorizes the known issues and irregularities with running
* A proxy server, kernel firewall rule, or redirection tool such as [redir](https://github.com/troglobit/redir) may be used to redirect traffic from a privileged port to an unprivileged one (where a podman pod is bound) in a server scenario - where a user has access to the root account (or setuid on the binary would be an acceptable risk), but wants to run the containers as an unprivileged user for enhanced security and for a limited number of pre-known ports.
* As of Podman 5.0, pasta is the default networking tool. Since pasta copies the IP address of the main interface, connections to that IP from containers do not work. This means that unless you have more than one interface, inter-container connections cannot be made without explicitly passing a pasta network configuration, either in `containers.conf` or at runtime.
* If you previously had port forwards (ex. via `-p 80:80`) that other containers could access, you can use the solution (setting pasta options with `10.0.2.x` IPs) posted [here](https://blog.podman.io/2024/03/podman-5-0-breaking-changes-in-detail/).
* Rootless bridge networks use `rootlessport` for port forwarding by default, which is a userspace proxy that does not preserve client source IPs. Setting `rootless_port_forwarder="pasta"` in the `[network]` section of `containers.conf` switches to pasta's kernel-level forwarding (via `pesto`), preserving the original client IP. This option is experimental and its behavior is subject to change. It requires a version of passt (`>= passt-0^20260507.g1afd4ed`) that ships the `pesto` binary.
* If /etc/subuid and /etc/subgid are not set up for a user, then podman commands
can easily fail
* Some identity providers (e.g. FreeIPA) have integrated subuid/subgid support, but many have not.
Expand Down
29 changes: 29 additions & 0 deletions test/e2e/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1629,6 +1629,35 @@ func testPortConnection(port int) {
Expect(err).ToNot(HaveOccurred())
}

// startNCContainer starts a detached container running nc (netcat) listening
// on the given port, waits for it to be ready, and returns the container name.
//
//nolint:unused,nolintlint // only called from linux-only test files, unused on freebsd
func (p *PodmanTestIntegration) startNCContainer(name string, listenPort int, extraArgs ...string) string {
GinkgoHelper()
portStr := strconv.Itoa(listenPort)
args := append([]string{"run", "-d", "--name", name}, extraArgs...)
args = append(args, ALPINE, "sh", "-c", "nc -l -n -v -p "+portStr+" 2>&1")
p.PodmanExitCleanly(args...)
p.WaitForContainerLog(name, "listening")
return name
}

// WaitForContainerLog polls container logs until the given substring appears
// in either stdout or stderr. Fails the test if not found within the timeout.
func (p *PodmanTestIntegration) WaitForContainerLog(ctrName string, substr string) {
GinkgoHelper()
for range 10 {
logs := p.Podman([]string{"logs", ctrName})
logs.WaitWithDefaultTimeout()
if strings.Contains(logs.ErrorToString(), substr) || strings.Contains(logs.OutputToString(), substr) {
return
}
time.Sleep(500 * time.Millisecond)
}
Fail(fmt.Sprintf("timed out waiting for %q in logs of container %s", substr, ctrName))
}

func createNetworkName(name string) string {
return name + stringid.GenerateRandomID()[:10]
}
Expand Down
Loading