diff --git a/check_empty_zones.py b/check_empty_zones.py new file mode 100644 index 0000000..7937f55 --- /dev/null +++ b/check_empty_zones.py @@ -0,0 +1,46 @@ +import sys + +import boto3 + + +def get_aws_zones(): + route53 = boto3.client("route53") + zones = [] + paginator = route53.get_paginator("list_hosted_zones") + for page in paginator.paginate(): + for zone in page["HostedZones"]: + zones.append((zone["Id"], zone["Name"])) + return zones + + +def is_zone_empty(zone_id): + route53 = boto3.client("route53") + paginator = route53.get_paginator("list_resource_record_sets") + for page in paginator.paginate(HostedZoneId=zone_id): + for record_set in page["ResourceRecordSets"]: + # Ignore NS and SOA records as they are default + if record_set["Type"] not in ["NS", "SOA"]: + return False + return True + + +def main(): + print("Checking for empty hosted zones...") + empty_zones = [] + for zone_id, zone_name in get_aws_zones(): + if is_zone_empty(zone_id): + # Remove trailing dot from zone name + empty_zones.append(zone_name.rstrip(".")) + + if empty_zones: + print("The following hosted zones are empty:") + for zone in empty_zones: + print(f" - {zone}") + sys.exit(1) + else: + print("No empty hosted zones found.") + sys.exit(0) + + +if __name__ == "__main__": + main() diff --git a/tests/test_check_for_empty_zones.py b/tests/test_check_for_empty_zones.py new file mode 100644 index 0000000..a6a3ce4 --- /dev/null +++ b/tests/test_check_for_empty_zones.py @@ -0,0 +1,96 @@ +from unittest.mock import MagicMock, patch + +import pytest +from check_empty_zones import get_aws_zones, is_zone_empty, main + + +@pytest.fixture +def mock_boto3_client(): + with patch('boto3.client') as mock_client: + yield mock_client + + +def test_get_aws_zones(mock_boto3_client): + mock_paginator = MagicMock() + mock_paginator.paginate.return_value = [ + { + 'HostedZones': [ + {'Id': '/hostedzone/Z1234567890ABC', 'Name': 'example.com.'}, + {'Id': '/hostedzone/Z0987654321DEF', 'Name': 'test.com.'} + ] + } + ] + mock_boto3_client.return_value.get_paginator.return_value = mock_paginator + + result = get_aws_zones() + assert result == [ + ('/hostedzone/Z1234567890ABC', 'example.com.'), + ('/hostedzone/Z0987654321DEF', 'test.com.') + ] + + +def test_is_zone_empty_true(mock_boto3_client): + mock_paginator = MagicMock() + mock_paginator.paginate.return_value = [ + { + 'ResourceRecordSets': [ + {'Type': 'NS'}, + {'Type': 'SOA'} + ] + } + ] + mock_boto3_client.return_value.get_paginator.return_value = mock_paginator + + assert is_zone_empty('dummy_zone_id') == True + + +def test_is_zone_empty_false(mock_boto3_client): + mock_paginator = MagicMock() + mock_paginator.paginate.return_value = [ + { + 'ResourceRecordSets': [ + {'Type': 'NS'}, + {'Type': 'SOA'}, + {'Type': 'A'} + ] + } + ] + mock_boto3_client.return_value.get_paginator.return_value = mock_paginator + + assert is_zone_empty('dummy_zone_id') == False + + +@patch('check_empty_zones.get_aws_zones') +@patch('check_empty_zones.is_zone_empty') +def test_main_empty_zones(mock_is_zone_empty, mock_get_aws_zones, capsys): + mock_get_aws_zones.return_value = [ + ('/hostedzone/Z1234567890ABC', 'example.com.'), + ('/hostedzone/Z0987654321DEF', 'test.com.') + ] + mock_is_zone_empty.side_effect = [True, False] + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 1 + captured = capsys.readouterr() + assert "The following hosted zones are empty:" in captured.out + assert " - example.com" in captured.out + assert " - test.com" not in captured.out + + +@patch('check_empty_zones.get_aws_zones') +@patch('check_empty_zones.is_zone_empty') +def test_main_no_empty_zones(mock_is_zone_empty, mock_get_aws_zones, capsys): + mock_get_aws_zones.return_value = [ + ('/hostedzone/Z1234567890ABC', 'example.com.'), + ('/hostedzone/Z0987654321DEF', 'test.com.') + ] + mock_is_zone_empty.return_value = False + + with pytest.raises(SystemExit) as e: + main() + + assert e.value.code == 0 + captured = capsys.readouterr() + assert "No empty hosted zones found." in captured.out