├── .gitmodules ├── CHANGELOG.md ├── lein-jib-build-test ├── src │ └── lein_jib_build_test │ │ └── core.clj ├── README.md └── project.clj ├── .gitignore ├── LICENSE ├── project.clj ├── src └── leiningen │ ├── aws_ecr_auth.clj │ └── jib_build.clj └── README.md /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "jib"] 2 | path = jib 3 | url = git@github.com:vehvis/jib.git 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.2.0] - 2019-11-03 4 | ### Changed 5 | - Lots of cleanup. 6 | - Registry authentication 7 | - AWS ECR support 8 | 9 | -------------------------------------------------------------------------------- /lein-jib-build-test/src/lein_jib_build_test/core.clj: -------------------------------------------------------------------------------- 1 | (ns lein-jib-build-test.core 2 | (:gen-class)) 3 | 4 | (defn -main 5 | "I don't do a whole lot ... yet." 6 | [& args] 7 | (println "Hello, World!")) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pom.xml 2 | pom.xml.asc 3 | *.jar 4 | *.class 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | /**/target/** 9 | /checkouts/ 10 | .lein-* 11 | .nrepl-port 12 | .cpcache/ 13 | .gradle/ 14 | .idea/ 15 | *.iml 16 | *.iws 17 | *.log 18 | .cpcache/ 19 | -------------------------------------------------------------------------------- /lein-jib-build-test/README.md: -------------------------------------------------------------------------------- 1 | # lein-jib-build sample project 2 | 3 | Sample project to demonstrate lein-jib-build 4 | 5 | ## Usage 6 | 7 | Install lein and docker first. 8 | 9 | $ lein do uberjar, jib-build 10 | 11 | You should now have a brand new container in your local docker repo: 12 | 13 | $ docker run lein-jib-build-test 14 | Hello, World! 15 | 16 | 17 | ## License 18 | 19 | Apache 2.0. See ../LICENSE 20 | 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019 Ville Vehviläinen 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /lein-jib-build-test/project.clj: -------------------------------------------------------------------------------- 1 | (defproject vaik.io/lein-jib-build-test "0.1.1-SNAPSHOT" 2 | :description "lein-jib-build sample project" 3 | :license {:name "Apache 2.0" 4 | :url "http://www.apache.org/licenses/LICENSE-2.0"} 5 | :dependencies [[org.clojure/clojure "1.10.1"]] 6 | 7 | :main lein-jib-build-test.core 8 | 9 | :plugins [[vaik.io/lein-jib-build "0.2.1-SNAPSHOT"]] 10 | :jib-build/build-config {:base-image {:type :registry 11 | :image-name "gcr.io/distroless/java"} 12 | :target-image {:type :docker 13 | :image-name "helloworld"}}) 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject vaik.io/lein-jib-build "0.2.1-SNAPSHOT" 2 | :description "Creates a docker container out of your uberjar without needing Docker." 3 | :url "https://github.com/vehvis/lein-jib-build" 4 | :license {:name "Apache 2.0" 5 | :url "http://www.apache.org/licenses/LICENSE-2.0"} 6 | 7 | :resource-paths ["lib"] 8 | 9 | :dependencies [[org.clojure/clojure "1.10.1"] 10 | [com.google.cloud.tools/jib-core "0.12.1-SNAPSHOT-GUAVASHADOW"] 11 | [com.cognitect.aws/api "0.8.391"] 12 | [com.cognitect.aws/endpoints "1.1.11.664"] 13 | [com.cognitect.aws/ecr "762.2.557.0"] 14 | [com.cognitect.aws/sts "747.2.533.0"]] 15 | 16 | :eval-in-leiningen true) 17 | -------------------------------------------------------------------------------- /src/leiningen/aws_ecr_auth.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.aws-ecr-auth 2 | (:require [cognitect.aws.client.api :as aws] 3 | [cognitect.aws.credentials :as credentials] 4 | [clojure.string :as str] 5 | [leiningen.core.main :as lein] 6 | [cognitect.aws.util]) 7 | (:import [java.util Base64] 8 | (java.io ByteArrayInputStream))) 9 | 10 | (defn decode-base64 [to-decode] 11 | (String. (.decode (Base64/getDecoder) to-decode))) 12 | 13 | (defn assumed-role-credentials-provider [role-arn session-name refresh-every-n-seconds] 14 | (credentials/auto-refreshing-credentials 15 | (reify credentials/CredentialsProvider 16 | (fetch [_] 17 | ;; The following is a workaround for a XML-parsing issue when using 18 | ;; the older clojure.data.xml bundled with Leiningen 19 | (with-redefs [cognitect.aws.util/xml-read 20 | (fn [s] (clojure.data.xml/parse (ByteArrayInputStream. (.getBytes ^String s "UTF-8")) 21 | :namespace-aware false))] 22 | 23 | (let [sts (aws/client {:api :sts}) 24 | sts-request {:op :AssumeRole 25 | :request {:RoleArn role-arn 26 | :RoleSessionName session-name}} 27 | sts-response (aws/invoke sts sts-request)] 28 | (lein/info "sts-resp" sts-response) 29 | (if-let [creds (:Credentials sts-response)] 30 | {:aws/access-key-id (:AccessKeyId creds) 31 | :aws/secret-access-key (:SecretAccessKey creds) 32 | :aws/session-token (:SessionToken creds) 33 | ::credentials/ttl refresh-every-n-seconds} 34 | (throw (ex-info (str "Unable to gain STS temporary credentials:" sts-response) sts-response))))))))) 35 | 36 | 37 | (defn get-client [api credentials-config] 38 | (if-let [crp (case (:type credentials-config) 39 | :assume-role 40 | (assumed-role-credentials-provider (:role-arn credentials-config) "session" 600) 41 | 42 | :access-key 43 | (credentials/basic-credentials-provider (select-keys credentials-config [:access-key-id :secret-access-key])) 44 | 45 | :system-properties 46 | (credentials/system-property-credentials-provider) 47 | 48 | :profile 49 | (credentials/profile-credentials-provider (:profile-name credentials-config)) 50 | 51 | :environment 52 | (credentials/environment-credentials-provider))] 53 | (aws/client {:api api :credentials-provider crp}) 54 | (aws/client {:api api}))) 55 | 56 | (defn ecr-auth [credentials-config] 57 | (lein/debug "Generating ECR authorization from" (str credentials-config)) 58 | (let [ecr-response (aws/invoke (get-client :ecr credentials-config) 59 | {:op :GetAuthorizationToken})] 60 | (if-let [authz (get-in ecr-response 61 | [:authorizationData 0])] 62 | (let [[user pass] (str/split (decode-base64 (:authorizationToken authz)) #":")] 63 | {:username user 64 | :password pass 65 | :endpoint (:proxyEndpoint authz)}) 66 | (throw (ex-info (str "Cannot generate ECR authorization credentials: " ecr-response) ecr-response))))) 67 | 68 | -------------------------------------------------------------------------------- /src/leiningen/jib_build.clj: -------------------------------------------------------------------------------- 1 | (ns leiningen.jib-build 2 | (:import [com.google.cloud.tools.jib.api Jib 3 | DockerDaemonImage 4 | Containerizer 5 | TarImage 6 | RegistryImage 7 | AbsoluteUnixPath 8 | ImageReference CredentialRetriever Credential] 9 | [java.io File] 10 | [java.util List ArrayList Optional] 11 | [java.nio.file Paths]) 12 | (:require [leiningen.core.main :as lein] 13 | [leiningen.core.classpath :as lein-cp] 14 | [leiningen.uberjar :as uberjar] 15 | [leiningen.jar :as jar] 16 | [clojure.pprint :as pprint] 17 | [leiningen.core.project :as project])) 18 | 19 | (def default-base-image {:type :registry 20 | :image-name "gcr.io/distroless/java"}) 21 | (def default-entrypoint ["java" "-jar"]) 22 | 23 | 24 | (defn- into-list 25 | [& args] 26 | (ArrayList. ^List args)) 27 | 28 | (defn- get-path [filename] 29 | (Paths/get (.toURI (File. ^String filename)))) 30 | 31 | (defn- to-imgref [image-config] 32 | (ImageReference/parse (:image-name image-config))) 33 | 34 | (defn add-registry-credentials [rimg registry-config] 35 | (cond 36 | (:username registry-config) 37 | (do (lein/debug "Using username/password authentication, user:" (:username registry-config)) 38 | (.addCredential rimg (:username registry-config) (:password registry-config))) 39 | 40 | (:authorizer registry-config) 41 | (let [auth (:authorizer registry-config)] 42 | (lein/debug "Using custom registry authentication:" (:authorizer registry-config)) 43 | (.addCredentialRetriever rimg (reify CredentialRetriever 44 | (retrieve [_] 45 | (require [(symbol (namespace (:fn auth)))]) 46 | (let [creds (eval `(~(:fn auth) ~(:args auth)))] 47 | (Optional/of (Credential/from (:username creds) (:password creds)))))))) 48 | 49 | :default rimg)) 50 | 51 | 52 | 53 | (defmulti configure-image (fn [image-config project] (:type image-config))) 54 | 55 | (defmethod configure-image :tar [{:keys [image-name]} project] 56 | (let [image-name (or image-name (str "target/" (:name project) ".tar"))] 57 | (lein/debug "Tar image:" image-name) 58 | (.named (TarImage/at (-> (File. ^String image-name) 59 | .toURI 60 | Paths/get)) 61 | ^String image-name))) 62 | 63 | (defmethod configure-image :registry [{:keys [image-name] :as image-config} project] 64 | (let [image-name (or image-name (:name project))] 65 | (lein/debug "Registry image:" image-name) 66 | (-> (RegistryImage/named ^ImageReference (to-imgref image-config)) 67 | (add-registry-credentials image-config)))) 68 | 69 | (defmethod configure-image :docker [{:keys [image-name] :as image-config} project] 70 | (let [image-name (or image-name (:name project))] 71 | (lein/debug "Local docker:" image-name) 72 | (DockerDaemonImage/named ^ImageReference (to-imgref image-config)))) 73 | 74 | (defmethod configure-image :default [image-config _] 75 | (throw (Exception. ^String (str "Unknown image type: " (:image-name image-config))))) 76 | 77 | (defn jib-build 78 | "It places the jar in the container (or else it gets the hose again)." 79 | [project & args] 80 | #_(pprint/pprint (lein-cp/ext-classpath project)) 81 | #_(pprint/pprint args) 82 | (let [project (project/merge-profiles project [:uberjar]) 83 | config (:jib-build/build-config project) 84 | standalone-jar (jar/get-jar-filename project :standalone) 85 | base-image (get config :base-image default-base-image) 86 | entrypoint (get config :entrypoint default-entrypoint) 87 | arguments (get config :arguments (.toString (.getFileName (get-path standalone-jar)))) 88 | app-layer [(into-list (get-path standalone-jar)) 89 | (AbsoluteUnixPath/get "/")]] 90 | (lein/info "Building container upon" (:image-name base-image) "with" standalone-jar) 91 | (-> (Jib/from (configure-image base-image project)) 92 | (.addLayer (first app-layer) (second app-layer)) 93 | (.setEntrypoint (apply into-list entrypoint)) 94 | (.setProgramArguments (into-list arguments)) 95 | (.containerize (Containerizer/to (configure-image (:target-image config) project)))))) 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lein-jib-build 2 | 3 | Build docker containers with Leiningen, no docker installation needed. Uses Google's [Jib](https://github.com/GoogleContainerTools/jib) toolkit. 4 | 5 | >**Note!** This is alpha quality code and has not been thoroughly tested. Use at your own discretion (and create a PR if you improve something) 6 | 7 | ## Requirements and caveats 8 | 9 | Clojure 1.10.0 or later is needed. Leiningen 2.9.0 or later is needed. This will probably work with Java 8 but 10 | I've only tested with Java 11. 11 | 12 | Docker is required only if you want to use images from or deploy images to your local docker repository. Remote registries 13 | and tarfiles work without Docker. 14 | 15 | **Your project must emit an uberjar** or something closely like it for this plugin to be useful in its current state. 16 | 17 | ## Usage 18 | 19 | The plugin is available on clojars: https://clojars.org/vaik.io/lein-jib-build 20 | 21 | Configure your project.clj as follows: 22 | 23 | ```clojure 24 | :plugins [[vaik.io/lein-jib-build "0.2.0"]] 25 | :jib-build/build-config {:base-image {:type :registry 26 | :image-name "gcr.io/distroless/java"} 27 | :target-image {:type :docker 28 | :image-name "helloworld"}} 29 | 30 | ``` 31 | 32 | Build the image and deploy it to the local docker repo: 33 | 34 | $ lein do uberjar, jib-build 35 | 36 | Now you can run it: 37 | 38 | $ docker run helloworld 39 | Hello, world! 40 | 41 | There's also an [example project](https://github.com/vehvis/lein-jib-build/tree/master/lein-jib-build-test) you can take a look at. 42 | 43 | 44 | ## Configuration options 45 | 46 | The `:jib-build/build-config` map has the following required options: 47 | * `:target-image {...}` - what to do with the built image, also see below 48 | 49 | The following are optional: 50 | * `:base-image {...}` - base Docker image to build upon, see below for details. Defaults to `gcr.io/distroless/java`. 51 | * `:entrypoint [...]` - a vector of strings to use as the container's ENTRYPOINT value, defaults to `["java" "-jar"]` 52 | * `:arguments "..."` - a string to give to the entrypoint as arguments, defaults to the name of the project's uberjar. 53 | 54 | #### Referring to images 55 | 56 | These options are usable with both `base-image` and `target-image`. 57 | 58 | ```clojure 59 | ;; Deploy to (or build upon an image from) your local docker daemon (requires dockerd to be running) 60 | :target-image {:type :docker 61 | :image-name "helloworld"} 62 | ``` 63 | ```clojure 64 | ;; Deploy as (or build upon) a tar archive 65 | :target-image {:type :tar 66 | :image-name "target/helloworld.tar"} 67 | ``` 68 | ```clojure 69 | ;; Deploy to (or build upon an image from) a Docker registry with optional username/password authentication 70 | ;; Please mind your security! 71 | :target-image {:type :registry 72 | :image-name "repository.mordor.me/sauron/helloworld" 73 | :username "sauron" ;; optional 74 | :password "VERYSECRET" ;; optional 75 | } 76 | ``` 77 | 78 | #### Pull from or push to AWS ECR, with authentication 79 | 80 | If you're using AWS ECR there's direct support for more sophisticated authentication. 81 | 82 | Deploy to ECR with assume-role (uses the standard AWS credential chain): 83 | 84 | ```clojure 85 | :target-image {:type :registry 86 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 87 | :authorizer {:fn leiningen.aws-ecr-auth/ecr-auth 88 | :args {:type :assume-role 89 | :role-arn "arn:aws:iam::123456789:role/nazgul"}}} 90 | ``` 91 | 92 | Deploy to ECR using a specific profile: 93 | 94 | ```clojure 95 | :target-image {:type :registry 96 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 97 | :authorizer {:fn leiningen.aws-ecr-auth/ecr-auth 98 | :args {:type :profile 99 | :profile-name "nazgul"}}} 100 | ``` 101 | 102 | Using environment variables: 103 | * `AWS_ACCESS_KEY_ID` (required) 104 | * `AWS_SECRET_ACCESS_KEY` (required) 105 | * `AWS_SESSION_TOKEN` (optional) 106 | ```clojure 107 | :target-image {:type :registry 108 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 109 | :authorizer {:fn leiningen.aws-ecr-auth/ecr-auth 110 | :args {:type :environment}}} 111 | ``` 112 | 113 | With an access key: 114 | ```clojure 115 | :target-image {:type :registry 116 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 117 | :authorizer {:fn leiningen.aws-ecr-auth/ecr-auth 118 | :args {:type :access-key 119 | :access-key-id "AK1231232414" 120 | :secret-access-key "111111111111111"}}} 121 | ``` 122 | 123 | With JVM system properties: 124 | * `aws.accessKeyId` (required) 125 | * `aws.secretKey` (required) 126 | 127 | ```clojure 128 | :target-image {:type registry 129 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 130 | :authorizer {:fn leiningen.aws-ecr-auth/ecr-auth 131 | :args {:type :system-properties}}} 132 | ``` 133 | 134 | #### Using a custom registry authorizer 135 | 136 | Nothing prevents you from making a custom authorizer just like the ECR thing above. 137 | Create a function in a namespace accessible to leiningen with a single argument, and 138 | have it return a username/password map: 139 | 140 | ```clojure 141 | (defn custom-authorizer [config] 142 | {:username (:username config) 143 | :password (apply str (reverse (:encrypted-password config))}) 144 | ``` 145 | 146 | The function gets passed the `args` map from your `project.clj`: 147 | 148 | ```clojure 149 | :target-image {:type :registry 150 | :image-name "123456789.dkr.ecr.mordor-east-1.amazonaws.com/helloworld" 151 | :authorizer {:fn my-namespace/my-custom-authorizer 152 | :args {:username "Sauron" :encrypted-password "TERCESYREV"}}} 153 | ``` 154 | 155 | ## Building (& other irritations) 156 | 157 | So that things would not be too easy, Leiningen includes some libraries that are a bit long in the tooth. 158 | And when creating plugins, those libraries override everything you bring with you. 159 | 160 | The main culprit in this case is Guava, a current version of which is required by the `jib-core` library used 161 | by this plugin. Leiningen however provides an old version, so we need to shadow the Guava library inside jib-core 162 | for it to function. 163 | 164 | I have placed a forked version of `jib` as a Git submodule, which contains the required shadowing configuration. 165 | 166 | The included build.sh script does everything that's needed: 167 | 168 | ```shell script 169 | $ ./build.sh 170 | --- Checking that we have the required submodule 171 | --- Build the customised jib-core 172 | BUILD SUCCESSFUL in 2s 173 | 4 actionable tasks: 2 executed, 2 up-to-date 174 | 'jib/jib-core/build/libs/jib-core-0.12.1-SNAPSHOT-GUAVASHADOW.jar' -> 'lib/jib-core-0.12.1-SNAPSHOT-GUAVASHADOW.jar' 175 | --- Now build the plugin 176 | Created /..../lein-jib-build/target/lein-jib-build-0.2.0.jar 177 | Wrote /..../lein-jib-build/pom.xml 178 | Installed jar and pom into local repo. 179 | ``` 180 | 181 | Another similar thing is that Leiningen includes an old version of `clojure.data.xml`, which is slightly incompatible 182 | with the `cognitect/aws-api` library used for ECR authentication. There's an ugly `with-redefs` somewhere because 183 | of that. 184 | 185 | ## License 186 | 187 | Copyright 2019 Ville Vehviläinen 188 | 189 | Licensed under the Apache License, Version 2.0 (the "License"); 190 | you may not use this file except in compliance with the License. 191 | You may obtain a copy of the License at 192 | 193 | http://www.apache.org/licenses/LICENSE-2.0 194 | 195 | Unless required by applicable law or agreed to in writing, software 196 | distributed under the License is distributed on an "AS IS" BASIS, 197 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 198 | See the License for the specific language governing permissions and 199 | limitations under the License. 200 | --------------------------------------------------------------------------------