2020-07-16 20:44:26 +00:00
|
|
|
(ns clj-test-containers.core
|
2020-08-09 19:38:20 +00:00
|
|
|
(:require
|
2020-08-10 06:15:00 +00:00
|
|
|
[clj-test-containers.spec.core :as cs]
|
2020-10-05 08:21:16 +00:00
|
|
|
[clojure.spec.alpha :as s]
|
|
|
|
|
[clojure.string])
|
2020-08-09 19:38:20 +00:00
|
|
|
(:import
|
|
|
|
|
(java.nio.file
|
|
|
|
|
Paths)
|
|
|
|
|
(org.testcontainers.containers
|
|
|
|
|
BindMode
|
|
|
|
|
GenericContainer
|
|
|
|
|
Network)
|
2020-10-05 08:21:16 +00:00
|
|
|
(org.testcontainers.containers.output
|
|
|
|
|
ToStringConsumer)
|
2020-09-24 17:26:02 +00:00
|
|
|
(org.testcontainers.containers.wait.strategy
|
|
|
|
|
Wait)
|
2020-08-09 19:38:20 +00:00
|
|
|
(org.testcontainers.images.builder
|
|
|
|
|
ImageFromDockerfile)
|
|
|
|
|
(org.testcontainers.utility
|
2020-09-24 17:26:02 +00:00
|
|
|
MountableFile)))
|
2020-06-04 20:51:33 +00:00
|
|
|
|
2020-07-16 20:44:26 +00:00
|
|
|
(defn- resolve-bind-mode
|
|
|
|
|
[bind-mode]
|
2020-06-04 20:51:33 +00:00
|
|
|
(if (= :read-write bind-mode)
|
2020-07-16 20:44:26 +00:00
|
|
|
BindMode/READ_WRITE
|
|
|
|
|
BindMode/READ_ONLY))
|
2020-06-04 20:51:33 +00:00
|
|
|
|
2020-09-26 19:32:14 +00:00
|
|
|
(defmulti wait
|
|
|
|
|
"Sets a wait strategy to the container.
|
2020-10-05 08:21:16 +00:00
|
|
|
Supports :http, :health and :log as strategies.
|
|
|
|
|
|
2020-09-26 19:32:14 +00:00
|
|
|
## HTTP Strategy
|
2020-10-05 08:21:16 +00:00
|
|
|
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
|
|
|
|
|
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.
|
2020-09-26 19:32:14 +00:00
|
|
|
Example:
|
2020-10-05 08:21:16 +00:00
|
|
|
|
2020-09-26 19:32:14 +00:00
|
|
|
```clojure
|
2020-10-05 08:21:16 +00:00
|
|
|
(wait {:wait-strategy :http
|
2020-09-26 19:32:14 +00:00
|
|
|
:port 80
|
|
|
|
|
:path \"/\"
|
|
|
|
|
:status-codes [200 201]
|
|
|
|
|
:tls true
|
|
|
|
|
:read-timeout 5
|
|
|
|
|
:basic-credentials {:username \"user\"
|
|
|
|
|
:password \"password\"}}
|
2020-10-05 08:21:16 +00:00
|
|
|
container)
|
2020-09-26 19:32:14 +00:00
|
|
|
```
|
|
|
|
|
## Health Strategy
|
2020-10-05 08:21:16 +00:00
|
|
|
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)
|
|
|
|
|
```
|
2020-09-26 19:32:14 +00:00
|
|
|
|
|
|
|
|
## Log Strategy
|
2020-10-05 08:21:16 +00:00
|
|
|
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. The output is `grepped` against
|
|
|
|
|
the log message.
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
|
|
```clojure
|
|
|
|
|
(wait {:wait-strategy :log
|
|
|
|
|
:message \"accept connections\"} container)
|
|
|
|
|
```"
|
|
|
|
|
:wait-strategy)
|
2020-09-24 17:23:12 +00:00
|
|
|
|
|
|
|
|
(defmethod wait :http
|
2020-09-26 19:32:14 +00:00
|
|
|
[{:keys [path port status-codes tls read-timeout basic-credentials] :as options} container]
|
|
|
|
|
(let [for-http (Wait/forHttp path)]
|
|
|
|
|
(when port
|
|
|
|
|
(.forPort for-http port))
|
|
|
|
|
|
|
|
|
|
(doseq [status-code status-codes]
|
|
|
|
|
(.forStatusCode for-http status-code))
|
|
|
|
|
|
|
|
|
|
(when tls
|
|
|
|
|
(.usingTls for-http))
|
|
|
|
|
|
|
|
|
|
(when read-timeout
|
|
|
|
|
(.withReadTimeout (java.time.Duration/ofSeconds read-timeout)))
|
|
|
|
|
|
|
|
|
|
(when basic-credentials
|
|
|
|
|
(let [{username :username password :password} basic-credentials]
|
|
|
|
|
(.withBasicCredentials username password)))
|
|
|
|
|
|
|
|
|
|
(.waitingFor container for-http)
|
|
|
|
|
|
|
|
|
|
{:wait-for-http (dissoc options :strategy)}))
|
2020-09-24 17:23:12 +00:00
|
|
|
|
|
|
|
|
(defmethod wait :health
|
|
|
|
|
[_ container]
|
|
|
|
|
(.waitingFor container (Wait/forHealthcheck))
|
|
|
|
|
{:wait-for-healthcheck true})
|
|
|
|
|
|
|
|
|
|
(defmethod wait :log
|
|
|
|
|
[{:keys [message]} container]
|
|
|
|
|
(let [log-message (str ".*" message ".*\\n")]
|
|
|
|
|
(.waitingFor container (Wait/forLogMessage log-message 1))
|
|
|
|
|
{:wait-for-log-message log-message}))
|
|
|
|
|
|
|
|
|
|
(defmethod wait :default [_ _] nil)
|
|
|
|
|
|
2020-08-10 06:15:00 +00:00
|
|
|
(s/fdef init
|
|
|
|
|
:args (s/cat :init-options ::cs/init-options)
|
|
|
|
|
:ret ::cs/container)
|
|
|
|
|
|
2020-08-05 05:25:59 +00:00
|
|
|
(defn init
|
2020-06-04 08:58:10 +00:00
|
|
|
"Sets the properties for a testcontainer instance"
|
2020-10-05 08:21:16 +00:00
|
|
|
[{:keys [container exposed-ports env-vars command network network-aliases wait-for] :as init-options}]
|
2020-08-05 13:37:35 +00:00
|
|
|
|
2020-08-05 05:25:59 +00:00
|
|
|
(.setExposedPorts container (map int exposed-ports))
|
2020-07-16 20:44:26 +00:00
|
|
|
|
2020-08-05 05:25:59 +00:00
|
|
|
(run! (fn [[k v]] (.addEnv container k v)) env-vars)
|
2020-07-16 20:44:26 +00:00
|
|
|
|
2020-08-05 05:25:59 +00:00
|
|
|
(when command
|
2020-08-05 13:37:35 +00:00
|
|
|
(.setCommand container (into-array String command)))
|
|
|
|
|
|
|
|
|
|
(when network
|
|
|
|
|
(.setNetwork container (:network network)))
|
|
|
|
|
|
|
|
|
|
(when network-aliases
|
|
|
|
|
(.setNetworkAliases container (java.util.ArrayList. network-aliases)))
|
2020-07-16 20:44:26 +00:00
|
|
|
|
2020-10-05 08:21:16 +00:00
|
|
|
(merge init-options {:container container
|
|
|
|
|
:exposed-ports (vec (.getExposedPorts container))
|
|
|
|
|
:env-vars (into {} (.getEnvMap container))
|
|
|
|
|
:host (.getHost container)
|
|
|
|
|
:network network} (wait wait-for container)))
|
2020-08-05 05:25:59 +00:00
|
|
|
|
2020-08-10 06:15:00 +00:00
|
|
|
(s/fdef create
|
|
|
|
|
:args (s/cat :create-options ::cs/create-options)
|
|
|
|
|
:ret ::cs/container)
|
|
|
|
|
|
2020-08-05 05:25:59 +00:00
|
|
|
(defn create
|
|
|
|
|
"Creates a generic testcontainer and sets its properties"
|
|
|
|
|
[{:keys [image-name] :as options}]
|
|
|
|
|
(->> (GenericContainer. image-name)
|
|
|
|
|
(assoc options :container)
|
|
|
|
|
init))
|
2020-06-04 10:35:16 +00:00
|
|
|
|
2020-08-05 05:13:31 +00:00
|
|
|
(defn create-from-docker-file
|
2020-08-05 07:12:23 +00:00
|
|
|
"Creates a testcontainer from a provided Dockerfile"
|
|
|
|
|
[{:keys [docker-file] :as options}]
|
|
|
|
|
(->> (.withDockerfile (ImageFromDockerfile.) (Paths/get "." (into-array [docker-file])))
|
|
|
|
|
(GenericContainer.)
|
|
|
|
|
(assoc options :container)
|
|
|
|
|
init))
|
2020-08-05 05:13:31 +00:00
|
|
|
|
2020-07-16 20:44:26 +00:00
|
|
|
(defn map-classpath-resource!
|
2020-06-06 15:30:00 +00:00
|
|
|
"Maps a resource in the classpath to the given container path. Should be called before starting the container!"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config
|
2020-06-13 12:49:07 +00:00
|
|
|
{:keys [resource-path container-path mode]}]
|
2020-07-16 20:44:26 +00:00
|
|
|
(assoc container-config :container (.withClasspathResourceMapping (:container container-config)
|
|
|
|
|
resource-path
|
|
|
|
|
container-path
|
2020-06-06 15:30:00 +00:00
|
|
|
(resolve-bind-mode mode))))
|
|
|
|
|
|
2020-07-16 20:44:26 +00:00
|
|
|
(defn bind-filesystem!
|
2020-06-06 15:30:00 +00:00
|
|
|
"Binds a source from the filesystem to the given container path. Should be called before starting the container!"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config {:keys [host-path container-path mode]}]
|
|
|
|
|
(assoc container-config
|
|
|
|
|
:container (.withFileSystemBind (:container container-config)
|
|
|
|
|
host-path
|
|
|
|
|
container-path
|
|
|
|
|
(resolve-bind-mode mode))))
|
2020-06-06 11:57:31 +00:00
|
|
|
|
2020-06-06 13:18:31 +00:00
|
|
|
(defn copy-file-to-container!
|
2020-06-06 11:57:31 +00:00
|
|
|
"Copies a file into the running container"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config
|
2020-06-13 12:49:07 +00:00
|
|
|
{:keys [container-path path type]}]
|
2020-07-16 20:44:26 +00:00
|
|
|
(let [mountable-file (cond
|
|
|
|
|
(= :classpath-resource type)
|
|
|
|
|
(MountableFile/forClasspathResource path)
|
|
|
|
|
|
|
|
|
|
(= :host-path type)
|
|
|
|
|
(MountableFile/forHostPath path)
|
|
|
|
|
:else
|
|
|
|
|
:error)]
|
|
|
|
|
(assoc container-config
|
|
|
|
|
:container
|
|
|
|
|
(.withCopyFileToContainer (:container container-config)
|
|
|
|
|
mountable-file
|
2020-06-06 11:57:31 +00:00
|
|
|
container-path))))
|
|
|
|
|
|
2020-07-16 20:44:26 +00:00
|
|
|
(defn execute-command!
|
2020-06-06 15:30:00 +00:00
|
|
|
"Executes a command in the container, and returns the result"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config command]
|
|
|
|
|
(let [container (:container container-config)
|
2020-06-06 12:43:11 +00:00
|
|
|
result (.execInContainer container
|
2020-07-16 20:44:26 +00:00
|
|
|
(into-array command))]
|
2020-06-06 12:43:11 +00:00
|
|
|
{:exit-code (.getExitCode result)
|
|
|
|
|
:stdout (.getStdout result)
|
|
|
|
|
:stderr (.getStderr result)}))
|
|
|
|
|
|
2020-10-05 08:21:16 +00:00
|
|
|
(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)))
|
|
|
|
|
|
2020-07-16 20:44:26 +00:00
|
|
|
(defn start!
|
2020-06-04 10:35:16 +00:00
|
|
|
"Starts the underlying testcontainer instance and adds new values to the response map, e.g. :id and :first-mapped-port"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config]
|
2020-10-05 08:21:16 +00:00
|
|
|
(let [{:keys [container log-to]} container-config]
|
2020-06-04 10:35:16 +00:00
|
|
|
(.start container)
|
2020-10-05 08:21:16 +00:00
|
|
|
(-> (merge container-config
|
|
|
|
|
{:id (.getContainerId container)
|
|
|
|
|
:mapped-ports (into {}
|
|
|
|
|
(map (fn [port] [port (.getMappedPort container port)])
|
|
|
|
|
(:exposed-ports container-config)))}
|
|
|
|
|
(log log-to container))
|
|
|
|
|
(dissoc :log-to))))
|
2020-06-04 10:35:16 +00:00
|
|
|
|
2020-06-06 13:18:31 +00:00
|
|
|
(defn stop!
|
2020-06-04 10:35:16 +00:00
|
|
|
"Stops the underlying container"
|
2020-07-16 20:44:26 +00:00
|
|
|
[container-config]
|
|
|
|
|
(.stop (:container container-config))
|
|
|
|
|
(-> container-config
|
2020-06-04 11:04:30 +00:00
|
|
|
(dissoc :id)
|
2020-10-05 08:21:16 +00:00
|
|
|
(dissoc :string-log)
|
2020-06-04 11:04:30 +00:00
|
|
|
(dissoc :mapped-ports)))
|
2020-08-05 13:37:35 +00:00
|
|
|
|
2020-08-18 12:44:04 +00:00
|
|
|
(s/fdef create-network
|
2020-08-10 06:15:00 +00:00
|
|
|
:args (s/alt :nullary (s/cat)
|
2020-08-18 12:44:04 +00:00
|
|
|
:unary (s/cat :create-network-options
|
|
|
|
|
::cs/create-network-options))
|
2020-08-10 06:15:00 +00:00
|
|
|
:ret ::cs/network)
|
2020-08-05 13:37:35 +00:00
|
|
|
|
2020-08-18 12:44:04 +00:00
|
|
|
(defn create-network
|
2020-08-05 13:37:35 +00:00
|
|
|
"Creates a network. The optional map accepts config values for enabling ipv6 and setting the driver"
|
|
|
|
|
([]
|
2020-08-18 12:44:04 +00:00
|
|
|
(create-network {}))
|
2020-08-10 06:15:00 +00:00
|
|
|
([{:keys [ipv6 driver]}]
|
|
|
|
|
(let [builder (Network/builder)]
|
|
|
|
|
(when ipv6
|
|
|
|
|
(.enableIpv6 builder true))
|
|
|
|
|
|
|
|
|
|
(when driver
|
|
|
|
|
(.driver builder driver))
|
|
|
|
|
|
|
|
|
|
(let [network (.build builder)]
|
|
|
|
|
{:network network
|
|
|
|
|
:name (.getName network)
|
|
|
|
|
:ipv6 (.getEnableIpv6 network)
|
|
|
|
|
:driver (.getDriver network)}))))
|
2020-08-18 12:44:04 +00:00
|
|
|
|
|
|
|
|
(def ^:deprecated init-network create-network)
|