├── doc └── intro.md ├── .gitignore ├── .travis.yml ├── project.clj ├── test └── consul │ ├── watch_test.clj │ ├── core_test.clj │ └── txn_test.clj ├── src └── consul │ ├── txn.clj │ ├── watch.clj │ └── core.clj ├── README.md └── LICENSE /doc/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction to consul-clojure 2 | 3 | TODO: write [great documentation](http://jacobian.org/writing/what-to-write/) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | dist: trusty 3 | before_script: 4 | - curl -v --tlsv1.2 --compressed -o consul_0.7.2_linux_amd64.zip 'https://releases.hashicorp.com/consul/0.7.2/consul_0.7.2_linux_amd64.zip' 5 | - unzip "consul_0.7.2_linux_amd64.zip" 6 | - ./consul --version 7 | script: 8 | - ./consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul & 9 | - lein test 10 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject consul-clojure "0.7.2" 2 | :description "A Consul client for Clojure applications." 3 | :url "http://github.com/bpoweski/consul-clojure" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[camel-snake-kebab "0.3.1" :exclusions [org.clojure/clojure com.keminglabs/cljx]] 7 | [cheshire "5.5.0"] 8 | [clj-http-lite "0.3.0" :exclusions [org.clojure/clojure]]] 9 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.7.0"] 10 | [org.clojure/core.async "0.1.346.0-17112a-alpha"]]}}) 11 | -------------------------------------------------------------------------------- /test/consul/watch_test.clj: -------------------------------------------------------------------------------- 1 | (ns consul.watch-test 2 | (:require [consul.watch :as w] 3 | [consul.core :as consul] 4 | [clojure.core.async :as async] 5 | [clojure.test :refer :all])) 6 | 7 | 8 | (defn cleanup [f] 9 | (consul/kv-del :local "consul-clojure" {:recurse? true}) 10 | (f) 11 | (consul/kv-del :local "consul-clojure" {:recurse? true})) 12 | 13 | (use-fixtures :each cleanup) 14 | 15 | (defn try [] transient f persistent!) 35 | response (core/consul conn :put [:txn] {:body operations 36 | :query-params (build-params params)})] 37 | (map (fn [result] (core/kv-map-convert (:KV result) string?)) 38 | (get-in response [:body :Results]))))) 39 | 40 | (defn kv-set 41 | "Sets the key to the given value" 42 | [tx key value & {:keys [flags] :as optional}] 43 | (let [base {:Value (core/str->base64 value)} 44 | params (if flags (assoc base :Flags flags) base) 45 | op (build-operation :KV "set" key params)] 46 | (conj! tx op))) 47 | 48 | (defn kv-set-cas 49 | "Sets the key ot the given value with check-and-set semantics. 50 | The key will only be set if its current modify index matches the 51 | supplied index" 52 | [tx key value index & {:keys [flags] :as optional}] 53 | (let [base {:Value (core/str->base64 value) 54 | :Index index} 55 | params (if flags (assoc base :Flags flags) base) 56 | op (build-operation :KV "cas" key params)] 57 | (conj! tx op))) 58 | 59 | (defn kv-lock 60 | "Unlocks the key with the given Session. The key will only release 61 | the lock if the session is valid and currently has it locked." 62 | [tx key value session & {:keys [flags] :as optional}] 63 | (let [base {:Value (core/str->base64 value) 64 | :Session session} 65 | params (if flags (assoc base :Flags flags) base) 66 | op (build-operation :KV "lock" key params)] 67 | (conj! tx op))) 68 | 69 | (defn kv-unlock 70 | "Gets the key during the transaction. This fails the transaction if 71 | the key doesn't exist. The key may not be present in the results if 72 | ACLs do not permit it to be read." 73 | [tx key value session & {:keys [flags] :as optional}] 74 | (let [base {:Value (core/str->base64 value) 75 | :Session session} 76 | params (if flags (assoc base :Flags flags) base) 77 | op (build-operation :KV "unlock" key params)] 78 | (conj! tx op))) 79 | 80 | (defn kv-get 81 | "Gets the key during the transaction. This fails the transaction if 82 | the key doesn't exist. The key may not be present in the results if 83 | ACLs do not permit it to be read." 84 | [tx key] 85 | (conj! tx (build-operation :KV "get" key nil))) 86 | 87 | (defn kv-get-tree 88 | "Gets all keys with a prefix of key during the transaction. This does 89 | not fail the transaction if the key doesn't exist. Not all keys may be 90 | present in the results if ACLs do not permit them to be read." 91 | [tx prefix] 92 | (conj! tx (build-operation :KV "get-tree" prefix nil))) 93 | 94 | (defn kv-check-index 95 | "Fails the transaction if key does not have a modify index equal to 96 | supplied index." 97 | [tx key index] 98 | (conj! tx (build-operation :KV "check-index" key {:Index index}))) 99 | 100 | (defn kv-check-session 101 | "Fails the transaction if key is not currently locked by session." 102 | [tx key session] 103 | (conj! tx (build-operation :KV "check-session" key {:Session session}))) 104 | 105 | (defn kv-delete 106 | "Deletes the key." 107 | [tx key] 108 | (conj! tx (build-operation :KV "delete" key nil))) 109 | 110 | (defn kv-delete-tree 111 | "Deletes all keys with a prefix of key." 112 | [tx prefix] 113 | (conj! tx (build-operation :KV "delete-tree" prefix nil))) 114 | 115 | (defn kv-delete-cas 116 | "Deletes the key with check-and-set semantics. The key will only 117 | be deleted if its current modify index matches the supplied index." 118 | [tx key index] 119 | (conj! tx (build-operation :KV "delete-cas" key {:Index index}))) 120 | -------------------------------------------------------------------------------- /test/consul/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns consul.core-test 2 | (:require [clojure.test :refer :all] 3 | [consul.core :refer :all]) 4 | (:import (java.util UUID))) 5 | 6 | 7 | (deftest endpoint->path-test 8 | (testing "Key/Value store" 9 | (are [path endpoint] (= path (endpoint->path endpoint)) 10 | "/v1/kv/key" [:kv "key"] 11 | "/v1/agent/services" [:agent :services] 12 | "/v1/agent/join/10.1.1.2" [:agent :join "10.1.1.2"]))) 13 | 14 | (deftest base64-test 15 | (is (= "bar" (base64->str "YmFy")))) 16 | 17 | (deftest consul-request-test 18 | (testing ":local conn" 19 | (is (= 8500 (:server-port (consul-request :local :get [:agent :checks])))) 20 | (is (= "127.0.0.1" (:server-name (consul-request :local :get [:agent :checks])))) 21 | (is (= :http (:scheme (consul-request :local :get [:agent :checks])))) 22 | (is (= "/v1/kv/key" (:uri (consul-request :local :get [:kv "key"])))) 23 | (is (= "/v1/kv/foo" (:uri (consul-request :local :get "/v1/kv/foo"))))) 24 | (testing "maps are merged into local-conn" 25 | (is (= :https (:scheme (consul-request {:scheme :https} :get [:agent :checks])))) 26 | (is (= 8501 (:server-port (consul-request {:scheme :https :server-name "10.0.37.4" :server-port 8501} :get [:agent :checks])))) 27 | (is (= :https (:scheme (consul-request {:scheme :https :server-name "10.0.37.4" :server-port 8501} :get [:agent :checks])))) 28 | (is (= "10.0.37.4" (:server-name (consul-request {:scheme :https :server-name "10.0.37.4" :server-port 8501} :get [:agent :checks])))))) 29 | 30 | (deftest map->check-test 31 | (let [m {:id "search" 32 | :name "Google" 33 | :notes "lies" 34 | :http "http://www.google.com" 35 | :interval "10s"}] 36 | (is (thrown? AssertionError (map->check {}))) 37 | (is (thrown? AssertionError (map->check (dissoc m :interval)))) 38 | (is (= (:ID (map->check m)) "search")) 39 | (is (= (:Name (map->check m)) "Google")) 40 | (is (= (:HTTP (map->check m)) "http://www.google.com")) 41 | (is (= (:Interval (map->check m)) "10s")) 42 | (is (= (:ServiceID (map->check (assoc m :service-id "redis-01"))) "redis-01")) 43 | (is (= (:TTL (map->check (-> m (dissoc :http :interval) (assoc :ttl "20s")))) "20s")))) 44 | 45 | (deftest ^{:integration true} consul-test 46 | (testing "with a connection failure" 47 | (are [v ks] (= v (get-in (ex-data (try (consul {:server-port 8501} :get [:kv "foo"]) (catch Exception err err))) ks)) 48 | 8501 [:http-request :server-port] 49 | :connect-failure [:reason]))) 50 | 51 | (deftest ^{:integration true} kv-store-test 52 | (let [k (str "consul-clojure." (UUID/randomUUID)) 53 | k-2 (str "consul-clojure." (UUID/randomUUID)) 54 | v (str (UUID/randomUUID))] 55 | (testing "basic kv-get operations" 56 | (is [k nil] (kv-get :local k)) 57 | (is (true? (kv-put :local k v))) 58 | (is (= {:key k :body v} (select-keys (kv-get :local k {:raw? true}) [:key :body]))) 59 | (is (= [k v] (kv (kv-get :local k {:string? true})))) 60 | (is (= [k (str->base64 v)] (kv (kv-get :local k {:string? false})))) 61 | (is (true? (kv-del :local k))) 62 | (is (= [k nil] (kv (kv-get :local k)))) 63 | (is (true? (kv-put :local k v)))) 64 | (testing "kv-keys" 65 | (is (= #{k} (:keys (kv-keys :local "consul-clojure")))) 66 | (is (nil? (:keys (kv-keys :local "x")))) 67 | (kv-put :local k-2 "x") 68 | (is (= #{k k-2} (:keys (kv-keys :local "consul-clojure"))))))) 69 | 70 | (deftest ^{:integration true} agent-test 71 | (testing "registering and removing checks" 72 | (let [check-id (str "consul-clojure.check." (UUID/randomUUID))] 73 | (is (nil? (get-in (agent-checks :local) [check-id]))) 74 | (is (true? (agent-register-check :local {:name "test-check" :interval "10s" :http "http://127.0.0.1:8500/ui/" :id check-id}))) 75 | (is (map? (get-in (agent-checks :local) [check-id]))) 76 | (is (true? (agent-deregister-check :local check-id))) 77 | (is (nil? (get-in (agent-checks :local) [check-id]))))) 78 | (testing "registering and deregistering a service" 79 | (let [service-id (str "consul-clojure.service." (UUID/randomUUID))] 80 | (is (nil? (get-in (agent-services :local) [service-id]))) 81 | (is (true? (agent-register-service :local {:name service-id}))) 82 | (is (map? (get-in (agent-services :local) [service-id]))) 83 | (is (true? (agent-deregister-service :local service-id))) 84 | (is (nil? (get-in (agent-services :local) [service-id]))))) 85 | (testing "registering a service with a ttl check" 86 | (let [service-id (str "consul-clojure.service.ttl." (UUID/randomUUID))] 87 | (is (true? (agent-register-service :local {:name service-id :check {:ttl "1s"}}))) 88 | (is (map? (get-in (agent-services :local) [service-id]))) 89 | (is (= 1 (count (filter (comp #{service-id} :service-id) (vals (agent-checks :local)))))))) 90 | (testing "registering a service with two ttl checks" 91 | (let [service-id (str "consul-clojure.service.ttl." (UUID/randomUUID))] 92 | (is (true? (agent-register-service :local {:name service-id :checks [{:ttl "1s"} {:ttl "5s"}]}))) 93 | (is (map? (get-in (agent-services :local) [service-id]))) 94 | (is (= 2 (count (filter (comp #{service-id} :service-id) (vals (agent-checks :local))))))))) 95 | 96 | (defn clean [] 97 | (kv-del :local "consul-clojure" {:recurse? true}) 98 | (doseq [service-id (filter #(re-seq #"^consul-clojure.*" %) (keys (agent-services :local)))] 99 | (agent-deregister-service :local service-id))) 100 | 101 | (defn cleanup [f] 102 | (clean) 103 | (f) 104 | (clean)) 105 | 106 | (use-fixtures :each cleanup) 107 | -------------------------------------------------------------------------------- /test/consul/txn_test.clj: -------------------------------------------------------------------------------- 1 | (ns consul.txn-test 2 | (:require [consul.txn :as txn] 3 | [consul.core :as core] 4 | [clojure.test :refer :all]) 5 | (:import (java.util UUID))) 6 | 7 | (defn clean [] 8 | (core/kv-del :local "consul-clojure" {:recurse? true}) 9 | (doseq [service-id (filter #(re-seq #"^consul-clojure.*" %) (keys (core/agent-services :local)))] 10 | (core/agent-deregister-service :local service-id))) 11 | 12 | (defn cleanup [f] 13 | (clean) 14 | (f) 15 | (clean)) 16 | 17 | (use-fixtures :each cleanup) 18 | 19 | (defn create-key [key] (str "consul-clojure." key)) 20 | 21 | (deftest ^{:integration true} kv-get-set-test 22 | (testing "basic get/set operations" 23 | (let [results (txn/put :local (fn [tx] 24 | (txn/kv-set tx (create-key "key") "value") 25 | (txn/kv-get tx (create-key "key")))) 26 | set-result (first results) 27 | get-result (last results)] 28 | (is (= 2 (count results))) 29 | (is (= (:create-index set-result) (:create-index get-result))) 30 | (is (= (:flags set-result) (:flags get-result))) 31 | (is (= "consul-clojure.key" (:key set-result) (:key get-result))) 32 | (is (= (:lock-index set-result) (:lock-index get-result))) 33 | (is (= (:modify-index set-result) (:modify-index get-result))) 34 | (is (nil? (:value set-result))) 35 | (is (= "value" (:value get-result)))) 36 | 37 | (let [result (first (txn/put :local (fn [tx] (txn/kv-set tx (create-key "key") "value" :flags 10))))] 38 | (is (= 10 (:flags result)))))) 39 | 40 | (deftest ^{:integration true} kv-cas-test 41 | (testing "check-and-set semantics with the correct index" 42 | (let [index (-> (txn/put :local (fn [tx] (txn/kv-set tx (create-key "key") "value"))) first :modify-index) 43 | result (-> (txn/put :local (fn [tx] (txn/kv-set-cas tx (create-key "key") "value" index))) first)] 44 | (is (integer? (:lock-index result))) 45 | (is (= "consul-clojure.key" (:key result))) 46 | (is (= 0 (:flags result))) 47 | (is (nil? (:value result))) 48 | (is (integer? (:create-index result))) 49 | (is (integer? (:modify-index result))) 50 | (is (not (= index (:modify-index result)))) 51 | (is (thrown-with-msg? Exception #"Transaction failure" 52 | (txn/put :local (fn [tx] (txn/kv-set-cas tx (create-key "key") "value" (- index 1))))))))) 53 | 54 | (deftest ^{:integration true} kv-lock-unlock-test 55 | (testing "locking and unlocking with a session" 56 | (let [session-key (core/session-create :local) 57 | results (txn/put :local (fn [tx] 58 | (txn/kv-lock tx (create-key "key") "value" session-key) 59 | (txn/kv-unlock tx (create-key "key") "value" session-key))) 60 | lock-result (first results) 61 | unlock-result (last results)] 62 | (is (= (:lock-index lock-result) (:lock-index unlock-result))) 63 | (is (= "consul-clojure.key" (:key lock-result) (:key unlock-result))) 64 | (is (= (:flags lock-result) (:flags unlock-result))) 65 | (is (= nil (:value lock-result) (:value unlock-result))) 66 | (is (= (:create-index lock-result) (:create-index unlock-result))) 67 | (is (= (:modify-index lock-result) (:modify-index unlock-result))) 68 | (is (thrown-with-msg? Exception #"Transaction failure" 69 | (txn/put :local (fn [tx] (txn/kv-lock tx (create-key "key") "value" "not-uuid"))))) 70 | (is (thrown-with-msg? Exception #"Transaction failure" 71 | (txn/put :local (fn [tx] (txn/kv-lock tx (create-key "key") "value" (str (UUID/randomUUID)))))))))) 72 | 73 | (deftest ^{:integration true} kv-get-tree-test 74 | (testing "getting all items with a prefix" 75 | (do (core/kv-put :local (create-key "prefix/key1") "value") 76 | (core/kv-put :local (create-key "prefix/key2") "value") 77 | (core/kv-put :local (create-key "other/key3") "value")) 78 | (let [results (txn/put :local (fn [tx] (txn/kv-get-tree tx (create-key "prefix/"))))] 79 | (is (= 2 (count results))) 80 | (is (= "consul-clojure.prefix/key1" (:key (first results)))) 81 | (is (= "consul-clojure.prefix/key2" (:key (last results))))))) 82 | 83 | (deftest ^{:integration true} kv-check-index-test 84 | (testing "checking the index" 85 | (let [index (:modify-index (first (txn/put :local (fn [tx] (txn/kv-set tx (create-key "key") "val"))))) 86 | result (first (txn/put :local (fn [tx] (txn/kv-check-index tx (create-key "key") index))))] 87 | (is (= index (:modify-index result))) 88 | (is (thrown-with-msg? Exception #"Transaction failure" 89 | (txn/put :local (fn [tx] (txn/kv-check-index tx (create-key "key") (+ 1 index))))))))) 90 | 91 | (deftest ^{:integration true} kv-check-session-test 92 | (testing "checking if the key is locked by a session" 93 | (let [session-key (core/session-create :local) 94 | results (txn/put :local (fn [tx] 95 | (txn/kv-lock tx (create-key "key") "value" session-key) 96 | (txn/kv-check-session tx (create-key "key") session-key)))] 97 | (is (= 2 (count results))) 98 | (is (thrown-with-msg? Exception #"Transaction failure" 99 | (txn/put :local (fn [tx] (txn/kv-check-session tx (create-key "key") (str (UUID/randomUUID)))))))))) 100 | 101 | (deftest ^{:integration true} kv-delete-test 102 | (testing "deletion of a key" 103 | (let [key (create-key "key")] 104 | (core/kv-put :local key "val") 105 | (let [del-result (txn/put :local (fn [tx] (txn/kv-delete tx key)))] 106 | (is (= 0 (count del-result))) 107 | (is (nil? (:value (core/kv-get :local key)))))))) 108 | 109 | (deftest ^{:integration true} kv-delete-tree-test 110 | (testing "deletion of multiple keys in a tree" 111 | (core/kv-put :local (create-key "prefix/key1") "val") 112 | (core/kv-put :local (create-key "prefix/key2") "val") 113 | (core/kv-put :local (create-key "other/key3") "val") 114 | (let [del-result (txn/put :local (fn [tx] (txn/kv-delete-tree tx (create-key "prefix/"))))] 115 | (is (= 0 (count del-result))) 116 | (is (nil? (:value (core/kv-get :local (create-key "prefix/key1"))))) 117 | (is (nil? (:value (core/kv-get :local (create-key "prefix/key2"))))) 118 | (is (= "val" (:value (core/kv-get :local (create-key "other/key3")))))))) 119 | 120 | (deftest ^{:integration true} kv-delete-cas-test 121 | (testing "deletion of a key with a specified index" 122 | (let [index (-> (txn/put :local (fn [tx] (txn/kv-set tx (create-key "key") "value"))) first :modify-index) 123 | result (-> (txn/put :local (fn [tx] (txn/kv-delete-cas tx (create-key "key") index))) first)] 124 | (is (= 0 (count result))) 125 | (is (nil? (:value (core/kv-get :local (create-key "key")))))) 126 | (let [index (-> (txn/put :local (fn [tx] (txn/kv-set tx (create-key "key") "value"))) first :modify-index)] 127 | (is (thrown-with-msg? Exception #"Transaction failure" 128 | (-> (txn/put :local (fn [tx] (txn/kv-delete-cas tx (create-key "key") (+ 1 index)))) first)))))) 129 | 130 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # consul-clojure, a Consul client for Clojure 2 | 3 | [Consul](https://www.consul.io) is an awesome service discovery and configuration provider. 4 | 5 | 6 | ## Changelog 7 | 8 | ### 0.7.2 9 | 10 | > This is a **minor update** 11 | 12 | ```clojure 13 | [consul-clojure "0.7.1"] 14 | ``` 15 | 16 | * [wkoelewijn] Fixes issue with TTL check 17 | 18 | ### 0.7.1 19 | 20 | > This is a **minor update** 21 | 22 | ```clojure 23 | [consul-clojure "0.7.1"] 24 | ``` 25 | * [pimeys] Fixed issue where bytes were serialized using the platform encoding instead of UTF-8 26 | 27 | ### 0.7.0 28 | 29 | > Consul 0.7 Features 30 | 31 | * Consul 0.7 KV transactions courtesy of @pimeys 32 | 33 | ### 0.6.0 34 | 35 | > This is a **major update** that **may be BREAKING**. 36 | 37 | ```clojure 38 | [consul-clojure "0.6.0"] 39 | ``` 40 | 41 | * Pulled in the following changes from a fork 42 | * **BREAK**: Dropped use of metadata for tracking various values 43 | * **BREAK**: No more keyword args in favor of maps 44 | * **BREAK**: Dropped the dependency on com.stuartsierra.component and components in favor of fns 45 | * **FIX**: NPE on first :failures increment 46 | * Add leader watch 47 | 48 | 49 | ### 0.1.0 50 | 51 | > Initial release 52 | 53 | ```clojure 54 | [consul-clojure "0.1.0"] ;; initial 55 | ``` 56 | 57 | ## Goals 58 | 59 | Provide a useful library for building Consul aware Clojure applications. 60 | 61 | ## Getting Started 62 | 63 | First, you'll need to setup a local consul agent. See [setup instructions](https://www.consul.io/intro/getting-started/install.html). It's much easier to see what's happening by using the consul-web-ui. 64 | 65 | This is the LaunchAgent configuration I use for OSX after installing `consul` and `consul-web-ui`. 66 | 67 | ```xml 68 | 69 | 70 | 71 | 72 | KeepAlive 73 | 74 | Label 75 | hashicorp.consul.server 76 | ProgramArguments 77 | 78 | /usr/local/bin/consul 79 | agent 80 | -dev 81 | -bind=127.0.0.1 82 | -data-dir=/usr/local/var/consul 83 | -config-dir=/usr/local/etc/consul.d 84 | -ui-dir=/usr/local/Cellar/consul/0.6.4/share/consul/web-ui 85 | 86 | RunAtLoad 87 | 88 | UserName 89 | bpoweski 90 | WorkingDirectory 91 | /usr/local/var 92 | StandardErrorPath 93 | /usr/local/var/log/consul.log 94 | StandardOutPath 95 | /usr/local/var/log/consul.log 96 | 97 | 98 | ``` 99 | 100 | ### Connections 101 | 102 | Given much of the time one is interacting with a local consul agent, the keyword `:local` can be used to assume the defaults. In those other cases 103 | where someone has gotten "creative" in the deployment of consul or you need to connect to a remote server, you can use a `clj-http-lite` compatible request maps. 104 | 105 | ```clojure 106 | {:server-name "127.0.0.1" :server-port 8500} 107 | ``` 108 | 109 | ### Key/Value store 110 | 111 | When using the Key/Value store endpoints directly, one has to contend with a variety of different response formats depending upon the parameters passed. 112 | Rather than continue with this approach, different functions are used instead. 113 | 114 | Getting a key: 115 | 116 | ```clojure 117 | (require '[consul.core :as consul]) 118 | 119 | (consul/kv-get :local "my-key") 120 | => ["my-key" nil] 121 | ``` 122 | 123 | But what about the consul index information? 124 | 125 | ```clojure 126 | (meta (consul/kv-get :local "my-key")) 127 | => {:x-consul-lastcontact "0", :known-leader true, :modify-index 352} 128 | ``` 129 | 130 | Setting a value: 131 | 132 | ```clojure 133 | (consul/kv-put :local "my-key" "a") 134 | => true 135 | ``` 136 | 137 | Want a list of keys? 138 | 139 | ```clojure 140 | (consul/kv-keys :local "my") 141 | => #{"my-key"} 142 | ``` 143 | 144 | Don't want a key? 145 | 146 | ```clojure 147 | (consul/kv-del :local "my-key") 148 | => true 149 | ``` 150 | 151 | Let's remove more than one: 152 | 153 | ```clojure 154 | (consul/kv-put :local "key-1" "a") 155 | => true 156 | (consul/kv-put :local "key-2" nil) 157 | => true 158 | (consul/kv-keys :local "key") 159 | => #{"key-1" "key-2"} 160 | 161 | (consul/kv-del :local "key" :recurse? true) 162 | => true 163 | (consul/kv-keys :local "key") 164 | => #{} 165 | ``` 166 | 167 | ## Atomic transactions (available in Consul 0.7) 168 | 169 | The txn library allows executing multiple operations in an atomic transaction. All operations 170 | in the txn Consul 0.7 txn documentation are supported https://www.consul.io/docs/agent/http/kv.html 171 | 172 | The result of a transaction is always either a list of results, or an exception if the transaction 173 | was not successful. 174 | 175 | ```clojure 176 | (require '[consul.txn :as txn]) 177 | 178 | (txn/put :local (fn [tx] (txn/kv-set tx "key" "val") 179 | (txn/kv-get tx "key"))) 180 | 181 | => ({:create-index 3296, 182 | :flags 0, 183 | :key "key", 184 | :lock-index 0, 185 | :modify-index 3296, 186 | :value nil} 187 | {:create-index 3296, 188 | :flags 0, 189 | :key "key", 190 | :lock-index 0, 191 | :modify-index 3296, 192 | :value "val"}) 193 | ``` 194 | 195 | If any of the operations fail, the whole transaction fails. Errors are reported in the exception `:errors` key. 196 | 197 | Currently supported operations: `kv-set`, `kv-get`, `kv-set-cas`, `kv-lock`, `kv-unlock`, `kv-get-tree`, `kv-check-index`, 198 | `kv-check-session`, `kv-delete`, `kv-delete-tree`, `kv-delete-cas`. 199 | 200 | ### Agent 201 | 202 | The agent consul messages generally are where most applications will interact with Consul. 203 | 204 | Return the checks a local agent is managing: 205 | 206 | ```clojure 207 | (agent-checks :local) 208 | => {"service:redis-04" 209 | {:Name "Service redis-shard-1' check", 210 | :ServiceName "redis-shard-1", 211 | :Status "warning", 212 | :CheckID "service:redis-04", 213 | :Output 214 | "Could not connect to Redis at 127.0.0.1:6503: Connection refused\n", 215 | :Notes "", 216 | :Node "MacBook-Pro.attlocal.net", 217 | :ServiceID "redis-04"}} 218 | ``` 219 | 220 | List services registered with the agent. 221 | 222 | ```clojure 223 | (agent-services :local) 224 | => {"consul" 225 | {:ID "consul", :Service "consul", :Tags [], :Address "", :Port 8300}} 226 | ``` 227 | 228 | List members the agent sees. 229 | 230 | ```clojure 231 | (agent-members :local) 232 | => ({:DelegateMax 4, 233 | :Name "server.domain.net", 234 | :Addr "192.168.1.72", 235 | :ProtocolMin 1, 236 | :Status 1, 237 | :ProtocolMax 2, 238 | :DelegateCur 4, 239 | :DelegateMin 2, 240 | :ProtocolCur 2, 241 | :Tags 242 | {:bootstrap "1", 243 | :build "0.5.2:9a9cc934", 244 | :dc "dc1", 245 | :port "8300", 246 | :role "consul", 247 | :vsn "2", 248 | :vsn_max "2", 249 | :vsn_min "1"}, 250 | :Port 8301}) 251 | ``` 252 | 253 | Return the local agent configuration. 254 | 255 | ```clojure 256 | (agent-self :local) 257 | => ... 258 | ``` 259 | 260 | Put a node into maintenance mode. 261 | 262 | ```clojure 263 | (agent-maintenance :local true) 264 | => true 265 | ``` 266 | 267 | Take it out of maintenance mode. 268 | 269 | ```clojure 270 | (agent-maintenance :local false) 271 | => true 272 | ``` 273 | 274 | Join a node into a cluster using the RPC address. 275 | 276 | ```clojure 277 | (agent-join :local "10.1.3.1:8400") 278 | => true 279 | ``` 280 | 281 | Force leave a node. 282 | 283 | ```clojure 284 | (agent-force-leave :local "10.1.3.1:8400") 285 | => true 286 | ``` 287 | 288 | #### Check Management 289 | 290 | 291 | #### Service Management 292 | 293 | 294 | ### Catalog 295 | 296 | ### Health Checks 297 | 298 | ### Access Control Lists 299 | 300 | ### User Events 301 | 302 | ### Status 303 | 304 | ## License 305 | 306 | Copyright © 2015 Benjamin Poweski 307 | 308 | Distributed under the Eclipse Public License either version 1.0 or (at 309 | your option) any later version. 310 | -------------------------------------------------------------------------------- /src/consul/watch.clj: -------------------------------------------------------------------------------- 1 | (ns consul.watch 2 | (:require [consul.core :as consul] 3 | [clojure.core.async :as async])) 4 | 5 | (def watch-fns 6 | {:key consul/kv-get 7 | :keyprefix consul/kv-recurse 8 | :service consul/service-health}) 9 | 10 | (defn poll! [conn [kw x] params] 11 | (let [f (get watch-fns kw)] 12 | (f conn x params))) 13 | 14 | (defn poll 15 | "Calls consul for the given spec passing params. Returns the exception if one occurs." 16 | [conn spec params] 17 | (try (poll! conn spec params) 18 | (catch Exception err 19 | err))) 20 | 21 | (defn long-poll 22 | "Polls consul using spec and publishes results onto ch. Applies no throttling of requests towards consul except via ch." 23 | [conn spec ch {:as options}] 24 | (assert (get watch-fns (first spec) (str "unimplemented watch type " spec))) 25 | (async/go-loop [resp (async/! ch resp) 27 | (recur (async/ old-state 37 | (update-in [:failures] (fnil inc 0)) 38 | (assoc :error new-config)) 39 | (assoc old-state :config new-config :failures 0)))) 40 | 41 | (defn exp-wait [n max-ms] 42 | {:pre [(number? n) (>= n 0) (number? max-ms) (> max-ms 0)]} 43 | (min (* (Math/pow 2 n) 100) max-ms)) 44 | 45 | (defn setup-watch 46 | "Creates a watching channel and notifies changes to a change-chan channel 47 | 48 | :query-params - the query params passed into the underlying service call 49 | :max-retry-wait - max interval before retying consul when a failure occurs. Defaults to 5s. 50 | 51 | The watch will terminate when the change-chan output channel is closed or when resulting 52 | function is called" 53 | [conn [watch-key path :as spec] change-chan {:keys [max-retry-wait query-params log] :as options}] 54 | (let [ch (async/chan) 55 | log (or log (fn [& _]))] 56 | (long-poll conn spec ch query-params) 57 | (async/go 58 | (loop [old-state nil] 59 | (log "Start watching " spec) 60 | (when-let [new-config (async/! change-chan new-config) 71 | (recur (update-state old-state new-config)))) 72 | :else 73 | (recur (update-state old-state new-config))))) 74 | (log "Finished watching " spec)) 75 | #(async/close! ch))) 76 | 77 | (defn ttl-check-update 78 | [conn check-id ^long ms ch] 79 | "Periodically updates check-id according to freq. Exits if ch closes." 80 | {:pre [(string? check-id) ms]} 81 | (async/go-loop [] 82 | (async/! leader-ch [k false]) 161 | (log "Invalid session, leader lost for" k " - " state) 162 | (async/! leader-ch [k false]) 172 | (log "Error, release leader for" k " - " (.getMessage result)) 173 | (async/! leader-ch [k false]) 177 | (log "Leader lost for" k " - " result) 178 | (async/! leader-ch [k true]) 182 | (log "Leader gained for" k " - " result) 183 | (async/! leader-ch [k false]) 193 | (consul/kv-put conn k "1" {:release session})))) 194 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /src/consul/core.clj: -------------------------------------------------------------------------------- 1 | (ns consul.core 2 | (:require [camel-snake-kebab.core :as csk] 3 | [camel-snake-kebab.extras :as cske] 4 | [cheshire.core :as json] 5 | [clj-http.lite.client :as client] 6 | [clojure.set :as set] 7 | [clojure.string :as str]) 8 | (:import (javax.xml.bind DatatypeConverter))) 9 | 10 | ;; A couple of initial helper functions. 11 | 12 | (defn base64->str [^String s] 13 | (String. (DatatypeConverter/parseBase64Binary s) "UTF8")) 14 | 15 | (defn str->base64 [^String s] 16 | (DatatypeConverter/printBase64Binary (.getBytes s "UTF8"))) 17 | 18 | (defn endpoint->path 19 | "Converts a vector into the consul endpoint path." 20 | [endpoint] 21 | {:pre [(vector? endpoint)]} 22 | (str "/v1/" (str/join "/" (map #(if (keyword? %) (name %) (str %)) endpoint)))) 23 | 24 | (defn parse-response [response] 25 | (let [content-type (get-in response [:headers "content-type"])] 26 | (if (= content-type "application/json") 27 | (update-in response [:body] json/parse-string keyword) 28 | response))) 29 | 30 | (def local-conn {:scheme :http :server-name "127.0.0.1" :server-port 8500}) 31 | 32 | (defn consul-request 33 | "Constructs a Consul HTTP request." 34 | [conn method endpoint & [{:as opts :keys [body]}]] 35 | {:pre [(or (= conn :local) (map? conn))] 36 | :post [(:request-method %) (:server-name %) (:server-port %)]} 37 | (cond-> (assoc local-conn :request-method method) 38 | (map? conn) (merge conn) 39 | (vector? endpoint) (assoc :uri (endpoint->path endpoint)) 40 | (string? endpoint) (assoc :uri endpoint) 41 | (map? opts) (merge opts) 42 | (map? body) (update-in [:body] json/generate-string) 43 | (vector? body) (update-in [:body] json/generate-string))) 44 | 45 | (defn success? [{:keys [status] :as resp}] 46 | (or (client/unexceptional-status? status) 47 | (= 404 status))) 48 | 49 | (defn ex-info? [x] 50 | (instance? clojure.lang.ExceptionInfo x)) 51 | 52 | (defn consul 53 | "Creates a request and calls consul using the HTTP API." 54 | [conn method endpoint & [{:as request}]] 55 | (let [http-request (consul-request conn method endpoint request)] 56 | (try 57 | (let [response (-> http-request 58 | (assoc :throw-exceptions false) 59 | client/request parse-response)] 60 | (if (success? response) 61 | response 62 | (if-let [errors (get-in response [:body :Errors])] 63 | (throw (ex-info "Transaction failure" {:reason :transaction-failure 64 | :conn conn 65 | :endpoint endpoint 66 | :request request 67 | :http-request http-request 68 | :http-response response 69 | :errors errors})) 70 | (throw (ex-info (:body response) {:reason :application-failure 71 | :conn conn 72 | :endpoint endpoint 73 | :request request 74 | :http-request http-request 75 | :http-response response}))))) 76 | (catch java.net.ConnectException ex 77 | (throw (ex-info "connection failure" {:reason :connect-failure :conn conn :endpoint endpoint :request request :http-request http-request} ex))) 78 | (catch java.net.UnknownHostException ex 79 | (throw (ex-info "unknown host" {:reason :unknown-host :conn conn :endpoint endpoint :request request :http-request http-request} ex))) 80 | (catch Exception ex 81 | (if (ex-info? ex) 82 | (throw ex) 83 | (throw (ex-info (.getMessage ex) {:reason :exception :conn conn :endpoint endpoint :request request :http-request http-request} ex))))))) 84 | 85 | (defn headers->index 86 | "Selects the X-Consul-* headers into a map with keywordized names." 87 | [m] 88 | (reduce-kv 89 | (fn [m k v] 90 | (cond (= "x-consul-knownleader" k) (assoc m :known-leader (= "true" v)) 91 | (and (= "x-consul-index" k) (string? v)) (assoc m :modify-index (Long/parseLong v)) 92 | (re-matches #"x-consul-.*" k) (assoc m (keyword k) v) 93 | :else m)) 94 | {} m)) 95 | 96 | (defn consul-200 97 | [conn method endpoint params] 98 | (-> (consul conn method endpoint params) 99 | :status 100 | (= 200))) 101 | 102 | (defn consul-index 103 | [conn method endpoint params] 104 | (let [{:keys [body headers]} (consul conn method endpoint params)] 105 | (assoc (headers->index headers) :body (cske/transform-keys csk/->kebab-case-keyword body)))) 106 | 107 | (def consul-pascal-case-substitutions 108 | "Differences from PascalCase used by Consul." 109 | {:Http :HTTP 110 | :Id :ID 111 | :Ttl :TTL 112 | :ServiceId :ServiceID 113 | :CheckId :CheckID}) 114 | 115 | (defn map->consulcase [m] 116 | (-> (cske/transform-keys csk/->PascalCase m) 117 | (set/rename-keys consul-pascal-case-substitutions))) 118 | 119 | (def kv (juxt :key :value)) 120 | 121 | ;; Key/Value endpoint - https://www.consul.io/docs/agent/http/kv.html 122 | 123 | (defn kv-map-convert 124 | [response convert?] 125 | {:pre [(map? response)]} 126 | (cske/transform-keys csk/->kebab-case-keyword 127 | (assoc response 128 | :value (if (and convert? (string? (:Value response))) 129 | (base64->str (:Value response)) 130 | (:Value response))))) 131 | 132 | (defn mapify-response 133 | "Converts a list of kv's into a map, stripping off a given prefix" 134 | [prefix kvs] 135 | (if (seq? kvs) 136 | (reduce 137 | (fn [a v] 138 | (let [ks 139 | (map 140 | keyword 141 | (-> 142 | ^String (:key v) 143 | (.replaceFirst prefix "") 144 | (.split "/") 145 | seq))] 146 | (assoc-in a ks (:value v)))) 147 | {} kvs) 148 | kvs)) 149 | 150 | (defn kv-keys 151 | "Retrieves a set of keys using prefix. 152 | 153 | Parameters: 154 | 155 | :dc - Optional data center in which to retrieve k, defaults agent's data center. 156 | :wait - Used in conjunction with :index to get using a blocking query. e.g. 10s, 1m, etc. 157 | :index - The current Consul index, suitable for making subsequent calls to wait for changes since this query was last run. 158 | 159 | :separator - List keys up to separator." 160 | ([conn prefix] 161 | (kv-keys conn prefix {})) 162 | ([conn prefix {:keys [dc wait index separator] :as params}] 163 | (let [{:keys [body headers status] :as response} 164 | (consul conn :get [:kv prefix] {:query-params (assoc params :keys "")})] 165 | (cond 166 | (= 404 status) (headers->index headers) 167 | (seq body) (assoc (headers->index headers) :keys (into #{} body)))))) 168 | 169 | (defn kv-get 170 | "Retrieves key k from consul given the following optional parameters. 171 | 172 | Parameters: 173 | 174 | :dc - Optional data center in which to retrieve k, defaults agent's data center. 175 | :wait - Used in conjunction with :index to get using a blocking query. e.g. 10s, 1m, etc. 176 | :index - The current Consul index, suitable for making subsequent calls to wait for changes since this query was last run. 177 | 178 | :raw? - If true and used with a non-recursive GET, the response is just the raw value of the key, without any encoding. 179 | :string? - Converts the value returned for k into a string. Defaults to true." 180 | ([conn k] 181 | (kv-get conn k {})) 182 | ([conn k {:keys [dc wait index raw? string?] :or {raw? false string? true} :as params }] 183 | (let [{:keys [body headers] :as response} 184 | (consul conn :get [:kv k] 185 | {:query-params (cond-> (dissoc params :raw? :string?) 186 | raw? (assoc :raw ""))})] 187 | (cond 188 | (or (nil? body) raw?) 189 | (assoc (headers->index headers) :key k :body body) 190 | :else (kv-map-convert (first body) string?))))) 191 | 192 | 193 | 194 | (defn kv-recurse 195 | "Retrieves key k from consul given the following optional parameters. 196 | 197 | Parameters: 198 | 199 | :dc - Optional data center in which to retrieve k, defaults agent's data center. 200 | :wait - Used in conjunction with :index to get using a blocking query. e.g. 10s, 1m, etc. 201 | :index - The current Consul index, suitable for making subsequent calls to wait for changes since this query was last run. 202 | 203 | :string? - Converts the value returned for k into a string. Defaults to true." 204 | ([conn prefix] 205 | (kv-recurse conn prefix {:map? true})) 206 | ([conn prefix {:as params :keys [string? map?] :or {string? true map? true}}] 207 | (let [{:keys [body headers] :as response} 208 | (consul conn :get [:kv prefix] {:query-params (assoc params :recurse "")}) 209 | body (if (and body (seq? body)) (map #(kv-map-convert %1 string?) body))] 210 | (assoc 211 | (headers->index headers) 212 | :body body 213 | :mapped (mapify-response prefix body))))) 214 | 215 | (defn kv-put 216 | "Sets key k with value v. 217 | 218 | Parameters: 219 | 220 | :dc - Optional data center in which to retrieve k, defaults agent's data center. 221 | 222 | :flags - This can be used to specify an unsigned value between 0 and 2^(64-1). Stored against the key for client app use. 223 | :cas - Uses a CAS index when updating. If the index is 0, puts only if the key does not exist. If index is non-zero, index must match the ModifyIndex of that key. 224 | :acquire - Session ID. Updates using lock acquisition. A key does not need to exist to be acquired 225 | :release - Session ID. Yields a lock acquired with :acquire." 226 | ([conn k v] 227 | (kv-put conn k v {})) 228 | ([conn k v {:keys [dc flags cas acquire release] :as params}] 229 | (:body (consul conn :put [:kv k] {:query-params params :body v})))) 230 | 231 | (defn kv-del 232 | "Deletes a key k from consul. 233 | 234 | Parameters: 235 | 236 | :dc - Optional data center in which to retrieve k, defaults agent's data center. 237 | :recurse? - If true, then consul it will delete all keys with the given prefix. Defaults to false. 238 | :cas - Uses a CAS index when deleting. Index must be non-zero and must match the ModifyIndex of the key to delete successfully" 239 | ([conn k] 240 | (kv-del conn k {})) 241 | ([conn k {:as params :keys [recurse?] :or {recurse? false}}] 242 | (:body (consul conn :delete [:kv k] {:query-params (cond-> (dissoc params :recurse?) 243 | recurse? (assoc :recurse ""))})))) 244 | 245 | ;; Agent endpoint - https://www.consul.io/docs/agent/http/agent.html 246 | 247 | (defn shallow-nameify-keys 248 | "Returns a map with the root keys converted to strings using name." 249 | [m] 250 | (reduce-kv #(assoc %1 (name %2) (cske/transform-keys csk/->kebab-case-keyword %3)) {} m)) 251 | 252 | (defn agent-checks 253 | "Returns all the checks that are registered with the local agent as a map with the check names as strings. " 254 | ([conn] 255 | (agent-checks conn {})) 256 | ([conn {:as params}] 257 | (shallow-nameify-keys (:body (consul conn :get [:agent :checks] {:query-params params}))))) 258 | 259 | (defn agent-services 260 | "Returns all services registered with the local agent." 261 | ([conn] 262 | (agent-services conn {})) 263 | ([conn {:as params}] 264 | (shallow-nameify-keys (:body (consul conn :get [:agent :services] {:query-params params}))))) 265 | 266 | (defn agent-members 267 | "Returns the members as seen by the local serf agent." 268 | ([conn] 269 | (agent-members conn {})) 270 | ([conn {:as params}] 271 | (cske/transform-keys csk/->kebab-case-keyword (:body (consul conn :get [:agent :members] {:query-params params}))))) 272 | 273 | (defn agent-self 274 | "Returns the local node configuration." 275 | ([conn] 276 | (agent-self conn {})) 277 | ([conn {:as params}] 278 | (cske/transform-keys csk/->kebab-case-keyword (:body (consul conn :get [:agent :self] {:query-params params}))))) 279 | 280 | 281 | 282 | (defn agent-maintenance 283 | "Manages node maintenance mode. 284 | 285 | Requires a boolean value for enable?. An optional :reason can be provided, returns true if successful." 286 | ([conn enable?] 287 | (agent-maintenance conn enable? {})) 288 | ([conn enable? {:as params}] 289 | {:pre [(contains? #{true false} enable?)]} 290 | (consul-200 conn :put [:agent :maintenance] {:query-params (assoc params :enable enable?)}))) 291 | 292 | (defn agent-join 293 | "Triggers the local agent to join a node. Returns true if successful." 294 | ([conn address] 295 | (agent-join conn address {})) 296 | ([conn address {:as params}] 297 | (consul-200 conn :get [:agent :join address] {:query-params params}))) 298 | 299 | (defn agent-force-leave 300 | "Forces removal of a node, returns true if the request succeeded." 301 | ([conn node] 302 | (agent-force-leave conn node {})) 303 | ([conn node {:as params}] 304 | (consul-200 conn :get [:agent :force-leave node] {:query-params params}))) 305 | 306 | ;; Agent Checks - https://www.consul.io/docs/agent/http/agent.html#agent_check_register 307 | 308 | 309 | (defn check? 310 | "Simple validation outlined in https://www.consul.io/docs/agent/checks.html." 311 | [{:keys [Name HTTP TTL Script Interval] :as check}] 312 | (and (string? Name) 313 | (or (string? Script) (string? HTTP) (string? TTL)) 314 | (if (or Script HTTP) 315 | (string? Interval) 316 | true))) 317 | 318 | 319 | (defn map->check 320 | "Creates a check from a map. Returns a map with excessive capitalization conforming to consul's expectations. 321 | 322 | Requires :script, :http or :ttl to be set. 323 | 324 | Map keys: 325 | 326 | :name - required 327 | :id - If not provided, is set to :name. However, duplicate :id values are not allowed. 328 | :notes - Free text. 329 | :script - Path of the script Consul should invoke as part of the check. 330 | :http - URL of the HTTP check. 331 | :interval - Required if :script or :http is set. 332 | :service-id - Associates the check with an existing service." 333 | [check] 334 | {:post [(check? %)]} 335 | (map->consulcase check)) 336 | 337 | (defn agent-register-check 338 | "Registers a new check with the local agent." 339 | ([conn {:keys [name id notes script http interval service-id] :as m}] 340 | (agent-register-check conn m {})) 341 | ([conn m {:as params}] 342 | (consul-200 conn :put [:agent :check :register] {:body (map->check m) :query-params params}))) 343 | 344 | (defn agent-deregister-check 345 | "Deregisters a check with the local agent." 346 | ([conn check-id] 347 | (agent-deregister-check conn check-id {})) 348 | ([conn check-id {:as params}] 349 | (consul-200 conn :delete [:agent :check :deregister check-id] {:query-params params}))) 350 | 351 | (defn agent-pass-check 352 | "Marks a local check as passing." 353 | ([conn check-id] 354 | (agent-pass-check conn check-id {})) 355 | ([conn check-id {:keys [note] :as params}] 356 | (consul-200 conn :get [:agent :check :pass check-id] {:query-params params}))) 357 | 358 | (defn agent-warn-check 359 | "Marks a local test as warning." 360 | ([conn check-id] 361 | (agent-warn-check conn check-id {})) 362 | ([conn check-id {:keys [note] :as params}] 363 | (consul-200 conn :get [:agent :check :warn check-id] {:query-params params}))) 364 | 365 | (defn agent-fail-check 366 | "Marks a local test as critical." 367 | ([conn check-id] 368 | (agent-fail-check conn check-id {})) 369 | ([conn check-id {:keys [note] :as params}] 370 | (consul-200 conn :get [:agent :check :warn check-id] {:query-params params}))) 371 | 372 | ;; Services - https://www.consul.io/docs/agent/http/agent.html#agent_service_register 373 | 374 | (defn agent-register-service 375 | "Registers a new local service." 376 | ([conn {:keys [id name tags address port check] :as m}] 377 | (agent-register-service conn m {})) 378 | ([conn m {:as params}] 379 | (consul-200 conn :put [:agent :service :register] {:query-params params :body m}))) 380 | 381 | (defn agent-deregister-service 382 | "Registers a new local service." 383 | ([conn service-id] 384 | (agent-deregister-service conn service-id {})) 385 | ([conn service-id {:as params}] 386 | (consul-200 conn :get [:agent :service :deregister service-id] {:query-params params}))) 387 | 388 | 389 | (defn agent-maintenance-service 390 | "Registers a new local service." 391 | ([conn service-id enable reason] 392 | (agent-maintenance-service conn service-id {:enable enable :reason reason})) 393 | ([conn service-id {:keys [enable reason] :as params}] 394 | (consul-200 conn :get [:agent :service :maintenance service-id] {:query-params params}))) 395 | 396 | 397 | ;; Catalog endpoints - https://www.consul.io/docs/agent/http/catalog.html 398 | 399 | ;; These are low level endpoints, so it is preferrable to use the other functions instead 400 | 401 | (defn catalog-register 402 | "Register a catalog entry. Low level - preferably" 403 | ([conn {:keys [datacenter node address service check] :as entry}] 404 | (catalog-register conn entry {})) 405 | ([conn entry {:as params}] 406 | (consul-200 conn :put [:catalog :register] {:query-params params :body (map->consulcase entry)}))) 407 | 408 | (defn catalog-deregister 409 | ([conn entry] 410 | (catalog-deregister conn entry {})) 411 | ([conn {:keys [datacenter node checkid serviceid] :as entry} {:as params}] 412 | (consul-200 conn :put [:catalog :deregister] {:query-params params :body (map->consulcase entry)}))) 413 | 414 | (defn catalog-datacenters 415 | ([conn] 416 | (catalog-datacenters conn {})) 417 | ([conn {:as params}] 418 | (consul-index conn :get [:catalog :datacenters] {:query-params params}))) 419 | 420 | (defn catalog-nodes 421 | ([conn] 422 | (catalog-nodes conn {})) 423 | ([conn {:keys [dc] :as params}] 424 | (:body (consul-index conn :get [:catalog :nodes] {:query-params params})))) 425 | 426 | (defn catalog-services 427 | ([conn] 428 | (catalog-services conn {})) 429 | ([conn {:keys [dc] :as params}] 430 | (:body (consul-index conn :get [:catalog :services] {:query-params params})))) 431 | 432 | (defn catalog-service 433 | ([conn service] 434 | (catalog-service conn service {})) 435 | ([conn service {:keys [tag dc] :as params}] 436 | (:body (consul-index conn :get [:catalog :service service] {:query-params params})))) 437 | 438 | (defn catalog-node 439 | ([conn node] 440 | (catalog-node conn node {})) 441 | ([conn node {:keys [dc] :as params}] 442 | (:body (consul-index conn :get [:catalog :node node] {:query-params params})))) 443 | 444 | 445 | ;; Sessions - https://www.consul.io/docs/agent/http/status.html 446 | 447 | (defn session-create 448 | "Create a new consul session" 449 | ([conn] 450 | (session-create conn {} {})) 451 | ([conn {:keys [lock-delay node name checks behavior ttl] :as session}] 452 | (session-create conn session {})) 453 | ([conn session {:keys [dc] :as params}] 454 | (-> (consul-index conn :put [:session :create] {:query-params params :body (map->consulcase session)}) 455 | :body :id))) 456 | 457 | (defn session-renew 458 | "Renews a session. NOTE the TTL on the response and use that as a basic on when to renew next time 459 | since consul may return a higher TTL, requesting that you renew less often (high server load)" 460 | ([conn session] 461 | (session-renew conn session {})) 462 | ([conn session {:keys [dc] :as params}] 463 | (-> (consul-index conn :put [:session :renew session] {:query-params params}) 464 | :body first))) 465 | 466 | (defn session-destroy 467 | ([conn session] 468 | (session-destroy conn session {})) 469 | ([conn session {:keys [dc] :as params}] 470 | (consul-200 conn :put [:session :destroy session] {:query-params params}))) 471 | 472 | (defn session-info 473 | ([conn session] 474 | (session-info conn session {})) 475 | ([conn session {:keys [dc] :as params}] 476 | (consul-index conn :put [:session :info session] {:query-params params}))) 477 | 478 | (defn session-node 479 | ([conn node] 480 | (session-node conn node {})) 481 | ([conn node {:keys [dc] :as params}] 482 | (consul-index conn :put [:session :node node] {:query-params params}))) 483 | 484 | (defn session-list 485 | ([conn] 486 | (session-list conn {})) 487 | ([conn {:keys [dc] :as params}] 488 | (consul-index conn :put [:session :list] {:query-params params}))) 489 | 490 | ;; ACL's - https://www.consul.io/docs/agent/http/acl.html 491 | 492 | ;; Also, read up on ACL's here https://www.consul.io/docs/internals/acl.html 493 | 494 | 495 | (defn acl-create 496 | "Create a new ACL token 497 | 498 | :type is 'client' or 'management'" 499 | [conn {:keys [name type rules] :as tkn} {:keys [token] :as params}] 500 | (-> (consul-index conn :put [:acl :create] {:query-params params :body (map->consulcase tkn)}) 501 | :body :id)) 502 | 503 | (defn acl-update 504 | [conn {:keys [id name type rules] :as tkn} {:keys [token] :as params}] 505 | (consul-index conn :put [:acl :update] {:query-params params :body (map->consulcase tkn)})) 506 | 507 | (defn acl-destroy 508 | [conn token-id {:keys [token] :as params}] 509 | (consul-200 conn :put [:acl :delete token-id] {:query-params params})) 510 | 511 | (defn acl-info 512 | [conn token-id {:keys [token] :as params}] 513 | (:body (consul-index conn :get [:acl :info token-id] {:query-params params}))) 514 | 515 | (defn acl-clone 516 | [conn token-id {:keys [token] :as params}] 517 | (-> (consul-index conn :put [:acl :clone token-id] {:query-params params}) 518 | :body :id)) 519 | 520 | (defn acl-list 521 | [conn {:keys [token] :as params}] 522 | (consul-index conn :put [:acl :list] {:query-params params})) 523 | 524 | ;; Events - https://www.consul.io/docs/agent/http/event.html 525 | ;; https://www.consul.io/docs/commands/event.html 526 | 527 | (defn event-fire 528 | ([conn name] 529 | (event-fire conn name {})) 530 | ([conn name {:keys [node service tag] :as params}] 531 | (consul-index conn :put [:event :fire name] {:query-params params}))) 532 | 533 | (defn event-list 534 | ([conn] 535 | (event-list conn {})) 536 | ([conn {:keys [name] :as params}] 537 | (consul-index conn :get [:event :list] {:query-params params}))) 538 | 539 | ;; Health Checks - https://www.consul.io/docs/agent/http/health.html 540 | 541 | (defn node-health 542 | "Returns the health info of a node" 543 | ([conn node] 544 | (node-health conn node {})) 545 | ([conn node {:keys [dc] :as params}] 546 | (consul-index conn :get [:health :node node] {:query-params params}))) 547 | 548 | (defn service-health-checks 549 | "Returns the checks of a service" 550 | ([conn service] 551 | (service-health-checks conn service {})) 552 | ([conn service {:as params}] 553 | (consul-index conn :get [:health :checks service] {:query-params params}))) 554 | 555 | (defn service-health 556 | "Returns the nodes and health info of a service." 557 | ([conn service] 558 | (service-health conn service {})) 559 | ([conn service {:keys [dc tag passing?] :or {passing? false}:as params }] 560 | (consul-index conn :get [:health :service service] 561 | {:query-params (cond-> (dissoc params :passing?) 562 | passing? (assoc :passing ""))}))) 563 | 564 | (defn health-state 565 | "Returns the checks in a given state (any, unknown, passing, warning or critical)" 566 | ([conn state] 567 | (health-state conn state {})) 568 | ([conn state {:as params}] 569 | (consul-index conn :get [:health :state state] {:query-params params}))) 570 | 571 | 572 | ;; Status - https://www.consul.io/docs/agent/http/status.html 573 | 574 | (defn leader 575 | "Returns the current Raft leader." 576 | [conn] 577 | (:body (consul conn :get [:status :leader]))) 578 | 579 | (defn peers 580 | "Returns the Raft peers for the datacenter in which the agent is running." 581 | [conn] 582 | (:body (consul conn :get [:status :peers]))) 583 | 584 | ;; Helper functions 585 | 586 | (defn passing? 587 | "Returns true if check is passing" 588 | [check] 589 | (contains? #{"passing"} (:Status check))) 590 | 591 | (defn healthy-service? 592 | "Returns true if every check is passing for each object returned from /v1/health/service/." 593 | [health-service] 594 | (every? passing? (:Checks health-service))) 595 | --------------------------------------------------------------------------------