├── .editorconfig ├── .formatter.exs ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── config └── config.exs ├── lib ├── libcluster_ec2.ex └── strategy │ └── tags.ex ├── mix.exs ├── mix.lock └── test └── test_helper.exs /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # .editorconfig for Elixir projects 4 | # https://git.io/elixir-editorconfig 5 | 6 | # top-most EditorConfig file 7 | root = true 8 | 9 | [*] 10 | indent_style = space 11 | indent_size = 2 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [*.{md, markdown, eex}] 18 | trim_trailing_whitespace = false 19 | 20 | [Makefile] 21 | indent_style = tab 22 | 23 | #[*.bat] 24 | #end_of_line = crlf -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | # files to format 3 | inputs: ["mix.exs", "{config,lib,test}/**/*.{ex,exs}"], 4 | locals_without_parens: [config: 0], 5 | line_length: 120, 6 | rename_deprecated_at: "1.6.1" 7 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # The directory Mix will write compiled artifacts to. 2 | /_build 3 | 4 | # If you run "mix test --cover", coverage assets end up here. 5 | /cover 6 | 7 | # The directory Mix downloads your dependencies sources to. 8 | /deps 9 | 10 | # Where 3rd-party dependencies like ExDoc output generated docs. 11 | /doc 12 | 13 | # Ignore .fetch files in case you like to edit your project deps locally. 14 | /.fetch 15 | 16 | # If the VM crashes, it generates a dump, let's ignore it too. 17 | erl_crash.dump 18 | 19 | # Also ignore archive artifacts (built via "mix archive.build"). 20 | *.ez 21 | 22 | # Ignore elixir language server files 23 | /.elixir_ls -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.8.2 2 | - Bug fix: Fetch instance metadata at startup only https://github.com/kyleaa/libcluster_ec2/pull/38 3 | 4 | v0.8.1 5 | - Bug fix: relax ex_aws requirement https://github.com/kyleaa/libcluster_ec2/pull/34 6 | 7 | v0.8.0 8 | - Enhancement: Support IMDSv2 (Requires ExAws 2.3.2 minimum) https://github.com/kyleaa/libcluster_ec2/pull/33 9 | 10 | v0.7.0 11 | - Enhancement: auto reconnect when disconnected by some reason https://github.com/kyleaa/libcluster_ec2/pull/27 12 | - Dependency Cleanup: Remove Poison dependency https://github.com/kyleaa/libcluster_ec2/pull/23 13 | 14 | v0.6.0 15 | - Enhancement: block on startup while attempting first load https://github.com/kyleaa/libcluster_ec2/pull/20 16 | - Enhancement: allow optional disable of debug logging https://github.com/kyleaa/libcluster_ec2/pull/21/files 17 | 18 | v0.5.0 19 | - Enhancement: add ability to configure ip_to_nodename function. https://github.com/kyleaa/libcluster_ec2/pull/17 20 | 21 | v0.4.2 22 | - Bug fix: correct public IP address detection with tags strategy https://github.com/kyleaa/libcluster_ec2/pull/16 23 | 24 | v0.4.1 25 | - Bug fix: skip EC2 instances that are not running 26 | 27 | v0.4.0 28 | - Updated to Tesla 1.0 and support for libcluster 3.0) 29 | 30 | v0.3.0 31 | - Moved to ex_aws 2.0 https://github.com/kyleaa/libcluster_ec2/issues/8 32 | 33 | v0.2.1 34 | - Bug fix: Reconnection error handling. https://github.com/kyleaa/libcluster_ec2/pull/6 35 | 36 | v0.2.0 37 | - Enhancement: Add error handling for AWS/EC2 API calls. In the event of a failure to communicate, maintain current node list. https://github.com/kyleaa/libcluster_ec2/pull/4 38 | 39 | v0.1.3 40 | - Enhancement: automatically determine instance region. https://github.com/kyleaa/libcluster_ec2/pull/3 41 | 42 | v0.1.2 43 | - Enhancement: add default config values. https://github.com/kyleaa/libcluster_ec2/pull/2 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kyle A Anderson 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ClusterEC2 2 | ========== 3 | 4 | This is an EC2 clustering strategy for [libcluster](https://hexdocs.pm/libcluster/). It currently supports identifying nodes based on EC2 tags. 5 | 6 | The default `Tags` strategy uses [ex_aws](https://github.com/ex-aws/ex_aws) to query the EC2 DescribeInstances API endpoint. Access to this API should be granted to the EC2 instance profile. See the ExAws docs for additional configuration options. 7 | 8 | ``` 9 | config :libcluster, 10 | topologies: [ 11 | example: [ 12 | strategy: ClusterEC2.Strategy.Tags, 13 | config: [ 14 | ec2_tagname: "elasticbeanstalk:environment-name" 15 | ], 16 | ] 17 | ] 18 | ``` 19 | 20 | ## Installation 21 | 22 | The package can be installed 23 | by adding `libcluster_ec2` to your list of dependencies in `mix.exs`: 24 | 25 | ```elixir 26 | def deps do 27 | [{:libcluster_ec2, "~> 0.5"}] 28 | end 29 | ``` 30 | 31 | ## AWS IAM Requirements 32 | 33 | Instances must have an instance role attached. There are two permissions required: 34 | * `ec2:DescribeInstances` - Required to determine tag values of the current running instance. Can be restricted by Resource to the current instance running the application 35 | * `ec2:DescribeTags` - Required to identify other instances with the same tags 36 | 37 | ``` 38 | { 39 | "Version": "2012-10-17", 40 | "Statement": [ 41 | { 42 | "Sid": "VisualEditor0", 43 | "Effect": "Allow", 44 | "Action": [ 45 | "ec2:DescribeInstances", 46 | "ec2:DescribeTags" 47 | ], 48 | "Resource": "*" 49 | } 50 | ] 51 | } 52 | ``` -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | import Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :libcluster_ec2, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:libcluster_ec2, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | 32 | if Mix.env() == :test do 33 | config :ex_aws, 34 | access_key_id: "xxx", 35 | secret_access_key: "xxx" 36 | end 37 | -------------------------------------------------------------------------------- /lib/libcluster_ec2.ex: -------------------------------------------------------------------------------- 1 | defmodule ClusterEC2 do 2 | @moduledoc File.read!("#{__DIR__}/../README.md") 3 | 4 | @doc """ 5 | Queries the local EC2 instance metadata API to determine the instance ID of the current instance. 6 | """ 7 | @spec local_instance_id() :: binary() 8 | def local_instance_id, do: get_metadata("/instance-id/") 9 | 10 | @doc """ 11 | Queries the local EC2 instance metadata API to determine the aws resource region of the current instance. 12 | """ 13 | @spec instance_region() :: binary() 14 | def instance_region do 15 | get_metadata("/placement/availability-zone/") 16 | |> String.slice(0..-2//1) 17 | end 18 | 19 | defp get_metadata(path) do 20 | ExAws.Config.new(:ec2) 21 | |> ExAws.InstanceMeta.request("http://169.254.169.254/latest/meta-data#{path}") 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/strategy/tags.ex: -------------------------------------------------------------------------------- 1 | defmodule ClusterEC2.Strategy.Tags do 2 | @moduledoc """ 3 | This clustering strategy works by loading all instances that have the given 4 | tag associated with them. 5 | 6 | All instances must be started with the same app name and have security groups 7 | configured to allow inter-node communication. 8 | 9 | config :libcluster, 10 | topologies: [ 11 | tags_example: [ 12 | strategy: #{__MODULE__}, 13 | config: [ 14 | ec2_tagname: "mytag", 15 | ec2_tagvalue: "tagvalue", 16 | app_prefix: "app", 17 | ip_to_nodename: &my_nodename_func/2, 18 | ip_type: :private, 19 | polling_interval: 10_000]]], 20 | show_debug: false 21 | 22 | ## Configuration Options 23 | 24 | | Key | Required | Description | 25 | | --- | -------- | ----------- | 26 | | `:ec2_tagname` | yes | Name of the EC2 instance tag to look for. | 27 | | `:ec2_tagvalue` | no | Can be passed a static value (string), a 0-arity function, or a 1-arity function (which will be passed the value of `:ec2_tagname` at invocation). | 28 | | `:app_prefix` | no | Will be prepended to the node's private IP address to create the node name. | 29 | | `:ip_type` | no | One of :private or :public, defaults to :private | 30 | | `:ip_to_nodename` | no | defaults to `app_prefix@ip` but can be used to override the nodename | 31 | | `:polling_interval` | no | Number of milliseconds to wait between polls to the EC2 api. Defaults to 5_000 | 32 | | `:show_debug` | no | True or false, whether or not to show the debug log. Defaults to true | 33 | """ 34 | 35 | use GenServer 36 | use Cluster.Strategy 37 | import Cluster.Logger 38 | import SweetXml, only: [sigil_x: 2] 39 | 40 | alias Cluster.Strategy.State 41 | 42 | @default_polling_interval 5_000 43 | 44 | def start_link(opts) do 45 | Application.ensure_all_started(:ex_aws) 46 | GenServer.start_link(__MODULE__, opts) 47 | end 48 | 49 | # libcluster ~> 3.0 50 | @impl GenServer 51 | def init([%State{} = state]) do 52 | instance_meta = [ 53 | instance_id: ClusterEC2.local_instance_id(), 54 | region: ClusterEC2.instance_region() 55 | ] 56 | 57 | state = 58 | state 59 | |> Map.put(:meta, MapSet.new()) 60 | |> Map.put(:config, state.config ++ instance_meta) 61 | 62 | {:ok, load(state)} 63 | end 64 | 65 | # libcluster ~> 2.0 66 | def init(opts) do 67 | instance_meta = [ 68 | instance_id: ClusterEC2.local_instance_id(), 69 | region: ClusterEC2.instance_region() 70 | ] 71 | 72 | state = %State{ 73 | topology: Keyword.fetch!(opts, :topology), 74 | connect: Keyword.fetch!(opts, :connect), 75 | disconnect: Keyword.fetch!(opts, :disconnect), 76 | list_nodes: Keyword.fetch!(opts, :list_nodes), 77 | config: Keyword.fetch!(opts, :config) ++ instance_meta, 78 | meta: MapSet.new([]) 79 | } 80 | 81 | {:ok, load(state)} 82 | end 83 | 84 | @impl GenServer 85 | def handle_info(:timeout, state) do 86 | handle_info(:load, state) 87 | end 88 | 89 | def handle_info(:load, %State{} = state) do 90 | {:noreply, load(state)} 91 | end 92 | 93 | def handle_info(_, state) do 94 | {:noreply, state} 95 | end 96 | 97 | defp load(%State{topology: topology, connect: connect, disconnect: disconnect, list_nodes: list_nodes} = state) do 98 | case get_nodes(state) do 99 | {:ok, new_nodelist} -> 100 | removed = MapSet.difference(state.meta, new_nodelist) 101 | 102 | new_nodelist = 103 | case Cluster.Strategy.disconnect_nodes(topology, disconnect, list_nodes, MapSet.to_list(removed)) do 104 | :ok -> 105 | new_nodelist 106 | 107 | {:error, bad_nodes} -> 108 | # Add back the nodes which should have been removed, but which couldn't be for some reason 109 | Enum.reduce(bad_nodes, new_nodelist, fn {n, _}, acc -> 110 | MapSet.put(acc, n) 111 | end) 112 | end 113 | 114 | new_nodelist = 115 | case Cluster.Strategy.connect_nodes(topology, connect, list_nodes, MapSet.to_list(new_nodelist)) do 116 | :ok -> 117 | new_nodelist 118 | 119 | {:error, bad_nodes} -> 120 | # Remove the nodes which should have been added, but couldn't be for some reason 121 | Enum.reduce(bad_nodes, new_nodelist, fn {n, _}, acc -> 122 | MapSet.delete(acc, n) 123 | end) 124 | end 125 | 126 | Process.send_after(self(), :load, Keyword.get(state.config, :polling_interval, @default_polling_interval)) 127 | %{state | :meta => new_nodelist} 128 | 129 | _ -> 130 | Process.send_after(self(), :load, Keyword.get(state.config, :polling_interval, @default_polling_interval)) 131 | state 132 | end 133 | end 134 | 135 | @spec get_nodes(State.t()) :: {:ok, [atom()]} | {:error, []} 136 | defp get_nodes(%State{topology: topology, config: config}) do 137 | instance_id = Keyword.get(config, :instance_id, "") 138 | region = Keyword.get(config, :region, "") 139 | tag_name = Keyword.fetch!(config, :ec2_tagname) 140 | tag_value = Keyword.get(config, :ec2_tagvalue, &local_instance_tag_value(&1, instance_id, region)) 141 | app_prefix = Keyword.get(config, :app_prefix, "app") 142 | ip_to_nodename = Keyword.get(config, :ip_to_nodename, &ip_to_nodename/2) 143 | show_debug? = Keyword.get(config, :show_debug, true) 144 | 145 | cond do 146 | tag_name != nil and tag_value != nil and app_prefix != nil and instance_id != "" and region != "" -> 147 | params = [filters: ["tag:#{tag_name}": fetch_tag_value(tag_name, tag_value), "instance-state-name": "running"]] 148 | request = ExAws.EC2.describe_instances(params) 149 | require Logger 150 | if show_debug?, do: Logger.debug("#{inspect(request)}") 151 | 152 | case ExAws.request(request, region: region) do 153 | {:ok, %{body: body}} -> 154 | resp = 155 | body 156 | |> SweetXml.xpath(ip_xpath(Keyword.get(config, :ip_type, :private))) 157 | |> ip_to_nodename.(app_prefix) 158 | 159 | {:ok, MapSet.new(resp)} 160 | 161 | _ -> 162 | {:error, []} 163 | end 164 | 165 | instance_id == "" -> 166 | warn(topology, "instance id could not be fetched!") 167 | {:error, []} 168 | 169 | region == "" -> 170 | warn(topology, "region could not be fetched!") 171 | {:error, []} 172 | 173 | tag_name == nil -> 174 | warn(topology, "ec2 tags strategy is selected, but :ec2_tagname is not configured!") 175 | {:error, []} 176 | 177 | :else -> 178 | warn(topology, "ec2 tags strategy is selected, but is not configured!") 179 | {:error, []} 180 | end 181 | end 182 | 183 | defp local_instance_tag_value(tag_name, instance_id, region) do 184 | ExAws.EC2.describe_instances(instance_id: instance_id) 185 | |> local_instance_tags(region) 186 | |> Map.get(tag_name) 187 | end 188 | 189 | defp local_instance_tags(body, region) do 190 | case ExAws.request(body, region: region) do 191 | {:ok, body} -> extract_tags(body) 192 | {:error, _} -> %{} 193 | end 194 | end 195 | 196 | defp extract_tags(%{body: xml}) do 197 | xml 198 | |> SweetXml.xpath( 199 | ~x"//DescribeInstancesResponse/reservationSet/item/instancesSet/item/tagSet/item"l, 200 | key: ~x"./key/text()"s, 201 | value: ~x"./value/text()"s 202 | ) 203 | |> Stream.map(fn %{key: k, value: v} -> {k, v} end) 204 | |> Enum.into(%{}) 205 | end 206 | 207 | defp ip_xpath(:private), 208 | do: ~x"//DescribeInstancesResponse/reservationSet/item/instancesSet/item/privateIpAddress/text()"ls 209 | 210 | defp ip_xpath(:public), 211 | do: ~x"//DescribeInstancesResponse/reservationSet/item/instancesSet/item/ipAddress/text()"ls 212 | 213 | defp fetch_tag_value(_k, v) when is_function(v, 0), do: v.() 214 | defp fetch_tag_value(k, v) when is_function(v, 1), do: v.(k) 215 | defp fetch_tag_value(_k, v), do: v 216 | 217 | defp ip_to_nodename(list, app_prefix) when is_list(list) do 218 | list 219 | |> Enum.map(fn ip -> 220 | :"#{app_prefix}@#{ip}" 221 | end) 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ClusterEC2.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :libcluster_ec2, 7 | version: "0.8.2", 8 | elixir: "~> 1.4", 9 | name: "libcluster_ec2", 10 | source_url: "https://github.com/kyleaa/libcluster_ec2", 11 | homepage_url: "https://github.com/kyleaa/libcluster_ec2", 12 | description: description(), 13 | build_embedded: Mix.env() == :prod, 14 | start_permanent: Mix.env() == :prod, 15 | deps: deps(), 16 | package: package() 17 | ] 18 | end 19 | 20 | # Configuration for the OTP application 21 | # 22 | # Type "mix help compile.app" for more information 23 | def application do 24 | # Specify extra applications you'll use from Erlang/Elixir 25 | [extra_applications: [:logger]] 26 | end 27 | 28 | defp deps do 29 | [ 30 | {:libcluster, "~> 2.0 or ~> 3.0"}, 31 | {:ex_aws, ">= 2.3.2"}, 32 | {:ex_aws_ec2, "~> 2.0"}, 33 | {:sweet_xml, "~> 0.6"}, 34 | {:hackney, "~> 1.8"}, 35 | {:ex_doc, ">= 0.0.0", only: :dev} 36 | ] 37 | end 38 | 39 | defp description do 40 | """ 41 | EC2 clustering strategy for libcluster 42 | """ 43 | end 44 | 45 | def package do 46 | [ 47 | maintainers: ["Kyle Anderson"], 48 | licenses: ["MIT License"], 49 | links: %{ 50 | "GitHub" => "https://github.com/kyleaa/libcluster_ec2.git" 51 | } 52 | ] 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, 3 | "earmark": {:hex, :earmark, "1.3.1", "73812f447f7a42358d3ba79283cfa3075a7580a3a2ed457616d6517ac3738cb9", [:mix], [], "hexpm", "000aaeff08919e95e7aea13e4af7b2b9734577b3e6a7c50ee31ee88cab6ec4fb"}, 4 | "earmark_parser": {:hex, :earmark_parser, "1.4.20", "89970db71b11b6b89759ce16807e857df154f8df3e807b2920a8c39834a9e5cf", [:mix], [], "hexpm", "1eb0d2dabeeeff200e0d17dc3048a6045aab271f73ebb82e416464832eb57bdd"}, 5 | "ex_aws": {:hex, :ex_aws, "2.3.4", "f22c9a9a8f5afdcf44a824df586954240f71d157cecb1ecfd5665d219f782322", [:mix], [{:configparser_ex, "~> 4.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8 or ~> 3.0", [hex: :jsx, repo: "hexpm", optional: true]}, {:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:sweet_xml, "~> 0.7", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "849b77125af5c405d38675de75cdddf5b37a7a6ea3982929a516586e338e0ce8"}, 6 | "ex_aws_ec2": {:hex, :ex_aws_ec2, "2.0.1", "1558c9bf4044048fb2b3b1ca3d7a62c65666959432ae29ef1d584258926d8fc0", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}], "hexpm", "adb86c51c2bcf630a739261bfff9293d2bf96946bf1a2d40d4f78e56a349a52b"}, 7 | "ex_doc": {:hex, :ex_doc, "0.28.2", "e031c7d1a9fc40959da7bf89e2dc269ddc5de631f9bd0e326cbddf7d8085a9da", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "51ee866993ffbd0e41c084a7677c570d0fc50cb85c6b5e76f8d936d9587fa719"}, 8 | "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, 9 | "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, 10 | "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, 11 | "libcluster": {:hex, :libcluster, "3.3.1", "e7a4875cd1290cee7a693d6bd46076863e9e433708b01339783de6eff5b7f0aa", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "b575ca63c1cd84e01f3fa0fc45e6eb945c1ee7ae8d441d33def999075e9e5398"}, 12 | "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"}, 13 | "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, 14 | "makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"}, 15 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, 16 | "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, 17 | "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, 18 | "nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"}, 19 | "parse_trans": {:hex, :parse_trans, "3.4.1", "6e6aa8167cb44cc8f39441d05193be6e6f4e7c2946cb2759f015f8c56b76e5ff", [:rebar3], [], "hexpm", "620a406ce75dada827b82e453c19cf06776be266f5a67cff34e1ef2cbb60e49a"}, 20 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"}, 21 | "sweet_xml": {:hex, :sweet_xml, "0.7.4", "a8b7e1ce7ecd775c7e8a65d501bc2cd933bff3a9c41ab763f5105688ef485d08", [:mix], [], "hexpm", "e7c4b0bdbf460c928234951def54fe87edf1a170f6896675443279e2dbeba167"}, 22 | "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, 23 | "tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"}, 24 | "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, 25 | } 26 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | --------------------------------------------------------------------------------