Add support for accessing container logs. (#34)

* Add support for accessing container logs.

closes #27

-=david=-

* Don't add logs to configuration if not capturing the log

* Further refinements
This commit is contained in:
David Harrigan 2020-10-05 08:21:16 +00:00 committed by GitHub
parent 9f36f0db78
commit ec26e4c078
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 106 additions and 32 deletions

View file

@ -23,9 +23,9 @@
[org.clojure/test.check "1.1.0"] [org.clojure/test.check "1.1.0"]
[org.clojure/tools.namespace "1.0.0"] [org.clojure/tools.namespace "1.0.0"]
[org.testcontainers/postgresql "1.14.3"]] [org.testcontainers/postgresql "1.14.3"]]
:githooks {:auto-install true ; :githooks {:auto-install true
:ci-env-variable "CI" ; :ci-env-variable "CI"
:pre-commit ["script/pre-commit"]} ; :pre-commit ["script/pre-commit"]
:source-paths ["dev-src"]}} :source-paths ["dev-src"]}}
:target-path "target/%s") :target-path "target/%s")

View file

@ -1,7 +1,8 @@
(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])
(:import (:import
(java.nio.file (java.nio.file
Paths) Paths)
@ -9,6 +10,8 @@
BindMode BindMode
GenericContainer GenericContainer
Network) Network)
(org.testcontainers.containers.output
ToStringConsumer)
(org.testcontainers.containers.wait.strategy (org.testcontainers.containers.wait.strategy
Wait) Wait)
(org.testcontainers.images.builder (org.testcontainers.images.builder
@ -24,17 +27,17 @@
(defmulti wait (defmulti wait
"Sets a wait strategy to the container. "Sets a wait strategy to the container.
Supports :http, :health and :log as strategies. Supports :http, :health and :log as strategies.
## HTTP Strategy ## HTTP Strategy
The :http strategy will only accept the container as initialized if it can be accessed 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 boolean that specifies 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 with basic credentials, containing if TLS is enabled, a read timeout in seconds and a map with basic credentials, containing
username and password. Only the path is required, all others are optional. username and password. Only the path is required, all others are optional.
Example: Example:
```clojure ```clojure
(wait {:strategy :http (wait {:wait-strategy :http
:port 80 :port 80
:path \"/\" :path \"/\"
:status-codes [200 201] :status-codes [200 201]
@ -42,14 +45,29 @@
:read-timeout 5 :read-timeout 5
:basic-credentials {:username \"user\" :basic-credentials {:username \"user\"
:password \"password\"}} :password \"password\"}}
container)) container)
``` ```
## Health Strategy ## Health Strategy
TBD The :health strategy only accepts a true or false value. This enables support for Docker's
healthcheck feature, whereby you can directly leverage the healthy state of your container
as your wait condition.
Example:
```clojure
(wait {:wait-strategy :health :true} container)
```
## Log Strategy ## Log Strategy
TBD" The :log strategy accepts a message which simply causes the output of your container's log
:strategy) to be used in determining if the container is ready or not. The output is `grepped` against
the log message.
Example:
```clojure
(wait {:wait-strategy :log
:message \"accept connections\"} container)
```"
:wait-strategy)
(defmethod wait :http (defmethod wait :http
[{:keys [path port status-codes tls read-timeout basic-credentials] :as options} container] [{:keys [path port status-codes tls read-timeout basic-credentials] :as options} container]
@ -93,7 +111,7 @@
(defn init (defn init
"Sets the properties for a testcontainer instance" "Sets the properties for a testcontainer instance"
[{:keys [container exposed-ports env-vars command network network-aliases wait-for]}] [{:keys [container exposed-ports env-vars command network network-aliases wait-for] :as init-options}]
(.setExposedPorts container (map int exposed-ports)) (.setExposedPorts container (map int exposed-ports))
@ -108,11 +126,11 @@
(when network-aliases (when network-aliases
(.setNetworkAliases container (java.util.ArrayList. network-aliases))) (.setNetworkAliases container (java.util.ArrayList. network-aliases)))
(merge {: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)
@ -179,16 +197,58 @@
:stdout (.getStdout result) :stdout (.getStdout result)
:stderr (.getStderr result)})) :stderr (.getStderr result)}))
(defmulti log
"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.
## String Strategy
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 the `dump-logs`
function.
Example:
```clojure
{:log-strategy :string}
```
Then, later in your program, you can access the logs thus:
```clojure
(def container-config (tc/start! container))
(tc/dump-logs container-config)
```
"
:log-strategy)
(defmethod log :string
[_ container]
(let [to-string-consumer (ToStringConsumer.)]
(.followOutput container to-string-consumer)
{:string-log (fn []
(-> (.toUtf8String to-string-consumer)
(clojure.string/replace #"\n+" "\n")))}))
(defmethod log :slf4j [_ _] nil)
(defmethod log :default [_ _] nil)
(defn dump-logs
"Dumps the logs found by invoking the function on the :string-log key"
[container-config]
((:string-log container-config)))
(defn start! (defn start!
"Starts the underlying testcontainer instance and adds new values to the response map, e.g. :id and :first-mapped-port" "Starts the underlying testcontainer instance and adds new values to the response map, e.g. :id and :first-mapped-port"
[container-config] [container-config]
(let [container (:container container-config)] (let [{:keys [container log-to]} container-config]
(.start container) (.start container)
(-> container-config (-> (merge container-config
(assoc :id (.getContainerId container)) {:id (.getContainerId container)
(assoc :mapped-ports (into {} :mapped-ports (into {}
(map (fn [port] [port (.getMappedPort container port)]) (map (fn [port] [port (.getMappedPort container port)])
(:exposed-ports container-config))))))) (:exposed-ports container-config)))}
(log log-to container))
(dissoc :log-to))))
(defn stop! (defn stop!
"Stops the underlying container" "Stops the underlying container"
@ -196,6 +256,7 @@
(.stop (:container container-config)) (.stop (:container container-config))
(-> container-config (-> container-config
(dissoc :id) (dissoc :id)
(dissoc :string-log)
(dissoc :mapped-ports))) (dissoc :mapped-ports)))
(s/fdef create-network (s/fdef create-network

View file

@ -35,7 +35,9 @@
(s/def ::log (s/def ::log
keyword?) keyword?)
(s/def ::strategy #{:http :health :log}) (s/def ::wait-strategy #{:http :health :log})
(s/def ::log-strategy #{:string})
(s/def ::path (s/def ::path
string?) string?)
@ -45,3 +47,6 @@
(s/def ::check (s/def ::check
boolean?) boolean?)
(s/def ::string
string?)

View file

@ -5,11 +5,15 @@
[clojure.spec.alpha :as s])) [clojure.spec.alpha :as s]))
(s/def ::wait-for (s/def ::wait-for
(s/keys :req-un [::csc/strategy] (s/keys :req-un [::csc/wait-strategy]
:opt-un [::csc/path :opt-un [::csc/path
::csc/message ::csc/message
::csc/check])) ::csc/check]))
(s/def ::log-to
(s/keys :req-un [::csc/log-strategy]
:opt-un [::csc/string]))
(s/def ::network (s/def ::network
(s/nilable (s/keys :req-un [::csn/network (s/nilable (s/keys :req-un [::csn/network
::csn/name ::csn/name
@ -22,7 +26,9 @@
::csc/env-vars ::csc/env-vars
::csc/host] ::csc/host]
:opt-un [::network :opt-un [::network
::wait-for])) ::wait-for
::log-to]))
(s/def ::init-options (s/def ::init-options
(s/keys :req-un [::csc/container] (s/keys :req-un [::csc/container]
@ -31,6 +37,7 @@
::csc/command ::csc/command
::network ::network
::wait-for ::wait-for
::log-to
::csc/network-aliases])) ::csc/network-aliases]))
(s/def ::create-options (s/def ::create-options
@ -40,6 +47,7 @@
::csc/command ::csc/command
::network ::network
::wait-for ::wait-for
::log-to
::csc/network-aliases])) ::csc/network-aliases]))
(s/def ::create-network-options (s/def ::create-network-options

View file

@ -23,7 +23,7 @@
(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 {:strategy :log :message "accept connections"}}) :wait-for {:wait-strategy :log :message "accept connections"}})
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)))