Rename project

This commit is contained in:
sonny 2021-01-02 14:40:28 +01:00
parent 49da8a2602
commit e780024140
8 changed files with 24 additions and 20 deletions

View file

@ -0,0 +1,39 @@
import os
import subprocess
from pathlib import Path
from dotenv import load_dotenv
env_path = Path("..") / ".env"
load_dotenv(dotenv_path=env_path)
def get_current_version():
if "VERSION" in os.environ:
return os.environ["VERSION"]
try:
output = subprocess.check_output(
["git", "describe", "--tags"], universal_newlines=True
)
return output.strip()
except (subprocess.CalledProcessError, OSError):
return ""
VERSION = get_current_version()
# Optionally use sentry integration
try:
from sentry_sdk import init as sentry_init
sentry_init(
dsn=os.environ.get("SENTRY_DSN"),
send_default_pii=False,
release=VERSION,
)
except ImportError:
pass

21
transip_client/cli.py Normal file
View file

@ -0,0 +1,21 @@
import click
from transip_client.main import detect
DEFAULT_DNS = "myip.opendns.com"
DEFAULT_DNS_NAME = "@resolver1.opendns.com"
DEFAULT_API_URL = "https://api.transip.nl/v6"
@click.command()
@click.argument("domains", envvar="DOMAINS", nargs=-1)
@click.argument("token", envvar="TOKEN")
@click.option("--dns", envvar="DNS", default=DEFAULT_DNS)
@click.option("--dns-name", envvar="DNS_NAME", default=DEFAULT_DNS_NAME)
@click.option("--api-url", envvar="API_URL", default=DEFAULT_API_URL)
@click.option("--read-only/--write", envvar="READ_ONLY", default=False)
def run(domains, token, dns, dns_name, api_url, read_only):
if not domains:
raise ValueError("No domain(s) specified")
detect(domains, (dns, dns_name), api_url, token, read_only)

103
transip_client/main.py Normal file
View file

@ -0,0 +1,103 @@
import json
import logging
import subprocess
from concurrent.futures import ThreadPoolExecutor, as_completed
import requests
logger = logging.getLogger(__name__)
def _get_ip(resolvers):
try:
output = subprocess.check_output(
["dig", "+short", *resolvers],
stderr=subprocess.STDOUT,
)
except subprocess.CalledProcessError as e:
raise OSError("Unable to retrieve current IP") from e
return output.decode("utf-8").strip()
def _get_domain(domain, token, api_url):
headers = {"Authorization": f"Bearer {token}"}
return requests.get(f"{api_url}/domains/{domain}/dns", headers=headers)
def _get_domain_data(domains, token, api_url):
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(_get_domain, domain, token, api_url): domain
for domain in domains
}
for future in as_completed(futures):
response = future.result()
domain = futures[future]
try:
response.raise_for_status()
except requests.HTTPError as e:
logger.exception(f"Failed retrieving information for {domain}")
continue
yield {"domain": domain, **response.json()}
def _update_domain(domain, payload, api_url, token):
headers = {"Authorization": f"Bearer {token}"}
return requests.put(
f"{api_url}/domains/{domain}/dns", data=json.dumps(payload), headers=headers
)
def _update_domains(updated_domains, api_url, token, read_only):
if read_only:
return
with ThreadPoolExecutor(max_workers=10) as executor:
futures = {
executor.submit(_update_domain, domain, entries, api_url, token): domain
for domain, entries in updated_domains.items()
}
for future in as_completed(futures):
response = future.result()
domain = futures[future]
try:
response.raise_for_status()
except request.HTTPError as e:
logger.exception(f"Unable to update domain {domain}")
continue
logger.info(f"Updated domain {domain}")
def detect(domains, resolvers, api_url, token, read_only):
ip = _get_ip(resolvers)
domain_data = _get_domain_data(domains, token, api_url)
updated_domains = {}
for data in domain_data:
dns_entries = data["dnsEntries"]
domain = data["domain"]
updated_entries = []
for dns_entry in dns_entries:
updated_entries.append(
{
**dns_entry,
"content": ip,
}
)
if dns_entries == updated_entries:
continue
updated_domains[domain] = {"dnsEntries": updated_entries}
_update_domains(updated_domains, api_url, token, read_only)

405
transip_client/tests.py Normal file
View file

@ -0,0 +1,405 @@
import json
from unittest import TestCase, skipIf
from unittest.mock import call, patch
from click.testing import CliRunner
from pkg_resources import get_distribution
from requests import HTTPError
from transip_client.cli import DEFAULT_API_URL, run
class RunTestCase(TestCase):
def setUp(self):
patcher = patch("transip_client.main.subprocess.check_output")
self.mocked_dns = patcher.start()
patcher = patch("transip_client.main.requests.get")
self.mocked_get = patcher.start()
patcher = patch("transip_client.main.requests.put")
self.mocked_put = patcher.start()
self.runner = CliRunner()
def test_simple(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.return_value = {
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
self.assertEqual(
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
)
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
expected_json = json.dumps(
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.420",
}
]
}
)
self.mocked_put.assert_called_with(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
data=expected_json,
headers={"Authorization": "Bearer TOKEN"},
)
def test_error_response(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.raise_for_status.side_effect = HTTPError
with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
error_log = logger.output[0]
self.assertIn("Failed retrieving information for foobar.com", error_log)
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
self.mocked_put.assert_not_called()
def test_matching_ip(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.return_value = {
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.420",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
result = self.runner.invoke(run, ["foobar.com", "TOKEN"])
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
self.mocked_put.assert_not_called()
def test_readonly(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.return_value = {
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
result = self.runner.invoke(run, ["foobar.com", "TOKEN", "--read-only"])
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
self.mocked_put.assert_not_called()
def test_different_api_url(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.return_value = {
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(
run, ["foobar.com", "TOKEN", "--api-url", "https://other-provider.com"]
)
self.assertEqual(
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
)
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
f"https://other-provider.com/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
expected_json = json.dumps(
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.420",
}
]
}
)
self.mocked_put.assert_called_with(
f"https://other-provider.com/domains/foobar.com/dns",
data=expected_json,
headers={"Authorization": "Bearer TOKEN"},
)
def test_env_var(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.return_value = {
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
}
with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(
run, ["foobar.com", "TOKEN"], env={"API_URL": "https://new-api.com"}
)
self.assertEqual(
logger.output, ["INFO:transip_client.main:Updated domain foobar.com"]
)
self.assertEqual(result.exit_code, 0)
self.mocked_get.assert_called_with(
"https://new-api.com/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
)
expected_json = json.dumps(
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.420",
}
]
}
)
self.mocked_put.assert_called_with(
"https://new-api.com/domains/foobar.com/dns",
data=expected_json,
headers={"Authorization": "Bearer TOKEN"},
)
# see https://click.palletsprojects.com/en/master/changelog/#version-8-0
@skipIf(
get_distribution("click").version < "8",
"Multiple arguments through env variables not supported",
)
def test_multi_arg_env_var(self):
self.mocked_dns.return_value = b"111.420\n"
self.mocked_get.return_value.json.side_effect = [
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foobar.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foobar.com",
},
],
},
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.421",
}
],
"_links": [
{
"rel": "self",
"link": "https://api.transip.nl/v6/domains/foofoo.com/dns",
},
{
"rel": "domain",
"link": "https://api.transip.nl/v6/domains/foofoo.com",
},
],
},
]
with self.assertLogs("transip_client.main", level="INFO") as logger:
result = self.runner.invoke(
run, ["TOKEN"], env={"DOMAINS": "foobar.com foofoo.com"}
)
self.assertIsNone(result.exception)
self.assertEqual(
logger.output,
[
"INFO:transip_client.main:Updated domain foobar.com",
"INFO:transip_client.main:Updated domain foofoo.com",
],
)
self.assertEqual(result.exit_code, 0)
expected_calls = [
call(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
headers={"Authorization": "Bearer TOKEN"},
),
call().raise_for_status(),
call().json(),
call(
f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
headers={"Authorization": "Bearer TOKEN"},
),
call().raise_for_status(),
call().json(),
]
self.mocked_get.assert_has_calls(expected_calls)
expected_json = json.dumps(
{
"dnsEntries": [
{
"name": "@",
"expire": 60,
"type": "A",
"content": "111.420",
}
]
}
)
expected_calls = [
call(
f"{DEFAULT_API_URL}/domains/foobar.com/dns",
data=expected_json,
headers={"Authorization": "Bearer TOKEN"},
),
call().raise_for_status(),
call(
f"{DEFAULT_API_URL}/domains/foofoo.com/dns",
data=expected_json,
headers={"Authorization": "Bearer TOKEN"},
),
call().raise_for_status(),
]
self.mocked_put.assert_has_calls(expected_calls)
def test_no_domains(self):
result = self.runner.invoke(run, ["TOKEN"])
self.assertEqual(result.exit_code, 1)
self.assertEqual(str(result.exception), "No domain(s) specified")
self.mocked_get.assert_not_called()
self.mocked_put.assert_not_called()