├── 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 |
--------------------------------------------------------------------------------