An HTTP client for Consul. Forked from @aphyr's verschlimmbesserung which was written for Etcd (thanks, Kyle!).
- Explicit client objects
- Explicit option maps, not kwargs, for ease of composition
- Explicit timeouts throughout
- Key and value escaping; unicode keys are yours.
- Comprehensive error handling; catch specific etcd error codes
- Clojure-style swap!
- Both raw (
get*
) and simplified (get
) APIs - Etcd metadata on raw responses
- Directories as (possibly nested) maps, etc
- Comprehensive docstrings
Via Clojars, as usual.
Require the library and define a client.
user=> (require '[verschlimmbesserung.core :as v])
nil
user=> (def c (v/connect "http://localhost:4001"))
#'user/c
First we'll create an node at /atom
. All functions take a client as their
first argument.
user=> (v/reset! c :atom "hi")
{:action "set", :node {:key "/atom", :value "hi", :modifiedIndex 1520, :createdIndex 1520}}
Which will show up if we list the root:
user=> (v/get c nil)
{"atom" "hi"}
Let's get that key's value
user=> (v/get c :atom)
"hi"
A little more detail, please
user=> (v/get* c :atom)
{:action "get", :node {:key "/atom", :value "hi", :modifiedIndex 1520, :createdIndex 1520}}
user=> (meta (v/get* c :atom))
{:status 200, :leader-peer-url "http://127.0.0.1:7001", :etcd-index "1520", :raft-index "33259", :raft-term "0"}
Let's compare-and-set it to a new value based on the current value
user=> (v/cas! c :atom "hi" "meow")
{:action "compareAndSwap", :node {:key "/atom", :value "meow", :modifiedIndex 1521, :createdIndex 1520}, :prevNode {:key "/atom", :value "hi", :modifiedIndex 1520, :createdIndex 1520}}
That same CAS will fail if we try it a second time:
user=> (v/cas! c :atom "hi" "meow")
false
We can also use the node's index to constrain a CAS:
user=> (-> c (v/get* :atom) :node :modifiedIndex)
1521
user=> (v/cas-index! c :atom 123 "meow")
false
user=> (v/cas-index! c :atom 1521 "meow")
{:action "compareAndSwap", :node {:key "/atom", :value "meow", :modifiedIndex 1522, :createdIndex 1520}, :prevNode {:key "/atom", :value "meow", :modifiedIndex 1521, :createdIndex 1520}}
Or use a Clojure style swap! to apply a pure function to a node:
user=> (v/get c :atom)
"meow"
user=> (v/swap! c :atom str " says the cat")
"meow says the cat"
user=> (v/swap! c :atom str " says the cat")
"meow says the cat says the cat"
U-U-U-U-ULTRA PARALLEL
user=> (v/reset! c :counter "0")
{:action "set", :node {:key "/counter", :value "0", :modifiedIndex 2229, :createdIndex 2229}}
user=> (->> (range 100)
(map #(future % (v/swap! c :counter (comp inc read-string))))
doall
(map deref))
(36 68 70 33 2 12 34 24 59 46 60 7 94 9 45 48 86 44 57 92 99 93 87 52 50 78 61 80 38 20 53 13 11 39 66 28 41 89 58 21 79 69 43 8 90 84 77 19 47 95 15 55 83 91 98 49 73 22 32 3 72 76 82 4 40 65 96 37 97 63 29 25 35 88 64 85 10 17 27 26 1 100 23 62 75 6 42 30 18 16 74 51 56 31 67 81 71 14 5 54)
Delete the node with delete!
user=> (v/delete! c :atom)
{:action "delete", :node {:key "/atom", :modifiedIndex 1525, :createdIndex 1520}, :prevNode {:key "/atom", :value "meow says the cat says the cat", :modifiedIndex 1524, :createdIndex 1520}}
Delete everything in a directory with delete-all!
. You can use nil
as a path to delete everything from the root.
user=> (v/delete-all! c nil)
nil
You can use strings, symbols, keywords, nil, and any sequential collection as a
key. Collections like vectors are joined with slashes, making it easier to manipulate nested objects. nil
refers to the root.
user=> (v/reset! c [:cats :mittens] "black n white")
{:action "set", :node {:key "/cats/mittens", :value "black n white", :modifiedIndex 1528, :createdIndex 1528}}
user=> (v/reset! c [:cats :patches] "calico")
{:action "set", :node {:key "/cats/patches", :value "calico", :modifiedIndex 1529, :createdIndex 1529}}
user=> (v/get c [:cats :patches])
"calico"
Directories are rendered as maps:
user=> (v/get c [:cats])
{"mittens" "black n white", "patches" "calico"}
By default, etcd
won't show the contents of subdirectories.
user=> (v/get c nil)
{"cats" nil}
user=> (v/get* c nil)
{:action "get", :node {:key "/", :dir true, :nodes [{:key "/cats", :dir true, :modifiedIndex 1528, :createdIndex 1528}]}}
But we can render them recursively like so:
user=> (v/get c nil {:recursive? true})
{"cats" {"mittens" "black n white", "patches" "calico"}}
Character encodings work like you'd expect. Let's create a unique key under "physics" with the value "student":
user=> (v/create! c "物理学" "学生")
"/物理学/1530"
user=> (v/get c ["物理学" 1530])
"学生"
Errors are mapped nicely through the Slingshot library, so you get details up front:
user=> (use 'slingshot.slingshot)
nil
user=> (try+ (v/swap! c :nonexistent identity) (catch Object e (pprint e)))
{:status 404,
:errorCode 100,
:message "Key not found",
:cause "/nonexistent",
:index 1903}
Which makes it easy to write code that correctly handles specific HTTP statuses:
user=> (try+ (v/swap! c :nonexistent identity) (catch [:status 404] _ :not-found))
:not-found
Or etcd error codes
user=> (try+ (v/swap! c :nonexistent identity) (catch [:errorCode 100] _ :not-found))
:not-found
No support for watches/locks/leaders, but they wouldn't be hard to add. Have at it! :)
Copyright © 2014 Jacques Fuentes jpfuentes2@gmail.com
Distributed under the Eclipse Public License, the same as Clojure.