An idiomatic, data-driven, REPL friendly Clojure Docker client inspired from Cognitect's AWS client.
See this for documentation for versions before 0.4.0.
The README here is for the current master branch and may not reflect the released version.
Please raise issues here for any new feature requests!
Leiningen/Boot
[lispyclouds/clj-docker-client "0.5.2"]
Clojure CLI/deps.edn
{lispyclouds/clj-docker-client {:mvn/version "0.5.2"}}
Gradle
compile 'lispyclouds:clj-docker-client:0.5.2'
Maven
<dependency>
<groupId>lispyclouds</groupId>
<artifactId>clj-docker-client</artifactId>
<version>0.5.2</version>
</dependency>
Auto generated code docs can be found here
This uses Docker's HTTP REST API to run. See the section API version matrix in https://docs.docker.com/develop/sdk/ to find the corresponding API version for the Docker daemon you're running.
See the page about the docker REST API to learn more about the params to pass.
Since this is fully data driven, using REBL is really beneficial as it allows us to walk through the output from Docker, see potential errors and be more productive with instant visual feedback.
This assumes Java 11+:
REBL_PATH=<PATH_TO_REBL_JAR> lein with-profile +rebl repl
.(rebl/ui)
to fire up the REBL UI.(require '[clj-docker-client.core :as docker])
This library aims to be a as thin layer as possible between you and Docker. This consists of following public functions:
Lists the categories of operations supported. Can be bound to an API version.
(docker/categories) ; Latest version
(docker/categories "v1.40") ; Locked to v1.40
#_=> #{:system
:exec
:images
:secrets
:events
:_ping
:containers
:auth
:tasks
:volumes
:networks
:build
:nodes
:commit
:plugins
:info
:swarm
:distribution
:version
:services
:configs
:session}
Connect to the docker daemon's UNIX socket and create a client scoped to the operations of a given category. Can be bound to an API version.
(def images (docker/client {:category :images
:conn {:uri "unix:///var/run/docker.sock"}})) ; Latest version
(def containers (docker/client {:category :containers
:conn {:uri "unix:///var/run/docker.sock"}
:api-version "v1.40"})) ; Container client for v1.40
Using a timeout for the connections. Thanks olymk2 for the suggestion. Docker actions can take quite a long time so set the timeout accordingly. When you don't provide timeouts then there will be no timeout clientside.
(def ping (docker/client {:category :_ping
:conn {:uri "unix:///var/run/docker.sock"
:timeouts {:connect-timeout 10
:read-timeout 30000
:write-timeout 30000
:call-timeout 30000}}}))
Lists the supported ops by a client.
(docker/ops images)
#_=> (:ImageList
:ImageCreate
:ImageInspect
:ImageHistory
:ImagePush
:ImageTag
:ImageDelete
:ImageSearch
:ImagePrune
:ImageGet
:ImageGetAll
:ImageLoad)
Returns the doc of an operation in a client.
(docker/doc images :ImageList)
#_=> {:doc
"List Images\nReturns a list of images on the server. Note that it uses a different, smaller representation of an image than inspecting a single image.",
:params
({:name "all", :type "boolean"}
{:name "filters", :type "string"}
{:name "digests", :type "boolean"})}
Invokes an operation via the client and a given operation map and returns the result data.
; Pulls the busybox:musl image from Docker hub
(docker/invoke images {:op :ImageCreate
:params {:fromImage "busybox:musl"}})
; Creates a container named conny from it
(docker/invoke containers {:op :ContainerCreate
:params {:name "conny"
:body {:Image "busybox:musl"
:Cmd "ls"}}})
The operation map is of the following structure:
{:op :NameOfOp
:params {:param-1 "value1"
:param-2 true}}
Takes an optional key as
. Defaults to :data
. Returns an InputStream if passed as :stream
, the raw underlying network socket if passed as :socket
. :stream
is useful for streaming responses like logs, events etc, which run till the container is up. :socket
is useful for events when bidirectional streams are returned by docker in operations like :ContainerAttach
.
{:op :NameOfOp
:params {:param-1 "value1"
:param-2 true}
:as :stream}
v1.40
API docs.stream
is mapped to java.io.InputStream
and when the API needs a stream as an input, send an InputStream. When it returns a stream, the call can possibly block till the container or source is up and its recommended to pass the as
param as :stream
to the invoke call and read it asynchronously. See this section for more info.(def images (docker/client {:category :images
:conn {:uri "unix:///var/run/docker.sock"}}))
(docker/invoke images {:op :ImageCreate
:params {:fromImage "busybox:musl"}})
(def containers (docker/client {:category :containers
:conn {:uri "unix:///var/run/docker.sock"}}))
(docker/invoke containers {:op :ContainerCreate
:params {:name "conny"
:body {:Image "busybox:musl"
:Cmd ["sh"
"-c"
"i=1; while :; do echo $i; sleep 1; i=$((i+1)); done"]}}})
(docker/invoke containers {:op :ContainerStart
:params {:id "conny"}})
; fn to react when data is available
(defn react-to-stream
[stream reaction-fn]
(future
(with-open [rdr (clojure.java.io/reader stream)]
(loop [r (java.io.BufferedReader. rdr)]
(when-let [line (.readLine r)]
(reaction-fn line)
(recur r))))))
(def log-stream (docker/invoke containers {:op :ContainerLogs
:params {:id "conny"
:follow true
:stdout true}
:as :stream}))
(react-to-stream log-stream println) ; prints the logs line by line when they come.
;; This is a raw bidirectional java.net.Socket, so both reads and writes are possible.
;; conny-reader has been started with: docker run -d -i --name conny-reader alpine:latest sh -c "cat - >/out"
(def sock (docker/invoke containers {:op :ContainerAttach
:params {:id "conny-reader"
:stream true
:stdin true}
:as :socket}))
(clojure.java.io/copy "hello" (.getOutputStream sock))
(.close sock) ; Important for freeing up resources.
There are some cases where you may need access to an API that is either experimental or is not in the swagger docs. Docker checkpoint is one such example. Thanks @mk for bringing it up!
Since this uses the published APIs from the swagger spec, the way to access them is to use the lower level fn fetch
from the clj-docker-client/requests
ns. The caveat is the response will be totally raw(data, stream or the socket itself).
fetch takes the following params as a map:
:get
./v1.40/containers/{id}/checkpoints
. Pass {:id "conny"}
here.invoke
. Default: :data
.(require '[clj-docker-client.requests :as req])
(require '[clj-docker-client.core :as docker])
;; This is the undocumented API in the Docker Daemon.
;; See https://github.com/moby/moby/pull/22049/files#diff-8038ade87553e3a654366edca850f83dR11
(req/fetch {:conn (req/connect* {:uri "unix:///var/run/docker.sock"})
:url "/v1.40/containers/conny/checkpoints"})
More examples of low level calls:
;; Ping the server
(req/fetch {:conn (req/connect* {:uri "unix:///var/run/docker.sock"})
:url "/v1.40/_ping"})
;; Copy a folder to a container
(req/fetch {:conn (req/connect* {:uri "unix:///var/run/docker.sock"})
:url "/v1.40/containers/conny/archive"
:method :put
:query {:path "/root/src"}
:body (-> "src.tar.gz"
io/file
io/input-stream)})
And anything else is possible!
Copyright © 2020 Rahul De and contributors.
Distributed under the LGPLv3+ License. See LICENSE.
Can you improve this documentation? These fine people already did:
Rahul De, Timo Kramer, Praveen & Rahuλ DéEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close