Skip to content

Instantly share code, notes, and snippets.

@hlship
Created October 30, 2014 21:48
Show Gist options
  • Save hlship/582cb6880c17290d6c8b to your computer and use it in GitHub Desktop.
Save hlship/582cb6880c17290d6c8b to your computer and use it in GitHub Desktop.
(ns docker-utils
"Utilies for starting and stopping a Postgres Docker container."
(:import
[java.io File]
[java.net ServerSocket])
(:require
[io.aviso.ansi :refer [bold]]
[clojure.java.shell :refer [sh]]
[clojure.tools.logging :as l]
[clojure.edn :as edn]
[clojure.string :as str]))
(defn- elapsed-since [start]
(- (System/currentTimeMillis) start))
(defn- read-local-docker-config
[]
;; docker.edn is a local file, not tracker by git, and is optional.
(let [file (File. "docker.edn")]
(when (.exists file)
(l/debugf "Reading local Docker configuration from `%s'." file)
(-> file slurp edn/read-string))))
(defn- wait-until
"Execute cmd-fn until evaluating test-fn of its result returns truthy. Time
out after max-wait millis.
Returns the result of cmd-fn, or ::timeout."
[cmd-fn test-fn max-wait]
(let [start-time (System/currentTimeMillis)]
(loop [result (cmd-fn)]
(cond
(test-fn result)
result
(> (elapsed-since start-time) max-wait)
::timeout
:else
(do (Thread/sleep 50)
(recur (cmd-fn)))))))
(defn- docker
"Executes docker, passing it provided arguments."
[docker-config & args]
(let [{:keys [docker-command env sudo]} docker-config
command (if sudo
["sudo" docker-command]
[docker-command])]
(apply sh (concat command
args
;; Options come last:
[:env env]))))
;;; Borrowed from: http://code.daaku.org/freeport.clj/
(defn get-free-port!
[]
(let [socket (ServerSocket. 0)
port (.getLocalPort socket)]
(.close socket)
port))
(def ^:private started-containers
"Maps port numbers to shutdown functions for the started container."
(atom {}))
(defn- wait-for-startup
"Waits for the container to startup. Returns nil or throws an exception."
[docker-config container-name start-result]
(let [wait-result (wait-until #(docker docker-config "logs" container-name)
#(re-find #"database system is ready to accept connections"
(:err %)) ;; docker logs outputs to STDERR for some reason
5000)]
(if (= ::timeout wait-result)
(throw (ex-info (format "Docker container `%s' did not complete initialization before timeout." container-name)
{:wait-result wait-result
:start-result start-result}))))
nil)
(defn- launch-container
[docker-config port]
(let
[container-name (format "%s.%d" (:container-name docker-config) port)
_ (l/infof "Starting Docker container `%s'." container-name)
start-result (docker docker-config "run" "-d"
"-p" (str port ":5432")
"--name" container-name
(:image-name docker-config))]
(if (not= 0 (:exit start-result))
(throw (ex-info (format "Docker container `%s' failed to start." container-name) start-result)))
(wait-for-startup docker-config container-name start-result)
(fn []
(if (:remove-on-completion docker-config)
(docker docker-config "rm" "-f" container-name)
(docker docker-config "stop" container-name))
(swap! started-containers dissoc port))))
(defn- read-docker-config
[]
(merge {:docker-command "docker"
:sudo false
:container-name "combined-db--specs"
:image-name "quay.io/aviso/combined-db"
:remove-on-completion true
:env {}}
(read-local-docker-config)))
(defn start-container
"Starts a new Docker container, returning the port number that the container listens on."
[]
(let [port (get-free-port!)
shutdown-fn (launch-container (read-docker-config) port)]
(swap! started-containers assoc port shutdown-fn)
port))
(defn stop-container
"Stops a previously started container, identified by its port number. The container is stopped asynchronously. Returns nil."
[port]
(when-let [f (get @started-containers port)]
(f))
nil)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment