Skip to content

Commit

Permalink
added tf support
Browse files Browse the repository at this point in the history
  • Loading branch information
mvanholsteijn committed May 22, 2022
1 parent 50762c4 commit b3dc18f
Show file tree
Hide file tree
Showing 9 changed files with 410 additions and 115 deletions.
2 changes: 2 additions & 0 deletions example/terraform/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*.tf
*-managed-zone/
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
from os import path
from setuptools import find_packages, setup

dependencies = ['easyzone3', 'ruamel.yaml', 'click']
dependencies = ['easyzone3', 'ruamel.yaml', 'Jinja2', 'click']

this_directory = path.abspath(path.dirname(__file__))
with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f:
Expand All @@ -22,6 +22,7 @@
long_description_content_type='text/markdown',
package_dir={'': 'src'},
packages=find_packages(where='src'),
package_data={'': ['terraform-modules/*']},
include_package_data=True,
zip_safe=False,
platforms='any',
Expand Down
41 changes: 41 additions & 0 deletions src/zonefile_migrate/dns_record_set.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import logging
from easyzone import easyzone
from dns.rdatatype import _by_text as DNSRecordTypes


class DNSRecordSet:
"""
a simple DNS record set representation
"""

def __init__(self, name: str, rectype: str, ttl: int, rrdatas: list[str]):
self.name = name
self.rectype = rectype
self.ttl = ttl
self.rrdatas = rrdatas

@staticmethod
def create_from_easyzone(
name: easyzone.Name, records: easyzone.Records
) -> "DNSRecordSet":
"""
create a simple DNS record from an easyzone record.
"""
rrdatas = records.items
if len(rrdatas) > 0 and isinstance(rrdatas[0], (tuple, list)):
rrdatas = list(map(lambda r: " ".join(map(lambda v: str(v), r)), rrdatas))

return DNSRecordSet(name.name, records.type, name.ttl, rrdatas)


def create_from_zone(zone: easyzone.Zone) -> [DNSRecordSet]:
result: [DNSRecordSet] = []
for key, name in zone.names.items():
for rectype in DNSRecordTypes.keys():
records = name.records(rectype)
if not records:
continue

result.append(DNSRecordSet.create_from_easyzone(name, records))

return result
27 changes: 27 additions & 0 deletions src/zonefile_migrate/terraform-modules/google-managed-zone.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
variable domain_name {
description = "domain name to create zone for"
type = string
}

variable dns_record_sets {
description = "DNS record sets in this domain"
type = list(object({
name = string
type = string
ttl = number
rrdatas = list(string)
}))
}
resource "google_dns_managed_zone" "managed_zone" {
name = replace(trimsuffix(var.domain_name, "."), "/\\./", "-")
dns_name = var.domain_name
}

resource "google_dns_record_set" "record" {
for_each = {for r in var.dns_record_sets: format("%s-%s", r.name, r.type) => r}
name = each.value.name
managed_zone = google_dns_managed_zone.managed_zone.name
type = each.value.type
ttl = each.value.ttl
rrdatas = each.value.rrdatas
}
166 changes: 53 additions & 113 deletions src/zonefile_migrate/to_cloudformation.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import click
import json
import logging
import re
import sys
import os
from dns.rdatatype import _by_text as DNSRecordTypes

from pathlib import Path
from ruamel.yaml import YAML, CommentedMap
from zonefile_migrate.logger import logging
from zonefile_migrate.logger import log
from easyzone import easyzone
from dns.exception import SyntaxError
from zonefile_migrate.dns_record_set import create_from_zone
from zonefile_migrate.utils import (
get_all_zonefiles_in_path,
convert_zonefiles,
target_file,
)


def logical_resource_id(name: str):
Expand Down Expand Up @@ -64,67 +65,49 @@ def convert_to_cloudformation(zone: easyzone.Zone) -> dict:
)
result["Resources"] = resources

for key, name in zone.names.items():
for rectype in DNSRecordTypes.keys():
records = name.records(rectype)
if not records:
continue

if rectype == "NS" and key == zone.domain:
logging.warning("ignoring NS records for origin %s", key)
continue
if rectype == "SOA":
logging.warning("ignoring SOA records for origin %s", key)
continue

logical_name = generate_unique_logical_resource_id(
re.sub(
r"[^0-9a-zA-Z]",
"",
logical_resource_id(
re.sub(
r"^\*",
"wildcard",
key.removesuffix("." + zone.domain)
if key != "@"
else "Origin",
)
),
)
+ records.type
+ "Record",
resources,
)
resource_records = records.items
if rectype == "MX":
resource_records = list(map(lambda r: f"{r[0]} {r[1]}", records.items))

resources[logical_name] = CommentedMap(
{
"Type": "AWS::Route53::RecordSet",
"Properties": {
"Name": key,
"Type": records.type,
"ResourceRecords": resource_records,
"TTL": name.ttl,
"HostedZoneId": {"Ref": "HostedZone"},
},
}
for record_set in create_from_zone(zone):
if record_set.rectype == "NS" and record_set.name == zone.domain:
log.debug("ignoring NS records for origin %s", zone.domain)
continue
if record_set.rectype == "SOA":
log.debug("ignoring SOA records for domain %s", record_set.name)
continue

logical_name = generate_unique_logical_resource_id(
re.sub(
r"[^0-9a-zA-Z]",
"",
logical_resource_id(
re.sub(
r"^\*",
"wildcard",
record_set.name.removesuffix("." + zone.domain)
if record_set.name == zone.domain
else "Origin",
)
),
)
+ record_set.rectype
+ "Record",
resources,
)

resources[logical_name] = CommentedMap(
{
"Type": "AWS::Route53::RecordSet",
"Properties": {
"Name": record_set.name,
"Type": record_set.rectype,
"ResourceRecords": record_set.rrdatas,
"TTL": record_set.ttl,
"HostedZoneId": {"Ref": "HostedZone"},
},
}
)

return result


def target_file(src: Path, dst: Path) -> Path:
if dst.is_file():
return dst

if src.suffix == ".zone":
return dst.joinpath(src.name).with_suffix(".yaml")

return dst.joinpath(src.name + ".yaml")


def common_parent(one: Path, other: Path) -> Path:
"""
returns the commons parent of two paths
Expand Down Expand Up @@ -203,18 +186,6 @@ def generate_sceptre_configuration(
YAML().dump(config, file)


def is_zonefile(path: Path) -> bool:
"""
returns true if the file pointed to by `path` contains a $ORIGIN or $TTL pragma, otherwise False
"""
if path.exists() and path.is_file():
with path.open("r") as file:
for line in file:
if re.search(r"^\s*\$(ORIGIN|TTL)\s+", line, re.IGNORECASE):
return True
return False


@click.command(name="to-cloudformation")
@click.option(
"--sceptre-group",
Expand All @@ -237,26 +208,17 @@ def command(sceptre_group, src, dst):
The zonefiles must contain a $ORIGIN and $TTL statement. If the SRC points to a directory
all files which contain one of these statements will be converted. If a $ORIGIN is missing,
the name of the file will be used as the domain name.
"""
if sceptre_group:
sceptre_group = Path(sceptre_group)

if not src:
raise click.UsageError("no source files were specified")

inputs = []
for filename in map(lambda s: Path(s), src):
if filename.is_dir():
inputs.extend(
[f for f in filename.iterdir() if f.is_file() and is_zonefile(f)]
)
else:
inputs.append(filename)
inputs = get_all_zonefiles_in_path(src)

if len(inputs) == 0:
click.UsageError("no zone files were found")
click.UsageError("no zonefiles were found")

dst = Path(dst)
if len(inputs) > 1:
Expand All @@ -265,38 +227,16 @@ def command(sceptre_group, src, dst):
if not dst.exists():
dst.mkdir(parents=True, exist_ok=True)

outputs = list(map(lambda d: target_file(d, dst), inputs))
outputs = list(map(lambda d: target_file(d, dst, ".yaml"), inputs))

for i, input in enumerate(map(lambda s: Path(s), inputs)):
with input.open("r") as file:
content = file.read()
found = re.search(
r"\$ORIGIN\s+(?P<domain_name>.*)\s*",
content,
re.MULTILINE | re.IGNORECASE,
)
if found:
domain_name = found.group("domain_name")
else:
domain_name = input.name.removesuffix(".zone")
logging.warning(
"could not find $ORIGIN from zone file %s, using %s",
input,
domain_name,
)

try:
logging.info("reading zonefile %s", input.as_posix())
zone = easyzone.zone_from_file(domain_name, input.as_posix())
except SyntaxError as error:
logging.error(error)
exit(1)

with outputs[i].open("w") as file:
def transform_to_cloudformation(zone: easyzone.Zone, output: Path):
with output.open("w") as file:
YAML().dump(convert_to_cloudformation(zone), stream=file)
if sceptre_group:
generate_sceptre_configuration(zone, outputs[i], sceptre_group)

convert_zonefiles(inputs, outputs, transform_to_cloudformation)


if __name__ == "__main__":
command()
Loading

0 comments on commit b3dc18f

Please sign in to comment.