diff options
author | Tiago Freire <tcfonnet@gmail.com> | 2016-07-22 09:18:21 -0300 |
---|---|---|
committer | Tiago Freire <tcfonnet@gmail.com> | 2016-07-25 10:51:14 -0300 |
commit | cad149156ee46ec9feb4929dc1ed5d60a4f4f44e (patch) | |
tree | a36f8548f7b220f7846d7c4f77d03d5ab55bd390 |
[WIP] Init.
-rw-r--r-- | .gitignore | 20 | ||||
-rw-r--r-- | README.md | 24 | ||||
-rw-r--r-- | config/config.exs | 30 | ||||
-rw-r--r-- | lib/http_client.ex | 18 | ||||
-rw-r--r-- | lib/powerdnsx.ex | 2 | ||||
-rw-r--r-- | lib/powerdnsx/config.ex | 36 | ||||
-rw-r--r-- | lib/powerdnsx/managers/zones_manager.ex | 40 | ||||
-rw-r--r-- | lib/powerdnsx/models/zone.ex | 25 | ||||
-rw-r--r-- | lib/powerdnsx/validations/zone_validation.ex | 38 | ||||
-rw-r--r-- | mix.exs | 22 | ||||
-rw-r--r-- | mix.lock | 13 | ||||
-rw-r--r-- | test/lib/powerdnsx/config_test.exs | 55 | ||||
-rw-r--r-- | test/lib/powerdnsx/managers/zones_manager_test.exs | 80 | ||||
-rw-r--r-- | test/lib/powerdnsx/models/zone_test.exs | 27 | ||||
-rw-r--r-- | test/support/cassettes/zones_manager/create/valid_parameters.json | 1 | ||||
-rw-r--r-- | test/support/cassettes/zones_manager/show/valid_record.json | 27 | ||||
-rw-r--r-- | test/support/fake_config.exs | 8 | ||||
-rw-r--r-- | test/test_helper.exs | 9 |
18 files changed, 475 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67e65f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# The directory Mix will write compiled artifacts to. +/_build + +# If you run "mix test --cover", coverage assets end up here. +/cover + +# The directory Mix downloads your dependencies sources to. +/deps + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc + +# If the VM crashes, it generates a dump, let's ignore it too. +erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). +*.ez + +# Vim files +*.swp diff --git a/README.md b/README.md new file mode 100644 index 0000000..08ed556 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# PowerDNSx + +**TODO: Add description** + +## Installation + +If [available in Hex](https://hex.pm/docs/publish), the package can be installed as: + + 1. Add `powerdnsx` to your list of dependencies in `mix.exs`: + + ```elixir + def deps do + [{:powerdnsx, "~> 0.1.0"}] + end + ``` + + 2. Ensure `powerdnsx` is started before your application: + + ```elixir + def application do + [applications: [:powerdnsx]] + end + ``` + diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..2272984 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,30 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure for your application as: +# +# config :powerdnsx, key: :value +# +# And access this configuration in your application as: +# +# Application.get_env(:powerdnsx, :key) +# +# Or configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" diff --git a/lib/http_client.ex b/lib/http_client.ex new file mode 100644 index 0000000..d819087 --- /dev/null +++ b/lib/http_client.ex @@ -0,0 +1,18 @@ +defmodule PowerDNSx.HttpClient do + @moduledoc""" + Client to do http requests for PowerDns API + """ + + use HTTPoison.Base + + alias PowerDNSx.Config + + def process_url(url) do + Config.powerdns_url <> url + end + + defp process_request_headers(headers) do + custom = ["X-API-Key": Config.powerdns_token] + Enum.into(headers, custom) + end +end diff --git a/lib/powerdnsx.ex b/lib/powerdnsx.ex new file mode 100644 index 0000000..711fde6 --- /dev/null +++ b/lib/powerdnsx.ex @@ -0,0 +1,2 @@ +defmodule PowerDNSx do +end diff --git a/lib/powerdnsx/config.ex b/lib/powerdnsx/config.ex new file mode 100644 index 0000000..d172fa9 --- /dev/null +++ b/lib/powerdnsx/config.ex @@ -0,0 +1,36 @@ +defmodule PowerDNSx.Config do + defstruct url: "", + token: "" + + alias PowerDNSx.Config + + def data do + set_attr_value = &(Map.put(&2, &1, get_key(&1))) + + %Config{} + |> Map.from_struct + |> Map.keys + |> Enum.reduce(%Config{}, set_attr_value) + end + + def powerdns_url do + data.url + end + + def powerdns_token do + data.token + end + + ### + # Private + ### + + defp get_key(key) do + case Application.fetch_env(:powerdns, key) do + {:ok, {:system, env_var_name}} -> System.get_env(env_var_name) + {:ok, value} -> value + _ -> + raise "[PowerDNSx] PowerDNS #{Atom.to_string(key)} not configured." + end + end +end diff --git a/lib/powerdnsx/managers/zones_manager.ex b/lib/powerdnsx/managers/zones_manager.ex new file mode 100644 index 0000000..6e313d6 --- /dev/null +++ b/lib/powerdnsx/managers/zones_manager.ex @@ -0,0 +1,40 @@ +defmodule PowerDNSx.ZonesManager do + + @default_server "localhost" + + alias PowerDNSx.HttpClient + alias HTTPoison.Response, as: PoisonResp + alias PowerDNSx.Models.Zone + + def create(%Zone{} = zone, server_name \\ @default_server) do + case Zone.is_valid?(zone) do + true -> + zone_path(server_name) + |> HttpClient.post!(zone) + |> process_request_response + {false, errors} -> {:error, errors} + end + end + + def show(zone_name, server_name \\ @default_server) when is_bitstring(zone_name) do + zone_path(server_name, zone_name) + |> HttpClient.get! + |> process_request_response + end + + ### + # Private + ### + + defp zone_path(server_name) when is_bitstring(server_name) do + "/servers/#{server_name}/zones" + end + + defp zone_path(server_name, zone_name) when is_bitstring(server_name) do + zone_path(server_name) <> "/#{zone_name}" + end + + defp process_request_response(%PoisonResp{body: body, status_code: status} ) do + body |> Poison.decode!(as: %Zone{}) + end +end diff --git a/lib/powerdnsx/models/zone.ex b/lib/powerdnsx/models/zone.ex new file mode 100644 index 0000000..b2a9249 --- /dev/null +++ b/lib/powerdnsx/models/zone.ex @@ -0,0 +1,25 @@ +defmodule PowerDNSx.Models.Zone do + @moduledoc """ + Model for PowerDns zones, create and validate format + """ + + @valid_hostname_regex ~r/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/ + + defstruct name: nil, + kind: "Native", + masters: [], + nameservers: [], + records: [], + account: nil, + comments: [], + dnssec: false, + id: nil, + last_check: 0, + notified_serial: 0, + serial: nil, + soa_edit: "", + soa_edit_api: "", + url: nil + + +end diff --git a/lib/powerdnsx/validations/zone_validation.ex b/lib/powerdnsx/validations/zone_validation.ex new file mode 100644 index 0000000..a2abc6e --- /dev/null +++ b/lib/powerdnsx/validations/zone_validation.ex @@ -0,0 +1,38 @@ +defmodule PowerDNSx.ZoneValidator do + alias PowerDNSx.Models.Zone + + @valid_zone_name_reg ~r/^(([a-zA-Z]|[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z]|[A-Za-z][A-Za-z0-9\-]*[A-Za-z0-9])$/ + + def check(%Zone{} = zone) do + verify_attr = fn({attr, value}, errors)-> + validate_attr_value(attr, value) + end + + errors = Enum.reduce(zone, nil, verify_attr) + if errors, do: {:error, errors}, else: :ok + end + + ### + # Private + ### + + defp validate_attr_value(:name, zone_name) do + case validate_zone_name(zone_name) do + {true, true} -> nil + {false, _} -> {:error, %{name: "#{zone_name} is invalid."}} + {_, false} -> {:error, %{name: "#{zone_name} is invalid. Max lenght is 64."}} + {:format_error} -> {:error, %{name: "Name MUST be a string."}} + end + end + + defp validate_zone_name(zone_name) when is_bitstring(zone_name) do + {String.match?(zone_name, @valid_zone_name_reg), correct_length?(zone_name)} + end + + defp validate_zone_name(_), do: {:format_error} + + defp correct_length?(zone_name) do + name = zone_name |> String.split(".") |> hd + name |> String.length <= 63 + end +end @@ -0,0 +1,22 @@ +defmodule PowerDNSx.Mixfile do + use Mix.Project + + def project do + [app: :powerdnsx, + version: "0.1.0", + elixir: "~> 1.3", + build_embedded: Mix.env == :prod, + start_permanent: Mix.env == :prod, + deps: deps()] + end + + def application do + [applications: [:logger, :httpoison]] + end + + defp deps do + [{:httpoison, "~> 0.9.0"}, + {:poison, "~> 2.2"}, + {:exvcr, "~> 0.8.0", only: :test}] + end +end diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..6e6118c --- /dev/null +++ b/mix.lock @@ -0,0 +1,13 @@ +%{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, + "exactor": {:hex, :exactor, "2.2.0", "2a7418b82d974fe8276edd62c1facf4a9dc06339cdf11b5dcd10359e107fe5c3", [:mix], []}, + "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, + "exvcr": {:hex, :exvcr, "0.8.0", "61b8da4723150938f612af8a2f885709af7e486e4deac4b454a161de3430e699", [:mix], [{:httpoison, "~> 0.8", [hex: :httpoison, optional: true]}, {:httpotion, "~> 3.0", [hex: :httpotion, optional: true]}, {:ibrowse, "~> 4.2.2", [hex: :ibrowse, optional: true]}, {:exjsx, "~> 3.2", [hex: :exjsx, optional: false]}, {:exactor, "~> 2.2", [hex: :exactor, optional: false]}, {:meck, "~> 0.8.3", [hex: :meck, optional: false]}]}, + "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:certifi, "0.4.0", [hex: :certifi, optional: false]}]}, + "httpoison": {:hex, :httpoison, "0.9.0", "68187a2daddfabbe7ca8f7d75ef227f89f0e1507f7eecb67e4536b3c516faddb", [:mix], [{:hackney, "~> 1.6.0", [hex: :hackney, optional: false]}]}, + "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, + "jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []}, + "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, + "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, + "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, + "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}, + "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} diff --git a/test/lib/powerdnsx/config_test.exs b/test/lib/powerdnsx/config_test.exs new file mode 100644 index 0000000..4a9a732 --- /dev/null +++ b/test/lib/powerdnsx/config_test.exs @@ -0,0 +1,55 @@ +defmodule PowerDNSx.ConfigTest do + + use ExUnit.Case, async: true + + alias PowerDNSx.FakeConfig, as: Config + + setup with_valid_url, do: Config.set_url + setup with_valid_token, do: Config.set_token + setup with_valid_token_and_url, do: Config.set_url && Config.set_token + + describe "Config.powerdns_token/0" do + test "Using Env vars", with_valid_url do + env_token = "3nv_S3cr37_T0k3n" + System.put_env("POWERDNS_TOKEN", env_token) + Application.put_env(:powerdns, :token, {:system, "POWERDNS_TOKEN"}) + assert PowerDNSx.Config.powerdns_token == env_token + end + + test "using application config", with_valid_token_and_url do + assert PowerDNSx.Config.powerdns_token == "4pp_S3cr37_T0k3n" + end + + test "given none token config", with_valid_url do + Application.delete_env(:powerdns, :token) + expected_error = "[PowerDNSx] PowerDNS token not configured." + + assert_raise RuntimeError, expected_error, fn -> + PowerDNSx.Config.powerdns_token + end + end + end + + describe "Config.powerdns_url/0" do + test "Using Env vars", with_valid_token do + env_url = "https://env-powerdns.test" + System.put_env("POWERDNS_URL", env_url) + Application.put_env(:powerdns, :url, {:system, "POWERDNS_URL"}) + assert PowerDNSx.Config.powerdns_url == env_url + end + + test "using application config", with_valid_token_and_url do + assert PowerDNSx.Config.powerdns_url == "https://app-config-powerdns.test" + end + + test "given none url config", with_valid_token do + Application.delete_env(:powerdns, :url) + expected_error = "[PowerDNSx] PowerDNS url not configured." + assert_raise RuntimeError, expected_error, fn -> + PowerDNSx.Config.powerdns_url + end + end + end + + +end diff --git a/test/lib/powerdnsx/managers/zones_manager_test.exs b/test/lib/powerdnsx/managers/zones_manager_test.exs new file mode 100644 index 0000000..cc793d0 --- /dev/null +++ b/test/lib/powerdnsx/managers/zones_manager_test.exs @@ -0,0 +1,80 @@ +defmodule PowerDNSx.ZonesManagerTest do + + use ExUnit.Case, async: true + use ExVCR.Mock, adapter: ExVCR.Adapter.Hackney + + alias PowerDNSx.ZonesManager + alias PowerDNSx.Models.Zone + alias PowerDNSx.FakeConfig, as: Config + + @valid_zone_loca %Zone{name: "devtiagofreire.art.br"} + + @valid_zone_test %Zone{name: "my-domain.tst", + serial: 2016060601, + comments: ["Test comment"] } + + @expected_records [ + %{"content" => "ns2.my-powerdns.api", "disabled" => false, + "name" => "my-domain.tst", "ttl" => 3600, "type" => "NS"}, + %{"content" => "ns1.my-powerdns.api", "disabled" => false, + "name" => "my-domain.tst", "ttl" => 3600, "type" => "NS"}, + %{"content" => "a.misconfigured.powerdns.server " <> + "hostmaster.my-domain.tst " <> + "2016060601 10800 3600 604800 3600", + "disabled" => false, "name" => "my-domain.tst", "ttl" => 3600, + "type" => "SOA"} + ] + + @expected_zone %Zone{name: "my-domain.tst", + id: "my-domain.tst.", + account: "", + serial: 2016060601, + url: "/servers/localhost/zones/my-domain.tst.", + records: @expected_records} + + setup_all do + #Config.set_url + #Config.set_token + + pwdns_url_loca = "http://cpro36999.systemintegration.locaweb.com.br" + pwdns_url_test = "http://my-powerdns.api" + Application.put_env(:powerdns, :url, pwdns_url_test) + + pwdns_token_loca = "Locaweb2016" + pwdns_token_test = "S3cr3t-T0k3n" + Application.put_env(:powerdns, :token, pwdns_token_test) + + ExVCR.Config.cassette_library_dir("test/support/cassettes", + "test/support/custom_cassettes") + HTTPoison.start + end + + describe "ZoneManager.create/2" do + @tag :zones_manager_create + test "type of return given correct parameters" do + use_cassette "zones_manager/create/valid_parameters" do + zone = ZonesManager.create(@valid_zone_test) + assert zone.__struct__ == PowerDNSx.Models.Zone + end + end + end + + describe "ZonesManager.show/2" do + @tag :zones_manager_show + test "type of return given a correct zone name" do + use_cassette "zones_manager/show/valid_record" do + zone = ZonesManager.show(@valid_zone_test.name) + assert zone.__struct__ == PowerDNSx.Models.Zone + end + end + + @tag :zones_manager_show + test "values in return given a correct zone name" do + use_cassette "zones_manager/show/valid_record" do + zone = ZonesManager.show(@valid_zone_test.name) + assert zone == @expected_zone + end + end + end + +end diff --git a/test/lib/powerdnsx/models/zone_test.exs b/test/lib/powerdnsx/models/zone_test.exs new file mode 100644 index 0000000..8a61602 --- /dev/null +++ b/test/lib/powerdnsx/models/zone_test.exs @@ -0,0 +1,27 @@ +# defmodule PowerDNSx.Models.ZoneTest do +# +# use ExUnit.Case, async: true +# +# alias PowerDNSx.Models.Zone +# +# describe "Zone.build/1" do +# test "build zone with all attributes" do +# ns_servers = ["ns1.powerdnsx.tst", "ns2.powerdnsx.tst"] +# +# zone_params = %{id: nil, +# name: "test.tst", +# nameservers: ns_servers, +# account: "pdnaccount", +# kind: "Native", +# masters: [], +# records: [], +# serial: nil, +# comments: [], +# soa_edit: nil, +# soa_edit_api: nil} +# +# zone_built = Zone.build(zone_params) +# assert zone_params === Map.from_struct(zone_built) +# end +# end +# end diff --git a/test/support/cassettes/zones_manager/create/valid_parameters.json b/test/support/cassettes/zones_manager/create/valid_parameters.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/test/support/cassettes/zones_manager/create/valid_parameters.json @@ -0,0 +1 @@ +[]
\ No newline at end of file diff --git a/test/support/cassettes/zones_manager/show/valid_record.json b/test/support/cassettes/zones_manager/show/valid_record.json new file mode 100644 index 0000000..75d348d --- /dev/null +++ b/test/support/cassettes/zones_manager/show/valid_record.json @@ -0,0 +1,27 @@ +[ + { + "request": { + "body": "", + "headers": { + "X-API-Key": "S3cr3t-T0k3n" + }, + "method": "get", + "options": [], + "request_body": "", + "url": "http://my-powerdns.api/servers/localhost/zones/my-domain.tst" + }, + "response": { + "body": "{\"id\":\"my-domain.tst.\",\"url\":\"/servers/localhost/zones/my-domain.tst.\",\"name\":\"my-domain.tst\",\"kind\":\"Native\",\"dnssec\":false,\"account\":\"\",\"masters\":[],\"serial\":2016060601,\"notified_serial\":0,\"last_check\":0,\"soa_edit_api\":\"\",\"soa_edit\":\"\",\"records\":[{\"name\":\"my-domain.tst\",\"type\":\"NS\",\"ttl\":3600,\"disabled\":false,\"content\":\"ns2.my-powerdns.api\"},{\"name\":\"my-domain.tst\",\"type\":\"NS\",\"ttl\":3600,\"disabled\":false,\"content\":\"ns1.my-powerdns.api\"},{\"name\":\"my-domain.tst\",\"type\":\"SOA\",\"ttl\":3600,\"disabled\":false,\"content\":\"a.misconfigured.powerdns.server hostmaster.my-domain.tst 2016060601 10800 3600 604800 3600\"}],\"comments\":[]}", + "headers": { + "Transfer-Encoding": "chunked", + "Access-Control-Allow-Origin": "*", + "Connection": "close", + "Content-Length": "717", + "Content-Type": "application/json", + "Server": "PowerDNS/3.4.8" + }, + "status_code": 200, + "type": "ok" + } + } +] diff --git a/test/support/fake_config.exs b/test/support/fake_config.exs new file mode 100644 index 0000000..e624d8e --- /dev/null +++ b/test/support/fake_config.exs @@ -0,0 +1,8 @@ +defmodule PowerDNSx.FakeConfig do + @app_config_token "4pp_S3cr37_T0k3n" + @app_config_url "https://app-config-powerdns.test" + + + def set_url, do: Application.put_env(:powerdns, :url, @app_config_url) + def set_token, do: Application.put_env(:powerdns, :token, @app_config_token) +end diff --git a/test/test_helper.exs b/test/test_helper.exs new file mode 100644 index 0000000..2fc6291 --- /dev/null +++ b/test/test_helper.exs @@ -0,0 +1,9 @@ +ExUnit.start() + +{:ok, files} = File.ls("./test/support/") + +Enum.each files, fn(file) -> + if String.match?(file, ~r/(.*).exs$/) do + Code.require_file "support/#{file}", __DIR__ + end +end |