Commit a76f8e3c9158
Changed files (3)
systems
common
services
tools
systems/common/services/dns/demeester.fr.nix
@@ -0,0 +1,64 @@
+# DNS zone for demeester.fr
+# Managed via Gandi LiveDNS API (tools/update-gandi-dns.sh)
+{ dns, globals, ... }:
+with dns.lib.combinators;
+{
+ SOA = {
+ nameServer = "ns1.gandi.net.";
+ adminEmail = "hostmaster.gandi.net";
+ serial = 1;
+ refresh = 10800;
+ retry = 3600;
+ expire = 604800;
+ minimum = 10800;
+ };
+
+ NS = [
+ "ns1.gandi.net."
+ ];
+
+ # Root domain points to carthage (public endpoint)
+ A = [ "46.224.100.116" ];
+
+ # iCloud Mail
+ MX = [
+ {
+ preference = 10;
+ exchange = "mx01.mail.icloud.com.";
+ }
+ {
+ preference = 10;
+ exchange = "mx02.mail.icloud.com.";
+ }
+ ];
+
+ TXT = [
+ "apple-domain=vML1eiTPb5VpQ5rc"
+ "v=spf1 include:icloud.com ~all"
+ ];
+
+ subdomains = {
+ # Wildcard for public endpoint (carthage)
+ "*".A = [ "46.224.100.116" ];
+
+ # Website
+ www.A = [ "46.224.100.116" ];
+ vincent.A = [ "46.224.100.116" ];
+
+ # Shortcuts
+ carthage.A = [ "46.224.100.116" ];
+ p.A = [ "46.224.100.116" ];
+
+ # Wildcard under www
+ "*.www".A = [ "46.224.100.116" ];
+
+ # iCloud DKIM
+ _domainkey.subdomains.sig1.CNAME = [ "sig1.dkim.demeester.fr.at.icloudmailadmin.com." ];
+
+ # ATProto PDS
+ pds.A = [ "46.224.100.116" ];
+
+ # ATProto handle verification (placeholder - update DID after account creation)
+ # "_atproto.vincent".TXT = [ "did=did:plc:PLACEHOLDER" ];
+ };
+}
systems/common/services/bind.nix
@@ -50,6 +50,12 @@ in
master = true;
file = mkZoneFile "192.168.1.in-addr.arpa" ./dns/192.168.1.nix;
}
+ # demeester.fr zone
+ {
+ name = "demeester.fr";
+ master = true;
+ file = mkZoneFile "demeester.fr" ./dns/demeester.fr.nix;
+ }
# vpn zone
{
name = "vpn";
tools/update-gandi-dns.sh
@@ -1,6 +1,7 @@
#!/usr/bin/env bash
# Update Gandi DNS records from NixOS DNS configuration
-# Usage: ./scripts/update-gandi-dns.sh [--dry-run]
+# Usage: ./tools/update-gandi-dns.sh [--dry-run] [domain...]
+# If no domains specified, updates all managed domains.
set -euo pipefail
@@ -13,21 +14,40 @@ RED='\033[0;31m'
CYAN='\033[0;36m'
RESET='\033[0m'
-DOMAIN="sbr.pm"
-API_URL="https://api.gandi.net/v5/livedns/domains/$DOMAIN/records"
DRY_RUN=false
+# Domain -> zone file mapping
+declare -A DOMAIN_ZONE_FILES
+DOMAIN_ZONE_FILES["sbr.pm"]="./systems/common/services/dns/sbr.pm-gandi.nix"
+DOMAIN_ZONE_FILES["demeester.fr"]="./systems/common/services/dns/demeester.fr.nix"
+
+ALL_DOMAINS=("sbr.pm" "demeester.fr")
+SELECTED_DOMAINS=()
+
# Parse arguments
for arg in "$@"; do
case $arg in
--dry-run)
DRY_RUN=true
- shift
+ ;;
+ *)
+ if [[ -n "${DOMAIN_ZONE_FILES[$arg]+_}" ]]; then
+ SELECTED_DOMAINS+=("$arg")
+ else
+ echo -e "${RED}Error: Unknown domain '$arg'${RESET}"
+ echo -e "${YELLOW}Available domains: ${ALL_DOMAINS[*]}${RESET}"
+ exit 1
+ fi
;;
esac
done
-# Check for API key (using lego's environment variable name)
+# Default to all domains if none specified
+if [[ ${#SELECTED_DOMAINS[@]} -eq 0 ]]; then
+ SELECTED_DOMAINS=("${ALL_DOMAINS[@]}")
+fi
+
+# Check for API key
if [[ -z "${GANDIV5_PERSONAL_TOKEN:-}" ]]; then
echo -e "${RED}Error: GANDIV5_PERSONAL_TOKEN environment variable not set${RESET}"
echo -e "${YELLOW}Please set it with: export GANDIV5_PERSONAL_TOKEN=your-api-key${RESET}"
@@ -36,204 +56,272 @@ if [[ -z "${GANDIV5_PERSONAL_TOKEN:-}" ]]; then
exit 1
fi
-echo -e "${BOLD}${BLUE}Updating Gandi DNS records for $DOMAIN...${RESET}"
-if [[ "$DRY_RUN" == "true" ]]; then
- echo -e "${YELLOW}DRY RUN MODE - No changes will be made${RESET}"
-fi
-echo
-
-# Get the DNS zone file content from Nix (using Gandi-specific config with VPN IPs)
-echo -e "${CYAN}Extracting DNS records from Nix configuration (Gandi/VPN version)...${RESET}"
-ZONE_FILE=$(nix eval --impure --raw --expr '
- let
- pkgs = import <nixpkgs> {};
- dns = (builtins.getFlake (toString ./.)).inputs.dns;
- globals = (import ./globals.nix) {};
- zone = import ./systems/common/services/dns/sbr.pm-gandi.nix { inherit dns globals; };
- in
- dns.lib.toString "sbr.pm" zone
-' 2>&1 | grep -v "^warning:" | grep -v "^Using saved setting" | grep -v "^building ")
-
-if [[ -z "$ZONE_FILE" ]]; then
- echo -e "${RED}Error: Could not generate zone file${RESET}"
- exit 1
-fi
-
-echo -e "${GREEN}DNS records extracted${RESET}"
-echo
-
-# Create a temporary file for the zone
-TEMP_ZONE=$(mktemp)
-echo "$ZONE_FILE" > "$TEMP_ZONE"
-
-# Extract A records (excluding SOA, NS, and comments)
-# Format examples:
-# name.domain. IN A ip
-# name.domain. TTL IN A ip
-# *.name.domain. IN A ip
-RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?A\s+" "$TEMP_ZONE" | \
- grep -v "SOA" | \
- grep -v "^;" || true)
-
-rm -f "$TEMP_ZONE"
-
-if [[ -z "$RECORDS" ]]; then
- echo -e "${YELLOW}No A records found in zone file${RESET}"
- exit 0
-fi
-
-echo -e "${GREEN}Found $(echo "$RECORDS" | wc -l) A records to process${RESET}"
-echo
-
-# Get current DNS records from Gandi (needed for both update and removal detection)
-echo -e "${CYAN}Fetching current DNS records from Gandi...${RESET}"
-CURRENT_RECORDS=$(curl -s \
- -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
- "$API_URL" || echo "[]")
-
-echo -e "${GREEN}Current records fetched${RESET}"
-echo
-
-# Process each record
-UPDATED=0
-SKIPPED=0
-FAILED=0
-DELETED=0
-
-# Track all desired A record names from Nix config
-declare -A DESIRED_NAMES
-
-while IFS= read -r line; do
- # Parse the line to extract: name, TTL, and value
- # Handle various formats:
- # name.sbr.pm. IN A value
- # name.sbr.pm. TTL IN A value
- # *.name.sbr.pm. TTL IN A value
-
- # Extract components
- FULL_NAME=$(echo "$line" | awk '{print $1}')
-
- # Check if second field is a number (TTL) or "IN"
- SECOND_FIELD=$(echo "$line" | awk '{print $2}')
- if [[ "$SECOND_FIELD" =~ ^[0-9]+$ ]]; then
- TTL="$SECOND_FIELD"
- VALUE=$(echo "$line" | awk '{print $5}')
- else
- TTL=10800
- VALUE=$(echo "$line" | awk '{print $4}')
- fi
-
- # Convert FQDN to Gandi record name
- # - "sbr.pm." (apex) → "@"
- # - "name.sbr.pm." → "name"
- # - "*.sbr.pm." → "*"
- # - "*.name.sbr.pm." → "*.name"
- if [[ "$FULL_NAME" == "sbr.pm." ]]; then
- NAME="@"
- else
- NAME="${FULL_NAME%.sbr.pm.}"
- fi
-
- # Track this name as desired
- DESIRED_NAMES["$NAME"]=1
-
- echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN A $VALUE (TTL: $TTL)"
+# Process a single domain
+update_domain() {
+ local DOMAIN="$1"
+ local ZONE_FILE="$2"
+ local API_URL="https://api.gandi.net/v5/livedns/domains/$DOMAIN/records"
+ echo -e "${BOLD}${BLUE}━━━ Updating Gandi DNS records for $DOMAIN ━━━${RESET}"
if [[ "$DRY_RUN" == "true" ]]; then
- echo -e " ${CYAN}[DRY RUN] Would update/create record${RESET}"
- UPDATED=$((UPDATED + 1))
- else
- # Check if record exists and has same value
- CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
- --arg name "$NAME" \
- --arg type "A" \
- '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
- 2>/dev/null || echo "")
+ echo -e "${YELLOW}DRY RUN MODE - No changes will be made${RESET}"
+ fi
+ echo
- if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
- echo -e " ${GREEN}✓ Record unchanged, skipping${RESET}"
- SKIPPED=$((SKIPPED + 1))
- else
- if [[ -z "$CURRENT_VALUE" ]]; then
- echo -e " ${YELLOW}Creating new record...${RESET}"
+ # Get the DNS zone file content from Nix
+ echo -e "${CYAN}Extracting DNS records from Nix configuration...${RESET}"
+ ZONE_CONTENT=$(nix eval --impure --raw --expr '
+ let
+ pkgs = import <nixpkgs> {};
+ dns = (builtins.getFlake (toString ./.)).inputs.dns;
+ globals = (import ./globals.nix) {};
+ zone = import '"$ZONE_FILE"' { inherit dns globals; };
+ in
+ dns.lib.toString "'"$DOMAIN"'" zone
+ ' 2>&1 | grep -v "^warning:" | grep -v "^Using saved setting" | grep -v "^building ")
+
+ if [[ -z "$ZONE_CONTENT" ]]; then
+ echo -e "${RED}Error: Could not generate zone file for $DOMAIN${RESET}"
+ return 1
+ fi
+
+ echo -e "${GREEN}DNS records extracted${RESET}"
+ echo
+
+ # Create a temporary file for the zone
+ TEMP_ZONE=$(mktemp)
+ echo "$ZONE_CONTENT" > "$TEMP_ZONE"
+
+ # Extract A records
+ RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?A\s+" "$TEMP_ZONE" | \
+ grep -v "SOA" | \
+ grep -v "^;" || true)
+
+ # Extract CNAME records
+ CNAME_RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?CNAME\s+" "$TEMP_ZONE" | \
+ grep -v "^;" || true)
+
+ # Extract TXT records
+ TXT_RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?TXT\s+" "$TEMP_ZONE" | \
+ grep -v "^;" || true)
+
+ # Extract MX records
+ MX_RECORDS=$(grep -E "^\S+\s+(([0-9]+\s+)?IN\s+)?MX\s+" "$TEMP_ZONE" | \
+ grep -v "^;" || true)
+
+ rm -f "$TEMP_ZONE"
+
+ TOTAL=$(echo "$RECORDS" | grep -c . || echo 0)
+ [[ -n "$CNAME_RECORDS" ]] && TOTAL=$((TOTAL + $(echo "$CNAME_RECORDS" | grep -c . || echo 0)))
+ echo -e "${GREEN}Found $TOTAL A/CNAME records to process${RESET}"
+ echo
+
+ # Get current DNS records from Gandi
+ echo -e "${CYAN}Fetching current DNS records from Gandi...${RESET}"
+ CURRENT_RECORDS=$(curl -s \
+ -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
+ "$API_URL" || echo "[]")
+
+ echo -e "${GREEN}Current records fetched${RESET}"
+ echo
+
+ # Process each record
+ local UPDATED=0
+ local SKIPPED=0
+ local FAILED=0
+ local DELETED=0
+
+ # Track all desired A record names from Nix config
+ declare -A DESIRED_NAMES
+
+ # Process A records
+ if [[ -n "$RECORDS" ]]; then
+ while IFS= read -r line; do
+ FULL_NAME=$(echo "$line" | awk '{print $1}')
+
+ SECOND_FIELD=$(echo "$line" | awk '{print $2}')
+ if [[ "$SECOND_FIELD" =~ ^[0-9]+$ ]]; then
+ TTL="$SECOND_FIELD"
+ VALUE=$(echo "$line" | awk '{print $5}')
else
- echo -e " ${YELLOW}Updating record (was: $CURRENT_VALUE)...${RESET}"
+ TTL=10800
+ VALUE=$(echo "$line" | awk '{print $4}')
fi
- # Update/create the record
- RESPONSE=$(curl -s -w "\n%{http_code}" \
- -X PUT \
- -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
- -H "Content-Type: application/json" \
- -d "{\"rrset_values\": [\"$VALUE\"], \"rrset_ttl\": $TTL}" \
- "$API_URL/$NAME/A")
+ if [[ "$FULL_NAME" == "$DOMAIN." ]]; then
+ NAME="@"
+ else
+ NAME="${FULL_NAME%.$DOMAIN.}"
+ fi
- HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
- BODY=$(echo "$RESPONSE" | sed '$d')
+ DESIRED_NAMES["$NAME"]=1
- if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
- echo -e " ${GREEN}✓ Record updated successfully${RESET}"
+ echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN A $VALUE (TTL: $TTL)"
+
+ if [[ "$DRY_RUN" == "true" ]]; then
+ echo -e " ${CYAN}[DRY RUN] Would update/create record${RESET}"
UPDATED=$((UPDATED + 1))
else
- echo -e " ${RED}✗ Failed to update record (HTTP $HTTP_CODE)${RESET}"
- echo -e " ${RED}Response: $BODY${RESET}"
- FAILED=$((FAILED + 1))
+ CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
+ --arg name "$NAME" \
+ --arg type "A" \
+ '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
+ 2>/dev/null || echo "")
+
+ if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
+ echo -e " ${GREEN}✓ Record unchanged, skipping${RESET}"
+ SKIPPED=$((SKIPPED + 1))
+ else
+ if [[ -z "$CURRENT_VALUE" ]]; then
+ echo -e " ${YELLOW}Creating new record...${RESET}"
+ else
+ echo -e " ${YELLOW}Updating record (was: $CURRENT_VALUE)...${RESET}"
+ fi
+
+ RESPONSE=$(curl -s -w "\n%{http_code}" \
+ -X PUT \
+ -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"rrset_values\": [\"$VALUE\"], \"rrset_ttl\": $TTL}" \
+ "$API_URL/$NAME/A")
+
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+ BODY=$(echo "$RESPONSE" | sed '$d')
+
+ if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
+ echo -e " ${GREEN}✓ Record updated successfully${RESET}"
+ UPDATED=$((UPDATED + 1))
+ else
+ echo -e " ${RED}✗ Failed to update record (HTTP $HTTP_CODE)${RESET}"
+ echo -e " ${RED}Response: $BODY${RESET}"
+ FAILED=$((FAILED + 1))
+ fi
+ fi
fi
- fi
+
+ echo
+ done <<< "$RECORDS"
fi
+ # Process CNAME records
+ if [[ -n "$CNAME_RECORDS" ]]; then
+ while IFS= read -r line; do
+ FULL_NAME=$(echo "$line" | awk '{print $1}')
+
+ SECOND_FIELD=$(echo "$line" | awk '{print $2}')
+ if [[ "$SECOND_FIELD" =~ ^[0-9]+$ ]]; then
+ TTL="$SECOND_FIELD"
+ VALUE=$(echo "$line" | awk '{print $5}')
+ else
+ TTL=10800
+ VALUE=$(echo "$line" | awk '{print $4}')
+ fi
+
+ NAME="${FULL_NAME%.$DOMAIN.}"
+
+ echo -e "${BLUE}Processing: ${RESET}$NAME.$DOMAIN CNAME $VALUE (TTL: $TTL)"
+
+ if [[ "$DRY_RUN" == "true" ]]; then
+ echo -e " ${CYAN}[DRY RUN] Would update/create CNAME record${RESET}"
+ UPDATED=$((UPDATED + 1))
+ else
+ CURRENT_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
+ --arg name "$NAME" \
+ --arg type "CNAME" \
+ '.[] | select(.rrset_name == $name and .rrset_type == $type) | .rrset_values[0]' \
+ 2>/dev/null || echo "")
+
+ if [[ "$CURRENT_VALUE" == "$VALUE" ]]; then
+ echo -e " ${GREEN}✓ Record unchanged, skipping${RESET}"
+ SKIPPED=$((SKIPPED + 1))
+ else
+ if [[ -z "$CURRENT_VALUE" ]]; then
+ echo -e " ${YELLOW}Creating new CNAME record...${RESET}"
+ else
+ echo -e " ${YELLOW}Updating CNAME record (was: $CURRENT_VALUE)...${RESET}"
+ fi
+
+ RESPONSE=$(curl -s -w "\n%{http_code}" \
+ -X PUT \
+ -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
+ -H "Content-Type: application/json" \
+ -d "{\"rrset_values\": [\"$VALUE\"], \"rrset_ttl\": $TTL}" \
+ "$API_URL/$NAME/CNAME")
+
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+ BODY=$(echo "$RESPONSE" | sed '$d')
+
+ if [[ "$HTTP_CODE" == "201" ]] || [[ "$HTTP_CODE" == "200" ]]; then
+ echo -e " ${GREEN}✓ CNAME record updated successfully${RESET}"
+ UPDATED=$((UPDATED + 1))
+ else
+ echo -e " ${RED}✗ Failed to update CNAME record (HTTP $HTTP_CODE)${RESET}"
+ echo -e " ${RED}Response: $BODY${RESET}"
+ FAILED=$((FAILED + 1))
+ fi
+ fi
+ fi
+
+ echo
+ done <<< "$CNAME_RECORDS"
+ fi
+
+ # Remove stale A records from Gandi that are no longer in Nix config
+ echo -e "${BOLD}${BLUE}Checking for stale A records to remove...${RESET}"
echo
-done <<< "$RECORDS"
-# Remove stale A records from Gandi that are no longer in Nix config
-echo -e "${BOLD}${BLUE}Checking for stale A records to remove...${RESET}"
-echo
+ GANDI_A_NAMES=$(echo "$CURRENT_RECORDS" | jq -r '.[] | select(.rrset_type == "A") | .rrset_name' 2>/dev/null || true)
-GANDI_A_NAMES=$(echo "$CURRENT_RECORDS" | jq -r '.[] | select(.rrset_type == "A") | .rrset_name' 2>/dev/null || true)
+ while IFS= read -r gandi_name; do
+ [[ -z "$gandi_name" ]] && continue
-while IFS= read -r gandi_name; do
- [[ -z "$gandi_name" ]] && continue
+ if [[ -z "${DESIRED_NAMES[$gandi_name]+_}" ]]; then
+ GANDI_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
+ --arg name "$gandi_name" \
+ '.[] | select(.rrset_name == $name and .rrset_type == "A") | .rrset_values | join(", ")' \
+ 2>/dev/null || echo "unknown")
- if [[ -z "${DESIRED_NAMES[$gandi_name]+_}" ]]; then
- GANDI_VALUE=$(echo "$CURRENT_RECORDS" | jq -r \
- --arg name "$gandi_name" \
- '.[] | select(.rrset_name == $name and .rrset_type == "A") | .rrset_values | join(", ")' \
- 2>/dev/null || echo "unknown")
+ echo -e "${RED}Stale record: ${RESET}$gandi_name.$DOMAIN A $GANDI_VALUE"
- echo -e "${RED}Stale record: ${RESET}$gandi_name.$DOMAIN A $GANDI_VALUE"
-
- if [[ "$DRY_RUN" == "true" ]]; then
- echo -e " ${CYAN}[DRY RUN] Would delete record${RESET}"
- DELETED=$((DELETED + 1))
- else
- RESPONSE=$(curl -s -w "\n%{http_code}" \
- -X DELETE \
- -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
- "$API_URL/$gandi_name/A")
-
- HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
- BODY=$(echo "$RESPONSE" | sed '$d')
-
- if [[ "$HTTP_CODE" == "204" ]] || [[ "$HTTP_CODE" == "200" ]]; then
- echo -e " ${GREEN}✓ Record deleted successfully${RESET}"
+ if [[ "$DRY_RUN" == "true" ]]; then
+ echo -e " ${CYAN}[DRY RUN] Would delete record${RESET}"
DELETED=$((DELETED + 1))
else
- echo -e " ${RED}✗ Failed to delete record (HTTP $HTTP_CODE)${RESET}"
- echo -e " ${RED}Response: $BODY${RESET}"
- FAILED=$((FAILED + 1))
+ RESPONSE=$(curl -s -w "\n%{http_code}" \
+ -X DELETE \
+ -H "Authorization: Bearer $GANDIV5_PERSONAL_TOKEN" \
+ "$API_URL/$gandi_name/A")
+
+ HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
+ BODY=$(echo "$RESPONSE" | sed '$d')
+
+ if [[ "$HTTP_CODE" == "204" ]] || [[ "$HTTP_CODE" == "200" ]]; then
+ echo -e " ${GREEN}✓ Record deleted successfully${RESET}"
+ DELETED=$((DELETED + 1))
+ else
+ echo -e " ${RED}✗ Failed to delete record (HTTP $HTTP_CODE)${RESET}"
+ echo -e " ${RED}Response: $BODY${RESET}"
+ FAILED=$((FAILED + 1))
+ fi
fi
+
+ echo
fi
+ done <<< "$GANDI_A_NAMES"
- echo
+ echo -e "${BOLD}${GREEN}DNS update complete for $DOMAIN!${RESET}"
+ echo
+ echo -e "${CYAN}Summary:${RESET}"
+ echo -e " Updated: $UPDATED"
+ echo -e " Skipped (unchanged): $SKIPPED"
+ echo -e " Deleted: $DELETED"
+ if [[ $FAILED -gt 0 ]]; then
+ echo -e " ${RED}Failed: $FAILED${RESET}"
fi
-done <<< "$GANDI_A_NAMES"
+ echo
+}
-echo -e "${BOLD}${GREEN}DNS update complete!${RESET}"
-echo
-echo -e "${CYAN}Summary:${RESET}"
-echo -e " Updated: $UPDATED"
-echo -e " Skipped (unchanged): $SKIPPED"
-echo -e " Deleted: $DELETED"
-if [[ $FAILED -gt 0 ]]; then
- echo -e " ${RED}Failed: $FAILED${RESET}"
-fi
+# Main loop
+for domain in "${SELECTED_DOMAINS[@]}"; do
+ update_domain "$domain" "${DOMAIN_ZONE_FILES[$domain]}"
+done