Extended config for wait strategies (#42)

This commit is contained in:
Tim Zöller 2022-03-18 14:03:42 +01:00
parent 6e94e86745
commit 71af11f503
2 changed files with 231 additions and 155 deletions

View file

@ -1,24 +1,25 @@
(ns clj-test-containers.core (ns clj-test-containers.core
(:require (:require
[clj-test-containers.spec.core :as cs] [clj-test-containers.spec.core :as cs]
[clojure.spec.alpha :as s] [clojure.spec.alpha :as s]
[clojure.string]) [clojure.string])
(:import (:import
(java.nio.file (java.nio.file
Paths) Paths)
(org.testcontainers.containers (org.testcontainers.containers
BindMode BindMode
GenericContainer GenericContainer
Network) Network)
(org.testcontainers.containers.output (org.testcontainers.containers.output
ToStringConsumer) ToStringConsumer)
(org.testcontainers.containers.wait.strategy (org.testcontainers.containers.wait.strategy
Wait) Wait)
(org.testcontainers.images.builder (org.testcontainers.images.builder
ImageFromDockerfile) ImageFromDockerfile)
(org.testcontainers.utility (org.testcontainers.utility
MountableFile MountableFile
ResourceReaper))) ResourceReaper)
(java.time Duration)))
(defn- resolve-bind-mode (defn- resolve-bind-mode
(^BindMode [bind-mode] (^BindMode [bind-mode]
@ -31,66 +32,72 @@
(ResourceReaper/instance)) (ResourceReaper/instance))
(defmulti wait (defmulti wait
"Sets a wait strategy to the container. Supports :http, :health and :log as "Sets a wait strategy to the container. Supports :http, :health and :log as
strategies. strategies.
## HTTP Strategy ## HTTP Strategy
The :http strategy will only accept the container as initialized if it can be The :http strategy will only accept the container as initialized if it can be
accessed via HTTP. It accepts a path, a port, a vector of status codes, a accessed via HTTP. It accepts a path, a port, a vector of status codes, a
boolean that specifies if TLS is enabled, a read timeout in seconds and a map boolean that specifies if TLS is enabled, a read timeout in seconds and a map
with basic credentials, containing username and password. Only the path is with basic credentials, containing username and password. Only the path is
required, all others are optional. required, all others are optional.
Example: Example:
```clojure ```clojure
(wait {:wait-strategy :http (wait {:wait-strategy :http
:port 80 :port 80
:path \"/\" :path \"/\"
:status-codes [200 201] :status-codes [200 201]
:tls true :tls true
:read-timeout 5 :read-timeout 5
:basic-credentials {:username \"user\" :basic-credentials {:username \"user\"
:password \"password\"}} :password \"password\"}}
container) container)
``` ```
## Health Strategy ## Health Strategy
The :health strategy only accepts a true or false value. This enables support The :health strategy only accepts a true or false value. This enables support
for Docker's healthcheck feature, whereby you can directly leverage the for Docker's healthcheck feature, whereby you can directly leverage the
healthy state of your container as your wait condition. healthy state of your container as your wait condition.
Example: Example:
```clojure ```clojure
(wait {:wait-strategy :health :true} container) (wait {:wait-strategy :health :true} container)
``` ```
## Log Strategy ## Log Strategy
The :log strategy accepts a message which simply causes the output of your The :log strategy accepts a message which simply causes the output of your
container's log to be used in determining if the container is ready or not. container's log to be used in determining if the container is ready or not.
The output is `grepped` against the log message. The output is `grepped` against the log message.
Example: Example:
```clojure ```clojure
(wait {:wait-strategy :log (wait {:wait-strategy :log
:message \"accept connections\"} container) :message \"accept connections\"} container)
```" ```"
:wait-strategy) :wait-strategy)
(defmethod wait :http (defmethod wait :http
[{:keys [path [{:keys [path
port port
method
status-codes status-codes
tls tls
read-timeout read-timeout
basic-credentials] :as options} basic-credentials
headers
startup-timeout] :as options}
^GenericContainer container] ^GenericContainer container]
(let [for-http (Wait/forHttp path)] (let [for-http (Wait/forHttp path)]
(when port (when port
(.forPort for-http port)) (.forPort for-http port))
(when method
(.withMethod for-http method))
(doseq [status-code status-codes] (doseq [status-code status-codes]
(.forStatusCode for-http status-code)) (.forStatusCode for-http status-code))
@ -98,27 +105,53 @@
(.usingTls for-http)) (.usingTls for-http))
(when read-timeout (when read-timeout
(.withReadTimeout for-http (java.time.Duration/ofSeconds read-timeout))) (.withReadTimeout for-http (Duration/ofSeconds read-timeout)))
(when basic-credentials (when basic-credentials
(let [{username :username password :password} basic-credentials] (let [{:keys [username password]} basic-credentials]
(.withBasicCredentials for-http username password))) (.withBasicCredentials for-http username password)))
(when headers
(.withHeaders for-http headers))
(when startup-timeout
(.withStartupTimeout for-http (Duration/ofSeconds startup-timeout)))
(.waitingFor container for-http) (.waitingFor container for-http)
{:wait-for-http (dissoc options :strategy)})) {:wait-for-http (dissoc options :strategy)}))
(defmethod wait :health (defmethod wait :health
[_ ^GenericContainer container] [{:keys [startup-timeout]} ^GenericContainer container]
(.waitingFor container (Wait/forHealthcheck)) (let [strategy (Wait/forHealthcheck)]
(when startup-timeout
(.withStartupTimeout strategy (Duration/ofSeconds startup-timeout)))
(.waitingFor container strategy))
{:wait-for-healthcheck true}) {:wait-for-healthcheck true})
(defmethod wait :log (defmethod wait :log
[{:keys [message]} ^GenericContainer container] [{:keys [message startup-timeout]} ^GenericContainer container]
(let [log-message (str ".*" message ".*\\n")] (let [log-message (str ".*" message ".*\\n")
(.waitingFor container (Wait/forLogMessage log-message 1)) strategy (Wait/forLogMessage log-message 1)]
(when startup-timeout
(.withStartupTimeout strategy (Duration/ofSeconds startup-timeout)))
(.waitingFor container strategy)
{:wait-for-log-message log-message})) {:wait-for-log-message log-message}))
(defmethod wait :port
[{:keys [startup-timeout]} ^GenericContainer container]
(let [strategy (Wait/forListeningPort)]
(when startup-timeout
(.withStartupTimeout strategy (Duration/ofSeconds startup-timeout)))
(.waitingFor container strategy))
{:wait-for-port true})
(defmethod wait :default [_ _] nil) (defmethod wait :default [_ _] nil)
(s/fdef init (s/fdef init
@ -149,11 +182,11 @@
(when network-aliases (when network-aliases
(.setNetworkAliases container network-aliases)) (.setNetworkAliases container network-aliases))
(merge init-options {:container container (merge init-options {:container container
:exposed-ports (vec (.getExposedPorts container)) :exposed-ports (vec (.getExposedPorts container))
:env-vars (into {} (.getEnvMap container)) :env-vars (into {} (.getEnvMap container))
:host (.getHost container) :host (.getHost container)
:network network} (wait wait-for container))) :network network} (wait wait-for container)))
(s/fdef create (s/fdef create
:args (s/cat :create-options ::cs/create-options) :args (s/cat :create-options ::cs/create-options)
@ -181,11 +214,11 @@
[{:keys [^GenericContainer container] :as container-config} [{:keys [^GenericContainer container] :as container-config}
{:keys [^String resource-path ^String container-path mode]}] {:keys [^String resource-path ^String container-path mode]}]
(assoc container-config (assoc container-config
:container :container
(.withClasspathResourceMapping container (.withClasspathResourceMapping container
resource-path resource-path
container-path container-path
(resolve-bind-mode mode)))) (resolve-bind-mode mode))))
(defn bind-filesystem! (defn bind-filesystem!
"Binds a source from the filesystem to the given container path. Should be "Binds a source from the filesystem to the given container path. Should be
@ -193,11 +226,11 @@
[{:keys [^GenericContainer container] :as container-config} [{:keys [^GenericContainer container] :as container-config}
{:keys [^String host-path ^String container-path mode]}] {:keys [^String host-path ^String container-path mode]}]
(assoc container-config (assoc container-config
:container :container
(.withFileSystemBind container (.withFileSystemBind container
host-path host-path
container-path container-path
(resolve-bind-mode mode)))) (resolve-bind-mode mode))))
(defn copy-file-to-container! (defn copy-file-to-container!
"If a container is not yet started, adds a mapping from mountable file to "If a container is not yet started, adds a mapping from mountable file to
@ -208,16 +241,16 @@
(let [^MountableFile mountable-file (let [^MountableFile mountable-file
(case type (case type
:classpath-resource (MountableFile/forClasspathResource path) :classpath-resource (MountableFile/forClasspathResource path)
:host-path (MountableFile/forHostPath path))] :host-path (MountableFile/forHostPath path))]
(if id (if id
(do (do
(.copyFileToContainer container mountable-file container-path) (.copyFileToContainer container mountable-file container-path)
container-config) container-config)
(assoc container-config (assoc container-config
:container :container
(.withCopyFileToContainer container (.withCopyFileToContainer container
mountable-file mountable-file
container-path))))) container-path)))))
(defn execute-command! (defn execute-command!
"Executes a command in the container, and returns the result" "Executes a command in the container, and returns the result"
@ -228,28 +261,28 @@
:stderr (.getStderr result)})) :stderr (.getStderr result)}))
(defmulti log (defmulti log
"Sets a log strategy on the container as a means of accessing the container "Sets a log strategy on the container as a means of accessing the container
logs. It currently only supports a :string as the strategy to use. logs. It currently only supports a :string as the strategy to use.
## String Strategy ## String Strategy
The :string strategy sets up a function in the returned map, under the The :string strategy sets up a function in the returned map, under the
`string-log` key. This function enables the dumping of the logs when passed to `string-log` key. This function enables the dumping of the logs when passed to
the `dump-logs` function. the `dump-logs` function.
Example: Example:
```clojure ```clojure
{:log-strategy :string} {:log-strategy :string}
``` ```
Then, later in your program, you can access the logs thus: Then, later in your program, you can access the logs thus:
```clojure ```clojure
(def container-config (tc/start! container)) (def container-config (tc/start! container))
(tc/dump-logs container-config) (tc/dump-logs container-config)
``` ```
" "
:log-strategy) :log-strategy)
(defmethod log :string (defmethod log :string
[_ ^GenericContainer container] [_ ^GenericContainer container]
@ -259,9 +292,9 @@
(-> (.toUtf8String to-string-consumer) (-> (.toUtf8String to-string-consumer)
(clojure.string/replace #"\n+" "\n")))})) (clojure.string/replace #"\n+" "\n")))}))
(defmethod log :slf4j [_ _] nil) ;; Not yet implemented (defmethod log :slf4j [_ _] nil) ;; Not yet implemented
(defmethod log :default [_ _] nil) ;; Not yet implemented (defmethod log :default [_ _] nil) ;; Not yet implemented
(defn dump-logs (defn dump-logs
"Dumps the logs found by invoking the function on the :string-log key" "Dumps the logs found by invoking the function on the :string-log key"
@ -286,9 +319,9 @@
container-id container-id
image-name) image-name)
(-> container-config (-> container-config
(merge {:id container-id (merge {:id container-id
:mapped-ports mapped-ports :mapped-ports mapped-ports
:image-name image-name} logger) :image-name image-name} logger)
(dissoc :log-to)))) (dissoc :log-to))))
(defn stop! (defn stop!
@ -320,9 +353,9 @@
network-name (.getName network)] network-name (.getName network)]
(.registerNetworkIdForCleanup ^ResourceReaper (reaper-instance) network-name) (.registerNetworkIdForCleanup ^ResourceReaper (reaper-instance) network-name)
{:network network {:network network
:name network-name :name network-name
:ipv6 (.getEnableIpv6 network) :ipv6 (.getEnableIpv6 network)
:driver (.getDriver network)})))) :driver (.getDriver network)}))))
(def ^:deprecated init-network create-network) (def ^:deprecated init-network create-network)

View file

@ -1,17 +1,17 @@
(ns clj-test-containers.core-test (ns clj-test-containers.core-test
(:require (:require
[clj-test-containers.core :as sut] [clj-test-containers.core :as sut]
[clojure.string :refer [includes?]] [clojure.string :refer [includes?]]
[clojure.test :refer [deftest is testing]]) [clojure.test :refer [deftest is testing]])
(:import (:import
(org.testcontainers.containers (org.testcontainers.containers
PostgreSQLContainer))) PostgreSQLContainer)))
(deftest create-test (deftest create-test
(testing "Testing basic testcontainer generic image initialisation" (testing "Testing basic testcontainer generic image initialisation"
(let [container (sut/create {:image-name "postgres:12.2" (let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
initialized-container (sut/start! container) initialized-container (sut/start! container)
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
(is (some? (:id initialized-container))) (is (some? (:id initialized-container)))
@ -22,16 +22,18 @@
(testing "Testing log access to the container" (testing "Testing log access to the container"
(let [container (sut/init {:container (PostgreSQLContainer. "postgres:12.2") (let [container (sut/init {:container (PostgreSQLContainer. "postgres:12.2")
:log-to {:log-strategy :string}}) :log-to {:log-strategy :string}})
initialized-container (sut/start! container)] initialized-container (sut/start! container)]
(Thread/sleep 500) (Thread/sleep 500)
(is (includes? (sut/dump-logs initialized-container) "database system is ready to accept connections")))) (is (includes? (sut/dump-logs initialized-container) "database system is ready to accept connections"))))
(testing "Testing basic testcontainer generic image initialisation with wait for log message" (testing "Testing basic testcontainer generic image initialisation with wait for log message"
(let [container (sut/create {:image-name "postgres:12.2" (let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"} :env-vars {"POSTGRES_PASSWORD" "pw"}
:wait-for {:wait-strategy :log :message "accept connections"}}) :wait-for {:wait-strategy :log
:message "accept connections"
:startup-timeout 10}})
initialized-container (sut/start! container) initialized-container (sut/start! container)
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
(is (some? (:id initialized-container))) (is (some? (:id initialized-container)))
@ -41,9 +43,50 @@
(is (nil? (:id stopped-container))) (is (nil? (:id stopped-container)))
(is (nil? (:mapped-ports stopped-container))))) (is (nil? (:mapped-ports stopped-container)))))
(testing "Testing basic testcontainer generic image initialisation with wait for host port"
(let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}
:wait-for {:wait-strategy :port}})
initialized-container (sut/start! container)
stopped-container (sut/stop! container)]
(is (some? (:id initialized-container)))
(is (some? (:mapped-ports initialized-container)))
(is (some? (get (:mapped-ports initialized-container) 5432)))
(is (= (:wait-for-port initialized-container) true))
(is (nil? (:id stopped-container)))
(is (nil? (:mapped-ports stopped-container)))))
(testing "Testing basic testcontainer generic image initialisation with wait for http"
(let [container (sut/create {:image-name "alpine:3.5"
:network-aliases ["foo"]
:command ["/bin/sh"
"-c"
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"]
:exposed-ports [8080]
:wait-for {:wait-strategy :http
:path "/"
:port 8080
:method "GET"
:status-codes [200]
:headers {"Accept" "text/plain"}}})
initialized-container (sut/start! container)
stopped-container (sut/stop! container)]
(is (some? (:id initialized-container)))
(is (some? (:mapped-ports initialized-container)))
(is (some? (get (:mapped-ports initialized-container) 8080)))
(is (= (:wait-for-http initialized-container) {:headers {"Accept" "text/plain"}
:method "GET"
:path "/"
:port 8080
:status-codes [200]
:wait-strategy :http}))
(is (nil? (:id stopped-container)))
(is (nil? (:mapped-ports stopped-container)))))
(testing "Testing basic testcontainer image creation from docker file" (testing "Testing basic testcontainer image creation from docker file"
(let [container (sut/create-from-docker-file {:exposed-ports [80] (let [container (sut/create-from-docker-file {:exposed-ports [80]
:docker-file "test/resources/Dockerfile"}) :docker-file "test/resources/Dockerfile"})
initialized-container (sut/start! container) initialized-container (sut/start! container)
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
(is (some? (:id initialized-container))) (is (some? (:id initialized-container)))
@ -64,9 +107,9 @@
(deftest execute-command-in-container (deftest execute-command-in-container
(testing "Executing a command in the running Docker container" (testing "Executing a command in the running Docker container"
(let [container (sut/create {:image-name "postgres:12.2" (let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
initialized-container (sut/start! container) initialized-container (sut/start! container)
result (sut/execute-command! initialized-container ["whoami"]) result (sut/execute-command! initialized-container ["whoami"])
_stopped-container (sut/stop! container)] _stopped-container (sut/stop! container)]
@ -76,12 +119,12 @@
(deftest init-volume-test (deftest init-volume-test
(testing "Testing mapping of a classpath resource" (testing "Testing mapping of a classpath resource"
(let [container (-> (sut/create {:image-name "postgres:12.2" (let [container (-> (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
(sut/map-classpath-resource! {:resource-path "test.sql" (sut/map-classpath-resource! {:resource-path "test.sql"
:container-path "/opt/test.sql" :container-path "/opt/test.sql"
:mode :read-only})) :mode :read-only}))
initialized-container (sut/start! container) initialized-container (sut/start! container)
file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"]) file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -93,12 +136,12 @@
(is (nil? (:mapped-ports stopped-container))))) (is (nil? (:mapped-ports stopped-container)))))
(testing "Testing mapping of a filesystem-binding" (testing "Testing mapping of a filesystem-binding"
(let [container (-> (sut/create {:image-name "postgres:12.2" (let [container (-> (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
(sut/bind-filesystem! {:host-path "." (sut/bind-filesystem! {:host-path "."
:container-path "/opt" :container-path "/opt"
:mode :read-only})) :mode :read-only}))
initialized-container (sut/start! container) initialized-container (sut/start! container)
file-check (sut/execute-command! initialized-container ["tail" "/opt/README.md"]) file-check (sut/execute-command! initialized-container ["tail" "/opt/README.md"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -110,12 +153,12 @@
(is (nil? (:mapped-ports stopped-container))))) (is (nil? (:mapped-ports stopped-container)))))
(testing "Copying a file from the host into the container" (testing "Copying a file from the host into the container"
(let [container (-> (sut/create {:image-name "postgres:12.2" (let [container (-> (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
(sut/copy-file-to-container! {:path "test.sql" (sut/copy-file-to-container! {:path "test.sql"
:container-path "/opt/test.sql" :container-path "/opt/test.sql"
:type :host-path})) :type :host-path}))
initialized-container (sut/start! container) initialized-container (sut/start! container)
file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"]) file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -128,12 +171,12 @@
(testing "Copying a file from the classpath into the container" (testing "Copying a file from the classpath into the container"
(let [container (-> (sut/create {:image-name "postgres:12.2" (let [container (-> (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
(sut/copy-file-to-container! {:path "test.sql" (sut/copy-file-to-container! {:path "test.sql"
:container-path "/opt/test.sql" :container-path "/opt/test.sql"
:type :classpath-resource})) :type :classpath-resource}))
initialized-container (sut/start! container) initialized-container (sut/start! container)
file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"]) file-check (sut/execute-command! initialized-container ["tail" "/opt/test.sql"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -145,14 +188,14 @@
(is (nil? (:mapped-ports stopped-container))))) (is (nil? (:mapped-ports stopped-container)))))
(testing "Copying a file from the host into a running container" (testing "Copying a file from the host into a running container"
(let [container (sut/create {:image-name "postgres:12.2" (let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
initialized-container (sut/start! container) initialized-container (sut/start! container)
_ (sut/copy-file-to-container! initialized-container _ (sut/copy-file-to-container! initialized-container
{:path "test.sql" {:path "test.sql"
:container-path "/opt/test.sql" :container-path "/opt/test.sql"
:type :host-path}) :type :host-path})
file-check (sut/execute-command! initialized-container file-check (sut/execute-command! initialized-container
["tail" "/opt/test.sql"]) ["tail" "/opt/test.sql"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -164,14 +207,14 @@
(is (nil? (:mapped-ports stopped-container))))) (is (nil? (:mapped-ports stopped-container)))))
(testing "Copying a file from the classpath into a running container" (testing "Copying a file from the classpath into a running container"
(let [container (sut/create {:image-name "postgres:12.2" (let [container (sut/create {:image-name "postgres:12.2"
:exposed-ports [5432] :exposed-ports [5432]
:env-vars {"POSTGRES_PASSWORD" "pw"}}) :env-vars {"POSTGRES_PASSWORD" "pw"}})
initialized-container (sut/start! container) initialized-container (sut/start! container)
_ (sut/copy-file-to-container! initialized-container _ (sut/copy-file-to-container! initialized-container
{:path "test.sql" {:path "test.sql"
:container-path "/opt/test.sql" :container-path "/opt/test.sql"
:type :classpath-resource}) :type :classpath-resource})
file-check (sut/execute-command! initialized-container file-check (sut/execute-command! initialized-container
["tail" "/opt/test.sql"]) ["tail" "/opt/test.sql"])
stopped-container (sut/stop! container)] stopped-container (sut/stop! container)]
@ -185,15 +228,15 @@
(deftest networking-test (deftest networking-test
(testing "Putting two containers into the same network and check their communication" (testing "Putting two containers into the same network and check their communication"
(let [network (sut/create-network) (let [network (sut/create-network)
server-container (sut/create {:image-name "alpine:3.5" server-container (sut/create {:image-name "alpine:3.5"
:network network :network network
:network-aliases ["foo"] :network-aliases ["foo"]
:command ["/bin/sh" :command ["/bin/sh"
"-c" "-c"
"while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"]}) "while true ; do printf 'HTTP/1.1 200 OK\\n\\nyay' | nc -l -p 8080; done"]})
client-container (sut/create {:image-name "alpine:3.5" client-container (sut/create {:image-name "alpine:3.5"
:network network :network network
:command ["top"]}) :command ["top"]})
started-server (sut/start! server-container) started-server (sut/start! server-container)
started-client (sut/start! client-container) started-client (sut/start! client-container)
response (sut/execute-command! started-client ["wget", "-O", "-", "http://foo:8080"]) response (sut/execute-command! started-client ["wget", "-O", "-", "http://foo:8080"])