(def images-docker (c/client {:engine :docker
:category :images
:version "v1.41"
:conn {:uri "unix:///var/run/docker.sock"}}))
(c/invoke images-docker {:op :ImageCreate
:params {:fromImage "busybox:musl"}})
Docker builds an image within a context, ie a set of files. The docker API
requires a tar file as the body of the request. As of 2022-02-04, the header
field Content-type
is not filled by its default value
application/x-tar
, so it has to be added manually.
Thus, to build an image, a user needs to create a tar with all the relevant
files for building it and also add the :Content-type
field in the query
params.
Given the following Dockerfile
FROM docker.io/amazoncorretto:17-alpine3.15-jdk
COPY app.jar .
CMD ["java", "-jar", "app.jar", "init"]
and an arbitrary jar app.jar
at the root of your project, the following
script would build the contajners-build-example
image.
(require '[babashka.process :as process])
(require '[cheshire.core :as json])
(require '[clojure.pprint :as pprint])
(def build
(c/client {:engine :docker
:category :build
:version "v1.41"
:conn {:uri "unix:///var/run/docker.sock"}}))
(defn ->tar!
[]
;; gather the files in the tar file
(process/sh ["tar" "-czvf" "docker.tar.gz"
"app.jar"
"Dockerfile"])
;; prints the content of the tar file
(process/sh ["tar" "tvf" "docker.tar.gz"]
{:out *out*}))
(defn build-cmd!
[]
(c/invoke
build
{:op :ImageBuild
:params {:t "contajners-build-example"
:Content-type "application/x-tar"} ;; Add the header here.
:data (io/input-stream "docker.tar.gz")
:as :stream}))
(defn show-build-output!
[input-stream]
(let [stream-data (json/parsed-seq (io/reader input-stream))]
(loop [data stream-data]
(when-let [line (first data)]
(if-let [s (get line "stream")]
(do
(print s)
(flush))
(pprint/pprint line))
(recur (rest data))))))
(defn build!
[& {:keys [verbose?]}]
(let [docker-output-stream (build-cmd!)]
(when verbose?
(show-build-output! docker-output-stream))))
(->tar!)
(->build! {:verbose? true})
; (->build! :verbose? true) ;; using Clojure 1.11+
Thanks @davidpham87 for this example!
(def containers-docker (c/client {:engine :docker
:category :containers
:conn {:uri "unix:///var/run/docker.sock"
:version "v1.41"}}))
(c/invoke containers-docker {:op :ContainerCreate
:params {:name "conny"}
:data {:Image "busybox:musl"
:Cmd ["sh"
"-c"
"i=1; while :; do echo $i; sleep 1; i=$((i+1)); done"]}})
(c/invoke containers-docker {:op :ContainerStart
:params {:id "conny"}})
(def networks-docker (c/client {:engine :docker
:category :networks
:conn {:uri "unix:///var/run/docker.sock"}
:version "v1.41"}))
(c/invoke networks-docker {:op :NetworkCreate
:data {:Name "conny-network"}})
; 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 (c/invoke containers-docker {: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.
Note: :as :socket
applies only to the JVM runtime.
;; 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 (c/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.
Thanks @AustinC for this example.
(ns dclj.core
(:require [contajners.core :as c]
[cheshire.core :as json])
(:import [java.util Base64]))
(defn b64-encode
[to-encode]
(.encodeToString (Base64/getEncoder) (.getBytes to-encode)))
(def auth
(-> {"username" "un"
"password" "pw"
"serveraddress" "docker.acme.com"}
json/encode
b64-encode))
(def images
(c/client {:engine :docker
:category :images
:conn {:uri "unix:///var/run/docker.sock"}
:version "v1.41"}))
(c/invoke images
{:op :ImageCreate
:params {:fromImage "docker.acme.com/eg:2.1.995"
:X-Registry-Auth auth}
:throw-exceptions true})
Since both https and unix sockets are suppported, and generally docker deamons exposed over HTTPS are protected via mTLS, here is an example using mTLS to connect to docker via HTTPS:
;; Create a client using https
;; The ca.pem, key.pem and cert.pem are produced by the docker daemon when protected via mTLS
(def http-tls-ping
(c/client {:category :_ping
:engine :docker
:version "v1.41"
:conn {:uri "https://my.remote.docker.host:8000"
:mtls {:ca "ca.pem"
:key "key.pem"
:cert "cert.pem"}}}))
(invoke http-tls-ping {:op :SystemPing}) ;; => Returns "OK"
The caveat here is password protected PEM files aren't supported yet. Please raise an issue if there is a need for it.
There are cases where you may need to send multiple values to a single param, for example:
Whenever the docs mention about an Array of <type>
you can send a collection of those values to that param.
(c/invoke containers-podman
{:op :ContainersStatsAllLibpod
:params {:containers ["id1" "name1" "id2"]}})
Thanks @leahneukirchen for this!
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 request
from either the contajners.jvm-runtime
or contajners.sci-runtime
ns. The caveat is the response will be totally raw(data, stream or the socket itself).
Warning: fns from the impl
and rt
ns are not guaranteed to have a stable API as they are internal.
client method path headers query-params body as throw-exceptions throw-entire-message
request
takes the following params as a map:
invoke
. Default: :data
.(require
#?(:bb '[contajners.sci-runtime :as rt]
:clj '[contajners.jvm-runtime :as rt]))
;; This is the undocumented API in the Docker Daemon.
;; See https://github.com/moby/moby/pull/22049/files#diff-8038ade87553e3a654366edca850f83dR11
(rt/request {:client (rt/client "unix:///var/run/docker.sock" {})
:path "/v1.41/containers/conny/checkpoints"
:method :get})
More examples of low level calls:
;; Ping the server
(rt/request {:client (rt/client "unix:///var/run/docker.sock" {})
:path "/v1.41/_ping"
:method :get})
;; Copy a folder to a container
(rt/request {:client (rt/client "unix:///var/run/docker.sock" {})
:method :put
:path "/v1.41/containers/conny/archive"
:query-params {:path "/root/src"}
:body (-> "src.tar.gz"
io/file
io/input-stream)})
When :throw-exceptions
is passed as true
and the :as
is set to :stream
, to read the response stream, pass throw-entire-message
as true
to the invoke. The stream is available as :body
in the ex-data of the exception. throw-entire-message
is only applicable to the JVM runtime.
(try
(invoke containers-docker
{:op :ContainerArchive
:params {:id "conny"
:path "/this-does-not-exist"}
:as :stream
:throw-exceptions true
:throw-entire-message true})
(catch Exception e
(-> e ex-data :body slurp println))) ; Prints out the body of error from docker.
And anything else is possible!
contajners has been designed keeping in mind that support for newer and upcoming engines should be simple enough to add. The only requirement from the engine is that it must expose a REST API and has some form of Swagger/OpenAPI docs available.
Currently only Swagger 2.0 parsing is there as thats whats necessary now however its easy to add support for OpenAPI 3.0+. Please raise an issue when necessary.
fetch_api/main.clj
sources
map add the name of the engine as a key
:url
template from where the api yaml can be downloadeddoc-url
template where the doc of a version and OperationId can be seen:versions
which would be used in the template urls:namespace
which makes the category available namespaced, for example :libpod/containers
for podman and :containers
for docker. This is useful for APIs exposing similar functionality but are compatible with other engines. Like Podman is with the Docker API.clojure -X:fetch-api
from the root of the repo to download and parse all the APIs into the optimized edn files in resources/contajners/<the engine name>/<version>.edn
contajners.core/client
the new engine should be available and can pass it as :the engine name
as a keyword. Similarly contajners.core/categories
should work as wellCan you improve this documentation? These fine people already did:
Rahul De, David Pham, david, Leah Neukirchen & Imdad AhmedEdit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close