diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 709ae03..011609c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,15 +14,15 @@ jobs: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v7 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: ${{ matrix.python-version }} - name: Cache pip packages - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt', '**/pyproject.toml') }} @@ -36,14 +36,14 @@ jobs: pip install -r requirements-dev.txt - name: Run pre-commit hooks - uses: pre-commit/action@v3.0.1 + uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1 - name: Run tests with pytest run: | pytest --cov --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov - uses: codecov/codecov-action@v7 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: file: ./coverage.xml fail_ci_if_error: false @@ -54,10 +54,10 @@ jobs: type-check: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Python - uses: actions/setup-python@v7 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.11" diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 82885e5..fea9739 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v6.0.0 + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -15,26 +15,26 @@ repos: - id: mixed-line-ending - repo: https://github.com/gitleaks/gitleaks - rev: v8.30.1 + rev: 83d9cd684c87d95d656c1458ef04895a7f1cbd8e # v8.30.1 hooks: - id: gitleaks entry: gitleaks dir --redact --verbose . pass_filenames: false - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.16.3 + rev: 65dbdb59d2f2d9c3bdc343c566821ad3319ddaa3 # v0.16.3 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/jendrikseipp/vulture - rev: v2.16 + rev: b0f67ba0044693aa9ec0d38fe460590facc98004 # v2.16 hooks: - id: vulture - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.19.1 + rev: a66e98df7b4aeeb3724184b332785976d062b92e # v1.19.1 hooks: - id: mypy exclude: ^tests/ diff --git a/install.py b/install.py index 5f5c090..5bc2264 100755 --- a/install.py +++ b/install.py @@ -1,13 +1,10 @@ #!/usr/bin/env python3 -"""Install script for cflan - NetworkManager dispatcher setup.""" - import os import shutil import sys def install() -> None: - """Deploy NetworkManager dispatcher script and configuration files.""" if os.getuid() != 0: sys.exit("Error: Must run as root") @@ -23,7 +20,6 @@ def install() -> None: print("\nDeploying configuration...") config_deployed = False - # Try unencrypted config first vars_path = os.path.join(script_dir, "vars.yaml") if os.path.exists(vars_path): target_path = "/vars.yaml" @@ -33,7 +29,6 @@ def install() -> None: print(f" Installed: {target_path}") config_deployed = True - # Fall back to sops encrypted config sops_path = os.path.join(script_dir, "sops_vars.yaml") if not config_deployed and os.path.exists(sops_path): target_path = "/sops_vars.yaml" diff --git a/opencode.json b/opencode.json index b9a08c4..f3dfe18 100644 --- a/opencode.json +++ b/opencode.json @@ -16,6 +16,7 @@ "aws-prod": {"enabled": false}, "grafana": {"enabled": false}, "terraform-docs": {"enabled": false}, + "argocd-makeitwork": {"enabled": false}, "argocd-staging-eks": {"enabled": false}, "argocd-prod-eks": {"enabled": false} }, @@ -31,6 +32,7 @@ "aws-prod_*": false, "grafana_*": false, "terraform-docs_*": false, + "argocd-makeitwork_*": false, "argocd-staging-eks_*": false, "argocd-prod-eks_*": false } diff --git a/set_dns.py b/set_dns.py index 27a1c03..5c6d396 100755 --- a/set_dns.py +++ b/set_dns.py @@ -20,7 +20,6 @@ def get_local_ip() -> str: - """Get the local IP address.""" local_ip_addr = "" try: local_ip_addr = socket.gethostbyname(socket.gethostname() + ".local") @@ -32,7 +31,6 @@ def get_local_ip() -> str: def validate_network_manager_args(local_ip_addr: str) -> None: - """Validate NetworkManager arguments.""" print("Parsing NetworkManager arguments...") try: if ( @@ -67,7 +65,6 @@ def validate_network_manager_args(local_ip_addr: str) -> None: def get_yaml_vars() -> dict[str, Any]: - """Get YAML variables from vars.yaml or sops_vars.yaml.""" print("Getting unencrypted values from vars.yaml ...") try: with open("vars.yaml") as f: @@ -94,7 +91,6 @@ def get_yaml_vars() -> dict[str, Any]: def get_zone_info(cf: CloudFlare.CloudFlare, domain_name: str) -> tuple[str, str]: - """Get CloudFlare DNS Zone ID and Name.""" print("Getting CloudFlare DNS Zone ID and Name...") zone_data = cf.zones.get(params={"per_page": "1", "name": domain_name})[0] return zone_data["id"], zone_data["name"] @@ -103,7 +99,6 @@ def get_zone_info(cf: CloudFlare.CloudFlare, domain_name: str) -> tuple[str, str def get_dns_record_id( cf: CloudFlare.CloudFlare, zone_id: str, hostname: str, zone_name: str ) -> str: - """Get existing DNS record ID if it exists.""" print( "Attempting to get existing DNS record for " + hostname @@ -131,7 +126,6 @@ def get_dns_record_id( def create_dns_record( cf: CloudFlare.CloudFlare, zone_id: str, hostname: str, ip_addr: str ) -> None: - """Create a new DNS record.""" print("Record not found...") print("Creating new record for " + hostname + "...") try: @@ -147,7 +141,6 @@ def create_dns_record( def update_dns_record( cf: CloudFlare.CloudFlare, zone_id: str, dns_id: str, hostname: str, ip_addr: str ) -> None: - """Update existing DNS record with new IP.""" print("Getting IP address for existing record...") dns_content = cf.zones.dns_records.get( zone_id, @@ -179,7 +172,6 @@ def update_dns_record( def set_dns() -> None: - """Main function to set DNS records.""" local_ip_addr = get_local_ip() print("Using IP address " + local_ip_addr + " ...") diff --git a/tests/test_set_dns.py b/tests/test_set_dns.py index 8324375..b4efb66 100644 --- a/tests/test_set_dns.py +++ b/tests/test_set_dns.py @@ -1,5 +1,3 @@ -"""Tests for the set_dns module.""" - import socket import sys from unittest.mock import MagicMock, mock_open, patch @@ -23,12 +21,9 @@ class TestGetLocalIp: - """Tests for get_local_ip function.""" - @patch("set_dns.socket.gethostbyname") @patch("set_dns.socket.gethostname") def test_get_local_ip_with_local_suffix(self, mock_hostname, mock_gethost): - """Test getting local IP with .local suffix.""" mock_hostname.return_value = "testhost" mock_gethost.return_value = "192.168.1.100" result = get_local_ip() @@ -38,7 +33,6 @@ def test_get_local_ip_with_local_suffix(self, mock_hostname, mock_gethost): @patch("set_dns.socket.gethostbyname") @patch("set_dns.socket.gethostname") def test_get_local_ip_fallback_to_lan(self, mock_hostname, mock_gethost): - """Test fallback to .lan suffix when .local fails.""" mock_hostname.return_value = "testhost" def side_effect(hostname): @@ -53,7 +47,6 @@ def side_effect(hostname): @patch("set_dns.socket.gethostbyname") @patch("set_dns.socket.gethostname") def test_get_local_ip_both_suffixes_fail(self, mock_hostname, mock_gethost): - """Test that exception propagates when both .local and .lan fail.""" mock_hostname.return_value = "testhost" def side_effect(hostname): @@ -66,23 +59,18 @@ def side_effect(hostname): class TestValidateNetworkManagerArgs: - """Tests for validate_network_manager_args function.""" - @patch("set_dns.netifaces") @patch("set_dns.sys") def test_valid_interface_and_action(self, mock_sys, mock_netifaces): - """Test validation passes with correct interface and action.""" mock_sys.argv = ["script", "eth0", "up"] mock_netifaces.ifaddresses.return_value = {2: [{"addr": "192.168.1.100"}]} mock_netifaces.AF_INET = 2 - # Should not raise or exit validate_network_manager_args("192.168.1.100") @patch("set_dns.netifaces") @patch("set_dns.sys") def test_mismatched_ip_address(self, mock_sys, mock_netifaces): - """Test validation fails when IP doesn't match.""" mock_sys.argv = ["script", "eth0", "up"] mock_sys.exit.side_effect = SystemExit mock_netifaces.ifaddresses.return_value = {2: [{"addr": "10.0.0.1"}]} @@ -94,7 +82,6 @@ def test_mismatched_ip_address(self, mock_sys, mock_netifaces): @patch("set_dns.netifaces") @patch("set_dns.sys") def test_wrong_action(self, mock_sys, mock_netifaces): - """Test validation fails with wrong action.""" mock_sys.argv = ["script", "eth0", "down"] mock_sys.exit.side_effect = SystemExit mock_netifaces.ifaddresses.return_value = {2: [{"addr": "192.168.1.100"}]} @@ -106,24 +93,19 @@ def test_wrong_action(self, mock_sys, mock_netifaces): @patch("set_dns.netifaces") @patch("set_dns.sys") def test_no_arguments(self, mock_sys, mock_netifaces): - """Test validation continues when no arguments provided.""" mock_sys.argv = ["script"] mock_netifaces.AF_INET = 2 - # Should not raise validate_network_manager_args("192.168.1.100") class TestGetYamlVars: - """Tests for get_yaml_vars function.""" - @patch( "builtins.open", mock_open(read_data="cf_token: test123\ncf_domain: example.com"), ) @patch("set_dns.yaml") def test_get_unencrypted_yaml(self, mock_yaml): - """Test reading unencrypted vars.yaml.""" mock_yaml.safe_load.return_value = { "cf_token": "test123", "cf_domain": "example.com", @@ -138,8 +120,6 @@ def test_get_unencrypted_yaml(self, mock_yaml): @patch("set_dns.yaml") @patch("builtins.open") def test_get_sops_encrypted_yaml(self, mock_open_file, mock_yaml, mock_run): - """Test reading sops encrypted yaml when vars.yaml missing.""" - # First open raises FileNotFoundError mock_open_file.side_effect = FileNotFoundError() mock_run.return_value = MagicMock( @@ -160,7 +140,6 @@ def test_get_sops_encrypted_yaml(self, mock_open_file, mock_yaml, mock_run): @patch("set_dns.subprocess.run") @patch("builtins.open") def test_sops_not_installed(self, mock_open_file, mock_run): - """Test exit when sops is not installed.""" mock_open_file.side_effect = FileNotFoundError() mock_run.side_effect = FileNotFoundError("sops not found") @@ -169,10 +148,7 @@ def test_sops_not_installed(self, mock_open_file, mock_run): class TestGetZoneInfo: - """Tests for get_zone_info function.""" - def test_get_zone_info(self): - """Test retrieving zone ID and name.""" mock_cf = MagicMock() mock_cf.zones.get.return_value = [{"id": "zone123", "name": "example.com"}] @@ -183,10 +159,7 @@ def test_get_zone_info(self): class TestGetDnsRecordId: - """Tests for get_dns_record_id function.""" - def test_existing_record_found(self): - """Test getting DNS record ID when record exists.""" mock_cf = MagicMock() mock_cf.zones.dns_records.get.return_value = [{"id": "record123"}] @@ -195,7 +168,6 @@ def test_existing_record_found(self): assert result == "record123" def test_no_existing_record(self): - """Test empty string returned when no record exists.""" mock_cf = MagicMock() mock_cf.zones.dns_records.get.return_value = [] @@ -205,10 +177,7 @@ def test_no_existing_record(self): class TestCreateDnsRecord: - """Tests for create_dns_record function.""" - def test_create_record_success(self): - """Test successful DNS record creation.""" mock_cf = MagicMock() create_dns_record(mock_cf, "zone123", "host", "192.168.1.100") @@ -219,10 +188,8 @@ def test_create_record_success(self): ) def test_create_record_api_error(self): - """Test handling of Cloudflare API error.""" mock_cf = MagicMock() - # Create a proper exception class that inherits from Exception class MockAPIError(Exception): def __str__(self): return "API Error" @@ -241,10 +208,7 @@ def __int__(self): class TestUpdateDnsRecord: - """Tests for update_dns_record function.""" - def test_update_when_ip_changed(self): - """Test updating record when IP has changed.""" mock_cf = MagicMock() mock_cf.zones.dns_records.get.return_value = [{"content": "10.0.0.1"}] @@ -256,7 +220,6 @@ def test_update_when_ip_changed(self): mock_cf.zones.dns_records.post.assert_called_once() def test_no_update_when_ip_unchanged(self): - """Test exiting when IP hasn't changed.""" mock_cf = MagicMock() mock_cf.zones.dns_records.get.return_value = [{"content": "192.168.1.100"}]