(ns clj-test-containers.core (:require [clj-test-containers.spec.container :as csc] [clj-test-containers.spec.core :as cs] [clojure.spec.alpha :as s]) (:import (java.nio.file Paths) (org.testcontainers.containers BindMode GenericContainer Network) (org.testcontainers.images.builder ImageFromDockerfile) (org.testcontainers.utility MountableFile))) (defn- resolve-bind-mode [bind-mode] (if (= :read-write bind-mode) BindMode/READ_WRITE BindMode/READ_ONLY)) (s/fdef init :args (s/cat :init-options ::cs/init-options) :ret ::cs/container) (defn init "Sets the properties for a testcontainer instance" [{:keys [container exposed-ports env-vars command network network-aliases]}] (.setExposedPorts container (map int exposed-ports)) (run! (fn [[k v]] (.addEnv container k v)) env-vars) (when command (.setCommand container (into-array String command))) (when network (.setNetwork container (:network network))) (when network-aliases (.setNetworkAliases container (java.util.ArrayList. network-aliases))) {:container container :exposed-ports (vec (.getExposedPorts container)) :env-vars (into {} (.getEnvMap container)) :host (.getHost container) :network network}) (s/fdef create :args (s/cat :create-options ::cs/create-options) :ret ::cs/container) (defn create "Creates a generic testcontainer and sets its properties" [{:keys [image-name] :as options}] (->> (GenericContainer. image-name) (assoc options :container) init)) (defn create-from-docker-file "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)) (defn map-classpath-resource! "Maps a resource in the classpath to the given container path. Should be called before starting the container!" [container-config {:keys [resource-path container-path mode]}] (assoc container-config :container (.withClasspathResourceMapping (:container container-config) resource-path container-path (resolve-bind-mode mode)))) (defn bind-filesystem! "Binds a source from the filesystem to the given container path. Should be called before starting the container!" [container-config {:keys [host-path container-path mode]}] (assoc container-config :container (.withFileSystemBind (:container container-config) host-path container-path (resolve-bind-mode mode)))) (defn copy-file-to-container! "Copies a file into the running container" [container-config {:keys [container-path path type]}] (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 container-path)))) (defn execute-command! "Executes a command in the container, and returns the result" [container-config command] (let [container (:container container-config) result (.execInContainer container (into-array command))] {:exit-code (.getExitCode result) :stdout (.getStdout result) :stderr (.getStderr result)})) (defn start! "Starts the underlying testcontainer instance and adds new values to the response map, e.g. :id and :first-mapped-port" [container-config] (let [container (:container container-config)] (.start container) (-> container-config (assoc :id (.getContainerId container)) (assoc :mapped-ports (into {} (map (fn [port] [port (.getMappedPort container port)]) (:exposed-ports container-config))))))) (defn stop! "Stops the underlying container" [container-config] (.stop (:container container-config)) (-> container-config (dissoc :id) (dissoc :mapped-ports))) (s/fdef init-network :args (s/alt :nullary (s/cat) :unary (s/cat :init-network-options ::cs/init-network-options)) :ret ::cs/network) (defn ^:no-gen init-network "Creates a network. The optional map accepts config values for enabling ipv6 and setting the driver" ([] (init-network {})) ([{:keys [ipv6 driver]}] (let [builder (Network/builder)] (when ipv6 (.enableIpv6 builder true)) (when driver (.driver builder driver)) (let [network (.build builder)] {:network network :id (.getId network) :name (.getName network) :ipv6 (.getEnableIpv6 network) :driver (.getDriver network)}))))