summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTiago Freire <tcfonnet@gmail.com>2016-07-22 09:18:21 -0300
committerTiago Freire <tcfonnet@gmail.com>2016-07-25 10:51:14 -0300
commitcad149156ee46ec9feb4929dc1ed5d60a4f4f44e (patch)
treea36f8548f7b220f7846d7c4f77d03d5ab55bd390
[WIP] Init.
-rw-r--r--.gitignore20
-rw-r--r--README.md24
-rw-r--r--config/config.exs30
-rw-r--r--lib/http_client.ex18
-rw-r--r--lib/powerdnsx.ex2
-rw-r--r--lib/powerdnsx/config.ex36
-rw-r--r--lib/powerdnsx/managers/zones_manager.ex40
-rw-r--r--lib/powerdnsx/models/zone.ex25
-rw-r--r--lib/powerdnsx/validations/zone_validation.ex38
-rw-r--r--mix.exs22
-rw-r--r--mix.lock13
-rw-r--r--test/lib/powerdnsx/config_test.exs55
-rw-r--r--test/lib/powerdnsx/managers/zones_manager_test.exs80
-rw-r--r--test/lib/powerdnsx/models/zone_test.exs27
-rw-r--r--test/support/cassettes/zones_manager/create/valid_parameters.json1
-rw-r--r--test/support/cassettes/zones_manager/show/valid_record.json27
-rw-r--r--test/support/fake_config.exs8
-rw-r--r--test/test_helper.exs9
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
diff --git a/mix.exs b/mix.exs
new file mode 100644
index 0000000..b6069f5
--- /dev/null
+++ b/mix.exs
@@ -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