Skip to content

Commit

Permalink
Merge branch 'release/v1.5.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
qdii committed Sep 30, 2023
2 parents e575b2c + d579a9c commit e436f5c
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 9 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,28 @@ This script requires the following permissions on `/domain/zone/myzone.fr`: GET
to fetch the current records and compare them with the intent, POST to create
new records and DELETE to remove records.

## Hermetic environment

### Building

[Bazel](http://bazel.build) is used to build and test the application in a
hermetic, sandboxed environment.

The following command will download the environment and build a cross-platform
python binary which executes within this environment.

```shell
$ bazel build //src:ovh_reconciler
```

### Testing

To run all unit tests, use the following command:

```shell
$ bazel test //src:ovh_reconciler_test
```

## Flags

- `--input`: Can be either a path towards a file containing the source of truth
Expand Down
6 changes: 3 additions & 3 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
# At the time of writing the most recent version is python 3.11.
http_archive(
name = "rules_python",
sha256 = "0a8003b044294d7840ac7d9d73eef05d6ceb682d7516781a4ec62eeb34702578",
strip_prefix = "rules_python-0.24.0",
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.24.0.tar.gz",
sha256 = "5868e73107a8e85d8f323806e60cad7283f34b32163ea6ff1020cf27abef6036",
strip_prefix = "rules_python-0.25.0",
url = "https://github.com/bazelbuild/rules_python/archive/refs/tags/0.25.0.tar.gz",
)
load("@rules_python//python:repositories.bzl", "py_repositories")
py_repositories()
Expand Down
7 changes: 7 additions & 0 deletions src/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ py_binary(
srcs=["ovh_reconciler.py"],
deps=[
requirement("absl-py"),
requirement("certifi"),
requirement("charset-normalizer"),
requirement("idna"),
requirement("ovh"),
requirement("parameterized"),
requirement("requests"),
requirement("urllib3"),
],
)

Expand Down
4 changes: 4 additions & 0 deletions src/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
v1.5.0
- Improve logging
- Fix bug where modifications were not applied

v1.4.0
- Allow setting a TTL to A, AAAA, TXT and CNAME records.

Expand Down
35 changes: 29 additions & 6 deletions src/ovh_reconciler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import fileinput
import ovh
import re
from typing import Dict, NamedTuple, Set
from typing import NamedTuple, Set
from enum import Enum
from absl import app
from absl import flags
Expand Down Expand Up @@ -149,7 +149,7 @@ def parse_a_record(line: str) -> Record | None:
return Record(
type=Type.A,
subdomain=result.group('subdomain'),
target=result.group('ipv4') or '',
target=result.group('ipv4'),
ttl=ttl,
id=0)

Expand All @@ -170,7 +170,7 @@ def parse_aaaa_record(line: str) -> Record | None:
ttl = int(ttl)
return Record(
type=Type.AAAA,
subdomain=result.group('subdomain') or '',
subdomain=result.group('subdomain'),
target=result.group('ipv6'),
ttl=ttl,
id=0)
Expand All @@ -193,7 +193,7 @@ def parse_txt_record(line: str) -> Record | None:
target = (result.group('txt1') or '') + (result.group('txt2') or '')
return Record(
type=Type.TXT,
subdomain=result.group('subdomain') or '',
subdomain=result.group('subdomain'),
target=target,
ttl=ttl,
id=0)
Expand Down Expand Up @@ -247,7 +247,7 @@ def fetch_records(record_type: Type, client: ovh.Client) -> Set[Record]:
record_ids = client.get(
f'/domain/zone/{_DNS_ZONE.value}/record',
fieldType=record_type.name)
logging.debug('Found %d records of type %s for zone %s.',
logging.info('Fetched %d records of type %s for zone %s.',
len(record_ids), record_type.name, _DNS_ZONE.value)
records = set()
for id in record_ids:
Expand All @@ -258,7 +258,7 @@ def fetch_records(record_type: Type, client: ovh.Client) -> Set[Record]:
target=d['target'],
ttl=d['ttl'],
id=id)
logging.info('Found record [%d]: %s', id, r)
logging.debug('Fetched record [%d]: %s', id, r)
records.add(r)
return records

Expand Down Expand Up @@ -288,6 +288,11 @@ def delete_record(record: Record, client: ovh.Client) -> None:
def parse_input() -> Set[Record]:
records = set()
i = 0
records_per_type = {}
for type in ALLOWED_TYPES:
records_per_type[type] = []

# Parsed each line of the file.
with fileinput.FileInput(files=_INPUT.value) as f:
for line in f:
i += 1
Expand All @@ -297,6 +302,16 @@ def parse_input() -> Set[Record]:
continue
logging.debug('Parsed line %d: %s', i, record)
records.add(record)
records_per_type[record.type].append(record)

# Print out debug information.
for type in ALLOWED_TYPES:
logging.info('Parsed %d records of type %s.',
len(records_per_type[type]), type.name)

for r in records_per_type[type]:
logging.debug('Parsed record: %s', r)

return records


Expand All @@ -313,6 +328,13 @@ def reconcile(intent: Set[Record], current: Set[Record], client: ovh.Client):
delete_record(r, client)


def apply(client: ovh.Client):
if _DRY_RUN.value:
return
logging.info('Applying modifications.')
client.post(f'/domain/zone/{_DNS_ZONE.value}/refresh')


def main(unused_argv):
client = ovh.Client(
endpoint=_ENDPOINT.value,
Expand All @@ -327,6 +349,7 @@ def main(unused_argv):
current = current.union(fetch_records(type, client))
logging.info('Reconciling intent and reality')
reconcile(intent, current, client)
apply(client)


if __name__ == '__main__':
Expand Down
17 changes: 17 additions & 0 deletions src/ovh_reconciler_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,23 @@ def testReconcile_IgnoresUnallowedTypes(self, mock_ovh_class):
delete_mock.assert_not_called()
add_mock.assert_not_called()

@flagsaver.flagsaver(dry_run=False)
@flagsaver.flagsaver(dns_zone='foo.com')
@patch('ovh.Client')
def testApply_CallsRefreshURL(self, mock_ovh_class):
mock_client = mock_ovh_class()
ovh_reconciler.apply(mock_client)
mock_client.post.assert_called_once_with(
'/domain/zone/foo.com/refresh')

@flagsaver.flagsaver(dns_zone='foo.com')
@flagsaver.flagsaver(dry_run=True)
@patch('ovh.Client')
def testApplyWithDryRun_DoesNotCallRefresh(self, mock_ovh_class):
mock_client = mock_ovh_class()
ovh_reconciler.apply(mock_client)
mock_client.post.assert_not_called()

@parameterized.expand([
('foo.dodges.it IN A 10.0.0.1'),
(' foo.dodges.it IN A 10.0.0.1 '),
Expand Down

0 comments on commit e436f5c

Please sign in to comment.