├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── lib ├── consul.ex └── consul │ ├── agent.ex │ ├── agent │ ├── check.ex │ └── service.ex │ ├── catalog.ex │ ├── endpoint.ex │ ├── event.ex │ ├── handler │ ├── base.ex │ ├── behaviour.ex │ └── kv.ex │ ├── health.ex │ ├── kv.ex │ ├── request.ex │ ├── response.ex │ ├── session.ex │ └── watch │ ├── event.ex │ └── handler.ex ├── mix.exs └── test ├── consul_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /deps 3 | erl_crash.dump 4 | *.ez 5 | .DS_Store 6 | mix.lock 7 | /doc 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | sudo: false 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Undead Labs, LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Consul 2 | 3 | [![Build Status](https://travis-ci.org/undeadlabs/consul-ex.png?branch=master)](https://travis-ci.org/undeadlabs/consul-ex) 4 | 5 | An Elixir client for Consul's HTTP API 6 | 7 | ## Requirements 8 | 9 | * Elixir 1.0.0 or newer 10 | 11 | ## Installation 12 | 13 | Add Consul as a dependency in your `mix.exs` file 14 | 15 | ```elixir 16 | def application do 17 | [applications: [:consul]] 18 | end 19 | 20 | defp deps do 21 | [ 22 | {:consul, "~> 1.0.0"} 23 | ] 24 | end 25 | ``` 26 | 27 | Then run `mix deps.get` in your shell to fetch the dependencies. 28 | 29 | ## Docs 30 | 31 | Run `mix docs` and open `doc/index.html` to view the documentation. 32 | 33 | ## Authors 34 | 35 | Jamie Winsor () 36 | -------------------------------------------------------------------------------- /lib/consul.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul do 8 | defmodule ResponseError do 9 | defexception response: nil 10 | 11 | def exception(value) do 12 | %__MODULE__{response: value} 13 | end 14 | 15 | def message(%{response: response}) do 16 | "Got non-200 OK status code: #{response.status_code}" 17 | end 18 | end 19 | 20 | use Application 21 | 22 | def start(_type, _args) do 23 | import Supervisor.Spec, warn: false 24 | 25 | opts = [strategy: :one_for_one, name: Consul.Supervisor] 26 | Supervisor.start_link([], opts) 27 | end 28 | end 29 | 30 | -------------------------------------------------------------------------------- /lib/consul/agent.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Agent do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @agent "agent" 12 | @checks "checks" 13 | @force_leave "force-leave" 14 | @join "join" 15 | @members "members" 16 | @self "self" 17 | @services "services" 18 | 19 | @spec checks(Keyword.t) :: Endpoint.response 20 | def checks(opts \\ []) do 21 | build_url([@agent, @checks], opts) 22 | |> req_get() 23 | end 24 | 25 | @spec join(binary, Keyword.t) :: Endpoint.response 26 | def join(address, opts \\ []) do 27 | build_url([@agent, @join, address], opts) 28 | |> req_get() 29 | end 30 | 31 | @spec force_leave(binary, Keyword.t) :: Endpoint.response 32 | def force_leave(node, opts \\ []) do 33 | build_url([@agent, @force_leave, node], opts) 34 | |> req_get() 35 | end 36 | 37 | @spec members(Keyword.t) :: Endpoint.response 38 | def members(opts \\ []) do 39 | build_url([@agent, @members], opts) 40 | |> req_get() 41 | end 42 | 43 | @spec self(Keyword.t) :: Endpoint.response 44 | def self(opts \\ []) do 45 | build_url([@agent, @self], opts) 46 | |> req_get() 47 | end 48 | 49 | @spec services(Keyword.t) :: Endpoint.response 50 | def services(opts \\ []) do 51 | build_url([@agent, @services], opts) 52 | |> req_get() 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/consul/agent/check.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Agent.Check do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @agent "agent" 12 | @check "check" 13 | @deregister "deregister" 14 | @fail "fail" 15 | @pass "pass" 16 | @register "register" 17 | @warn "warn" 18 | 19 | @spec register(map, Keyword.t) :: Endpoint.response 20 | def register(body, opts \\ []) do 21 | build_url([@agent, @check, @register], opts) 22 | |> req_put(JSX.encode!(body)) 23 | end 24 | 25 | @spec deregister(binary, Keyword.t) :: Endpoint.response 26 | def deregister(id, opts \\ []) do 27 | build_url([@agent, @check, @deregister, id], opts) 28 | |> req_delete() 29 | end 30 | 31 | @spec pass(binary, Keyword.t) :: Endpoint.response 32 | def pass(id, opts \\ []) do 33 | build_url([@agent, @check, @pass, id], opts) 34 | |> req_get() 35 | end 36 | 37 | @spec warn(binary, Keyword.t) :: Endpoint.response 38 | def warn(id, opts \\ []) do 39 | build_url([@agent, @check, @warn, id], opts) 40 | |> req_get() 41 | end 42 | 43 | @spec fail(binary, Keyword.t) :: Endpoint.response 44 | def fail(id, opts \\ []) do 45 | build_url([@agent, @check, @fail, id], opts) 46 | |> req_get() 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/consul/agent/service.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Agent.Service do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @agent "agent" 12 | @deregister "deregister" 13 | @register "register" 14 | @service "service" 15 | 16 | @spec register(map, Keyword.t) :: Endpoint.response 17 | def register(%{"Name" => _} = body, opts \\ []) do 18 | build_url([@agent, @service, @register], opts) 19 | |> req_put(JSX.encode!(body)) 20 | end 21 | 22 | @spec deregister(binary, Keyword.t) :: Endpoint.response 23 | def deregister(id, opts \\ []) do 24 | build_url([@agent, @service, @deregister, id], opts) 25 | |> req_delete() 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/consul/catalog.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Catalog do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @catalog "catalog" 12 | @datacenters "datacenters" 13 | @deregister "deregister" 14 | @nodes "nodes" 15 | @node "node" 16 | @services "services" 17 | @service "service" 18 | 19 | @spec datacenters(Keyword.t) :: Endpoint.response 20 | def datacenters(opts \\ []) do 21 | build_url([@catalog, @datacenters], opts) 22 | |> req_get() 23 | end 24 | 25 | @spec deregister(map, Keyword.t) :: Endpoint.response 26 | def deregister(%{"Datacenter" => _, "Node" => _} = body, opts \\ []) do 27 | build_url([@catalog, @deregister], opts) 28 | |> req_put(JSX.encode!(body)) 29 | end 30 | 31 | @spec nodes(Keyword.t) :: Endpoint.response 32 | def nodes(opts \\ []) do 33 | build_url([@catalog, @nodes], opts) 34 | |> req_get() 35 | end 36 | 37 | @spec node(binary, Keyword.t) :: Endpoint.response 38 | def node(id, opts \\ []) do 39 | build_url([@catalog, @node, id], opts) 40 | |> req_get() 41 | end 42 | 43 | @spec services(Keyword.t) :: Endpoint.response 44 | def services(opts \\ []) do 45 | build_url([@catalog, @services], opts) 46 | |> req_get() 47 | end 48 | 49 | @spec service(binary, Keyword.t) :: Endpoint.response 50 | def service(name, opts \\ []) do 51 | build_url([@catalog, @service, name], opts) 52 | |> req_get() 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/consul/endpoint.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Endpoint do 8 | @path_separator "/" 9 | 10 | @type response :: {:ok, Consul.Response.t} | {:error, Consul.Response.t} 11 | 12 | @spec build_url(binary | [binary], Keyword.t) :: binary 13 | def build_url(path, opts \\ []) 14 | def build_url(path, opts) when is_list(path) do 15 | List.flatten(path) 16 | |> Enum.join(@path_separator) 17 | |> build_url(opts) 18 | end 19 | def build_url(path, []), do: path 20 | def build_url(path, opts) when is_binary(path) do 21 | path <> "?" <> URI.encode_query(opts) 22 | end 23 | 24 | defmacro __using__(handler: handler) do 25 | quote do 26 | alias Consul.Request, as: Request 27 | import unquote(__MODULE__), only: [build_url: 1, build_url: 2] 28 | 29 | defp req_get(url, headers \\ [], options \\ []) do 30 | Request.get(url, headers, options) |> handle_response 31 | end 32 | 33 | defp req_put(url, body, headers \\ [], options \\ []) do 34 | Request.put(url, body, headers, options) |> handle_response 35 | end 36 | 37 | defp req_head(url, headers \\ [], options \\ []) do 38 | Request.head(url, headers, options) |> handle_response 39 | end 40 | 41 | defp req_post(url, body, headers \\ [], options \\ []) do 42 | Request.post(url, body, headers, options) |> handle_response 43 | end 44 | 45 | defp req_patch(url, body, headers \\ [], options \\ []) do 46 | Request.patch(url, body, headers, options) |> handle_response 47 | end 48 | 49 | defp req_delete(url, headers \\ [], options \\ []) do 50 | Request.delete(url, headers, options) |> handle_response 51 | end 52 | 53 | defp req_options(url, headers \\ [], options \\ []) do 54 | Request.options(url, headers, options) |> handle_response 55 | end 56 | 57 | defp handle_response({:ok, response}), do: unquote(handler).handle(response) 58 | defp handle_response({:error, _} = error), do: error 59 | end 60 | end 61 | end 62 | -------------------------------------------------------------------------------- /lib/consul/event.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Event do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | defstruct id: nil, 12 | name: nil, 13 | payload: nil, 14 | node_filter: nil, 15 | service_filter: nil, 16 | tag_filter: nil, 17 | version: nil, 18 | l_time: nil 19 | 20 | @type t :: %{ 21 | id: binary, 22 | name: binary, 23 | payload: binary | nil, 24 | service_filter: binary, 25 | tag_filter: binary, 26 | version: integer, 27 | l_time: integer, 28 | } 29 | 30 | @event "event" 31 | @fire "fire" 32 | @list "list" 33 | 34 | @spec fire(binary, binary, Keyword.t) :: Endpoint.response 35 | def fire(name, payload \\ "", opts \\ []) when is_binary(payload) do 36 | build_url([@event, @fire, name], opts) 37 | |> req_put(payload) 38 | end 39 | 40 | @doc """ 41 | Build a list of `Consul.Event` from the given `Consul.Response`. 42 | """ 43 | @spec from_response(Consul.Response.t) :: [t] 44 | def from_response(%{body: body}) when is_list(body) do 45 | Enum.map body, &build_event/1 46 | end 47 | 48 | @doc """ 49 | Return the l_time of the most recent `Consul.Event` in the given list. 50 | """ 51 | @spec last_time([t]) :: integer | nil 52 | def last_time([]), do: nil 53 | def last_time(events) do 54 | [%__MODULE__{l_time: time}|_] = sort(events) 55 | time 56 | end 57 | 58 | @spec list(Keyword.t) :: Endpoint.response 59 | def list(opts \\ []) do 60 | build_url([@event, @list], opts) 61 | |> req_get() 62 | end 63 | 64 | @doc """ 65 | Sort a list of `Consul.Event` by their `l_time` field. 66 | """ 67 | @spec sort([t]) :: [t] 68 | def sort(events) do 69 | Enum.sort events, &(&1.l_time > &2.l_time) 70 | end 71 | 72 | # 73 | # Private 74 | # 75 | 76 | defp build_event(%{"ID" => id, "Name" => name, "Payload" => nil, "NodeFilter" => node_filter, 77 | "ServiceFilter" => service_filter, "TagFilter" => tag_filter, "Version" => version, "LTime" => l_time}) do 78 | %Consul.Event{id: id, name: name, payload: nil, node_filter: node_filter, service_filter: service_filter, 79 | tag_filter: tag_filter, version: version, l_time: l_time} 80 | end 81 | defp build_event(%{"ID" => id, "Name" => name, "Payload" => payload, "NodeFilter" => node_filter, 82 | "ServiceFilter" => service_filter, "TagFilter" => tag_filter, "Version" => version, "LTime" => l_time}) do 83 | %Consul.Event{id: id, name: name, payload: :base64.decode(payload), node_filter: node_filter, 84 | service_filter: service_filter, tag_filter: tag_filter, version: version, l_time: l_time} 85 | end 86 | end 87 | -------------------------------------------------------------------------------- /lib/consul/handler/base.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Handler.Base do 8 | use Consul.Handler.Behaviour 9 | end 10 | -------------------------------------------------------------------------------- /lib/consul/handler/behaviour.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Handler.Behaviour do 8 | use Behaviour 9 | 10 | defcallback handle(result :: Consul.Response.t) 11 | 12 | defmacro __using__(_) do 13 | quote do 14 | @behaviour unquote(__MODULE__) 15 | 16 | @spec handle(Consul.Response.t) :: Consule.Endpoint.response 17 | def handle(%{status_code: 200} = response) do 18 | {:ok, response} 19 | end 20 | def handle(response) do 21 | {:error, response} 22 | end 23 | 24 | defoverridable [handle: 1] 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/consul/handler/kv.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Handler.Kv do 8 | use Consul.Handler.Behaviour 9 | 10 | def handle(%{status_code: 200, body: body} = response) do 11 | {:ok, %{response | body: decode_body(body)}} 12 | end 13 | def handle(response), do: super(response) 14 | 15 | # 16 | # Private API 17 | # 18 | 19 | defp decode_body(items) when is_list(items) do 20 | Enum.map(items, &decode_body/1) 21 | end 22 | defp decode_body(%{"Value" => value} = item) when is_binary(value) do 23 | %{item | "Value" => :base64.decode(value)} 24 | end 25 | defp decode_body(item), do: item 26 | end 27 | -------------------------------------------------------------------------------- /lib/consul/health.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Health do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @checks "checks" 12 | @health "health" 13 | @node "node" 14 | @service "service" 15 | @state "state" 16 | 17 | @spec checks(binary, Keyword.t) :: Endpoint.response 18 | def checks(id, opts \\ []) do 19 | build_url([@health, @checks, id], opts) 20 | |> req_get() 21 | end 22 | 23 | @spec node(binary, Keyword.t) :: Endpoint.response 24 | def node(id, opts \\ []) do 25 | build_url([@health, @node, id], opts) 26 | |> req_get() 27 | end 28 | 29 | @spec service(binary, Keyword.t) :: Endpoint.response 30 | def service(id, opts \\ []) do 31 | build_url([@health, @service, id], opts) 32 | |> req_get() 33 | end 34 | 35 | @spec state(binary, Keyword.t) :: Endpoint.response 36 | def state(id, opts \\ []) do 37 | build_url([@health, @state, id], opts) 38 | |> req_get() 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/consul/kv.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Kv do 8 | alias Consul.Endpoint 9 | alias Consul.Response 10 | use Consul.Endpoint, handler: Consul.Handler.Kv 11 | 12 | @kv "kv" 13 | 14 | @spec fetch(binary | [binary], Keyword.t) :: Endpoint.response 15 | def fetch(key, opts \\ [], http_opts \\ []) do 16 | List.flatten([@kv, key]) 17 | |> build_url(opts) 18 | |> req_get([], http_opts) 19 | end 20 | 21 | @spec fetch!(binary | [binary], Keyword.t) :: Response.t | no_return 22 | def fetch!(key, opts \\ []) do 23 | case fetch(key, opts) do 24 | {:ok, value} -> 25 | value 26 | {:error, response} -> 27 | raise(Consul.ResponseError, response) 28 | end 29 | end 30 | 31 | @spec keys(binary | [binary]) :: Endpoint.response 32 | def keys(prefix) do 33 | List.flatten([@kv, prefix]) 34 | |> build_url(keys: true) 35 | |> req_get() 36 | end 37 | 38 | @spec keys!(binary | [binary]) :: Response.t | no_return 39 | def keys!(prefix) do 40 | case keys(prefix) do 41 | {:ok, value} -> 42 | value 43 | {:error, response} -> 44 | raise(Consul.ResponseError, response) 45 | end 46 | end 47 | 48 | @spec put(binary | [binary], term, Keyword.t) :: boolean 49 | def put(key, value, opts \\ []) do 50 | case List.flatten([@kv, key]) |> build_url(opts) |> req_put(to_string(value)) do 51 | {:ok, %{body: body}} -> 52 | body 53 | error -> 54 | error 55 | end 56 | end 57 | 58 | @spec put!(binary | [binary], term, Keyword.t) :: Response.t | no_return 59 | def put!(key, value, opts \\ []) do 60 | case put(key, value, opts) do 61 | {:error, response} -> 62 | raise(Consul.ResponseError, response) 63 | result -> 64 | result 65 | end 66 | end 67 | 68 | @spec delete(binary | [binary], Keyword.t) :: Endpoint.response 69 | def delete(key, opts \\ []) do 70 | case List.flatten([@kv, key]) |> build_url(opts) |> req_delete() do 71 | {:ok, %{body: body}} -> 72 | body 73 | error -> 74 | error 75 | end 76 | end 77 | 78 | @spec delete!(binary | [binary], Keyword.t) :: Response.t | no_return 79 | def delete!(key, opts \\ []) do 80 | case delete(key, opts) do 81 | {:ok, value} -> 82 | value 83 | {:error, response} -> 84 | raise(Consul.ResponseError, response) 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /lib/consul/request.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Request do 8 | use HTTPoison.Base 9 | 10 | def process_url(url) do 11 | case String.downcase(url) do 12 | <<"http://"::utf8, _::binary>> -> 13 | url 14 | <<"https://"::utf8, _::binary>> -> 15 | url 16 | _ -> 17 | Path.join("http://#{host}:#{port}/v1", url) 18 | end 19 | end 20 | 21 | def process_response_body(body) do 22 | case JSX.decode(body) do 23 | {:ok, decoded} -> 24 | decoded 25 | _ -> 26 | body 27 | end 28 | end 29 | 30 | # 31 | # Private API 32 | # 33 | 34 | defp host do 35 | Application.get_env(:consul, :host, "localhost") 36 | end 37 | 38 | defp port do 39 | Application.get_env(:consul, :port, 8500) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /lib/consul/response.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Response do 8 | @type t :: HTTPoison.Response.t 9 | 10 | @spec consul_index(t) :: integer | nil 11 | def consul_index(%{headers: %{"X-Consul-Index" => index}}), do: index 12 | def consul_index(_), do: nil 13 | end 14 | -------------------------------------------------------------------------------- /lib/consul/session.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Session do 8 | alias Consul.Endpoint 9 | use Consul.Endpoint, handler: Consul.Handler.Base 10 | 11 | @session "session" 12 | @create "create" 13 | @destroy "destroy" 14 | @info "info" 15 | @list "list" 16 | @node "node" 17 | @renew "renew" 18 | 19 | @spec create(map, Keyword.t) :: Endpoint.response 20 | def create(body, opts \\ []) do 21 | build_url([@session, @create], opts) 22 | |> req_put(JSX.encode!(body)) 23 | end 24 | 25 | @spec create!(map, Keyword.t) :: binary | no_return 26 | def create!(body, opts \\ []) do 27 | case create(body, opts) do 28 | {:ok, %{body: body}} -> 29 | body["ID"] 30 | {:error, response} -> 31 | raise Consul.ResponseError, response 32 | end 33 | end 34 | 35 | @spec destroy(binary, Keyword.t) :: Endpoint.response 36 | def destroy(session_id, opts \\ []) do 37 | build_url([@session, @destroy, session_id], opts) 38 | |> req_put("") 39 | end 40 | 41 | @spec info(binary, Keyword.t) :: Endpoint.response 42 | def info(session_id, opts \\ []) do 43 | build_url([@session, @info, session_id], opts) 44 | |> req_get() 45 | end 46 | 47 | @spec node(binary, Keyword.t) :: Endpoint.response 48 | def node(node_id, opts \\ []) do 49 | build_url([@session, @node, node_id], opts) 50 | |> req_get() 51 | end 52 | 53 | @spec list(Keyword.t) :: Endpoint.response 54 | def list(opts \\ []) do 55 | build_url([@session, @list], opts) 56 | |> req_get() 57 | end 58 | 59 | @spec renew(binary, Keyword.t) :: Endpoint.response 60 | def renew(session_id, opts \\ []) do 61 | build_url([@session, @renew, session_id], opts) 62 | |> req_put("") 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/consul/watch/event.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Watch.Event do 8 | alias Consul.Watch 9 | alias Consul.Event 10 | 11 | use GenServer 12 | import Consul.Response, only: [consul_index: 1] 13 | 14 | @wait "10m" 15 | @retry_ms 30 * 1000 16 | 17 | @spec start_link(binary, GenEvent.handler | {GenEvent.handler, list}) :: GenServer.on_start 18 | def start_link(name, handlers \\ []) do 19 | GenServer.start_link(__MODULE__, [name, handlers]) 20 | end 21 | 22 | # 23 | # GenServer callbacks 24 | # 25 | 26 | def init([name, handlers]) do 27 | {:ok, em} = GenEvent.start_link(handlers) 28 | 29 | Enum.each handlers, fn 30 | {handler, args} -> 31 | :ok = GenEvent.add_handler(em, handler, args) 32 | handler -> 33 | :ok = GenEvent.add_handler(em, handler, []) 34 | end 35 | 36 | {:ok, %{name: name, em: em, index: nil, l_time: nil}, 0} 37 | end 38 | 39 | def handle_info(:timeout, %{name: name, index: index, em: em, l_time: l_time} = state) do 40 | case Event.list(wait: @wait, index: index) do 41 | {:ok, response} -> 42 | events = Event.from_response(response) |> Enum.filter &(&1.name == name) 43 | new_l_time = Event.last_time(events) 44 | notify_events(events, em, index, l_time) 45 | {:noreply, %{state | index: consul_index(response), l_time: new_l_time}, 0} 46 | {:error, _response} -> 47 | {:noreply, state, @retry_ms} 48 | end 49 | end 50 | 51 | # 52 | # Private 53 | # 54 | 55 | defp notify_events([], _em, _index, _last_time), do: :ok 56 | defp notify_events(events, em, nil, _last_time) do 57 | Watch.Handler.notify_events(em, events) 58 | end 59 | defp notify_events(events, em, _index, last_time) do 60 | [latest|_] = Event.sort(events) 61 | if latest.l_time > last_time do 62 | Watch.Handler.notify_events(em, [latest]) 63 | end 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /lib/consul/watch/handler.ex: -------------------------------------------------------------------------------- 1 | # 2 | # The MIT License (MIT) 3 | # 4 | # Copyright (c) 2014-2015 Undead Labs, LLC 5 | # 6 | 7 | defmodule Consul.Watch.Handler do 8 | use Behaviour 9 | 10 | @type on_return :: {:ok, term} | :remove_handler 11 | 12 | @doc """ 13 | Handle notifications of Consul Events. When the handler is initially added all prior events 14 | will be notified to the handler, subsequent notifications will occur only upon change. 15 | 16 | See: http://www.consul.io/docs/agent/http.html#event 17 | """ 18 | defcallback handle_consul_events(events :: [Consul.Event.t], list) :: on_return 19 | 20 | defmacro __using__(_) do 21 | quote do 22 | @behaviour Consul.Watch.Handler 23 | use GenEvent 24 | 25 | @doc false 26 | def handle_event({:consul_events, events}, state) do 27 | handle_consul_events(events, state) 28 | end 29 | 30 | @doc false 31 | def handle_consul_events(_events, state) do 32 | {:ok, state} 33 | end 34 | 35 | defoverridable [handle_consul_events: 2] 36 | end 37 | end 38 | 39 | # 40 | # Public API 41 | # 42 | 43 | @doc """ 44 | Notifies handlers attached a Watch's event manager of Consul Events. 45 | """ 46 | @spec notify_events(GenEvent.manager, [Consul.Event.t]) :: :ok 47 | def notify_events(_, []), do: :ok 48 | def notify_events(manager, events) when is_pid(manager) and is_list(events) do 49 | GenEvent.ack_notify(manager, {:consul_events, events}) 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Consul.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :consul, 7 | version: "1.1.0", 8 | elixir: "~> 1.0", 9 | deps: deps(), 10 | package: package(), 11 | description: description() 12 | ] 13 | end 14 | 15 | def application do 16 | [ 17 | applications: [ 18 | :httpoison, 19 | :exjsx 20 | ], 21 | mod: {Consul, []}, 22 | env: [ 23 | host: "localhost", 24 | port: 8500, 25 | ] 26 | ] 27 | end 28 | 29 | defp deps do 30 | [ 31 | {:exjsx, "~> 3.0"}, 32 | {:httpoison, "~> 0.11.0"}, 33 | {:ex_doc, "~> 0.14", only: :dev} 34 | ] 35 | end 36 | 37 | defp description do 38 | """ 39 | An Elixir client for Consul's HTTP API 40 | """ 41 | end 42 | 43 | defp package do 44 | %{licenses: ["MIT"], 45 | maintainers: ["Jamie Winsor"], 46 | links: %{"Github" => "https://github.com/undeadlabs/consul-ex"}} 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/consul_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ConsulTest do 2 | use ExUnit.Case 3 | end 4 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | --------------------------------------------------------------------------------