Skip to content

Commit 8a1222d

Browse files
authored
Refactor Route53DNSProvider for weighted routing support
Refactor Route53DNSProvider methods to add support for weighted routing. Update methods to handle weighted routing parameters and adjust record ID parsing for weighted records.
1 parent ce32c25 commit 8a1222d

1 file changed

Lines changed: 100 additions & 45 deletions

File tree

  • custom-domain/dstack-ingress/scripts/dns_providers

custom-domain/dstack-ingress/scripts/dns_providers/route53.py

Lines changed: 100 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
import os
44
import sys
5+
import time
56
from typing import List, Optional
67
from .base import DNSProvider, DNSRecord, CAARecord, RecordType
78

89

9-
1010
class Route53DNSProvider(DNSProvider):
1111
"""DNS provider implementation for AWS Route53."""
1212

@@ -40,19 +40,8 @@ def __init__(self):
4040
self.hosted_zone_id: Optional[str] = None
4141
self.hosted_zone_name: Optional[str] = None
4242

43-
4443
def setup_certbot_credentials(self) -> bool:
45-
"""Setup AWS credentials file for certbot.
46-
47-
This container will be provided with aws credentials purely for the purpose
48-
of assuming a role. Doing so will enable the boto platform to provision
49-
temporary access key and secret keys on demand!
50-
51-
Using this strategy we can impose least permissive and fast expiring access
52-
to our domain.
53-
54-
"""
55-
44+
"""Setup AWS credentials file for certbot."""
5645
try:
5746
# Pre-fetch hosted zone ID if we have a domain
5847
domain = os.getenv("DOMAIN")
@@ -77,11 +66,7 @@ def validate_credentials(self) -> bool:
7766
return False
7867

7968
def _get_hosted_zone_info(self, domain: str) -> Optional[tuple[str, str]]:
80-
"""Get the hosted zone ID and name for a domain.
81-
82-
Returns:
83-
Tuple of (hosted_zone_id, hosted_zone_name) or None
84-
"""
69+
"""Get the hosted zone ID and name for a domain."""
8570
try:
8671
# List all hosted zones
8772
paginator = self.client.get_paginator("list_hosted_zones")
@@ -142,7 +127,7 @@ def _normalize_record_name(self, name: str) -> str:
142127
def get_dns_records(
143128
self, name: str, record_type: Optional[RecordType] = None
144129
) -> List[DNSRecord]:
145-
"""Get DNS records for a domain."""
130+
"""Get DNS records for a domain, including Weighted Record info."""
146131
hosted_zone_id = self._ensure_hosted_zone_id(name)
147132
if not hosted_zone_id:
148133
print(
@@ -174,7 +159,13 @@ def get_dns_records(
174159

175160
# Parse record content
176161
content = ""
177-
data = None
162+
data = {}
163+
164+
# --- Check for Weighted Routing Params ---
165+
if "Weight" in record_set:
166+
data["weight"] = record_set["Weight"]
167+
if "SetIdentifier" in record_set:
168+
data["set_identifier"] = record_set["SetIdentifier"]
178169

179170
if record_type_str == "CAA":
180171
# CAA records have special format
@@ -187,11 +178,12 @@ def get_dns_records(
187178
tag = parts[1]
188179
value = parts[2].strip('"')
189180
content = caa_value
190-
data = {"flags": flags, "tag": tag, "value": value}
181+
data.update(
182+
{"flags": flags, "tag": tag, "value": value}
183+
)
191184
else:
192185
# Standard records
193186
if "ResourceRecords" in record_set:
194-
# Get first record value (multiple values would need separate DNSRecord objects)
195187
content = record_set["ResourceRecords"][0]["Value"]
196188
# Remove quotes from TXT records
197189
if record_type_str == "TXT":
@@ -200,8 +192,12 @@ def get_dns_records(
200192
# Alias record (Route53 specific)
201193
content = record_set["AliasTarget"]["DNSName"].rstrip(".")
202194

203-
# Route53 doesn't have persistent record IDs, use name+type as identifier
195+
# Route53 doesn't have persistent record IDs.
196+
# Standard: name:type
197+
# Weighted: name:type:set_identifier
204198
record_id = f"{record_name}:{record_type_str}"
199+
if "set_identifier" in data:
200+
record_id = f"{record_id}:{data['set_identifier']}"
205201

206202
records.append(
207203
DNSRecord(
@@ -211,8 +207,8 @@ def get_dns_records(
211207
content=content,
212208
ttl=record_set.get("TTL", 60),
213209
proxied=False, # Route53 doesn't have proxy feature
214-
priority=None, # Would be in record value for MX/SRV
215-
data=data,
210+
priority=None,
211+
data=data, # Contains weight/id if present
216212
)
217213
)
218214

@@ -223,7 +219,10 @@ def get_dns_records(
223219
return []
224220

225221
def create_dns_record(self, record: DNSRecord) -> bool:
226-
"""Create a DNS record."""
222+
"""
223+
Create a DNS record.
224+
Injects Weighted Routing if ROUTE53_INITIAL_WEIGHT env var is numeric.
225+
"""
227226
hosted_zone_id = self._ensure_hosted_zone_id(record.name)
228227
if not hosted_zone_id:
229228
print(
@@ -241,17 +240,55 @@ def create_dns_record(self, record: DNSRecord) -> bool:
241240
else:
242241
record_value = record.content
243242

243+
# Build Resource Record Set
244+
resource_record_set = {
245+
"Name": normalized_name,
246+
"Type": record.type.value,
247+
"TTL": record.ttl,
248+
"ResourceRecords": [{"Value": record_value}],
249+
}
250+
251+
# --- Handle Weighted Routing Injection ---
252+
253+
# 1. Determine Weight
254+
weight_to_set = None
255+
256+
# Check explicit data first (Controller overrides Env Var)
257+
if record.data and "weight" in record.data:
258+
weight_to_set = record.data["weight"]
259+
else:
260+
# Check Env Var
261+
env_weight = os.getenv("ROUTE53_INITIAL_WEIGHT")
262+
if env_weight is not None and env_weight.isdigit():
263+
weight_to_set = int(env_weight)
264+
265+
# 2. Determine Identifier (Required if Weight is set)
266+
set_identifier = None
267+
268+
if weight_to_set is not None:
269+
# Check explicit data first
270+
if record.data and "set_identifier" in record.data:
271+
set_identifier = record.data["set_identifier"]
272+
else:
273+
# Generate unique ID if strictly injecting via Env Var
274+
# Format: auto-{timestamp} to prevent collisions on repeated runs
275+
set_identifier = f"auto-{int(time.time())}"
276+
277+
# 3. Apply to Record Set
278+
if weight_to_set is not None and set_identifier:
279+
resource_record_set["Weight"] = int(weight_to_set)
280+
resource_record_set["SetIdentifier"] = str(set_identifier)
281+
print(
282+
f" > Weighted Routing Active: Weight={weight_to_set}, "
283+
f"ID='{set_identifier}' (Source: {'ENV' if not record.data or 'weight' not in record.data else 'Explicit'})"
284+
)
285+
244286
# Prepare change batch
245287
change_batch = {
246288
"Changes": [
247289
{
248290
"Action": "UPSERT", # UPSERT creates or updates
249-
"ResourceRecordSet": {
250-
"Name": normalized_name,
251-
"Type": record.type.value,
252-
"TTL": record.ttl,
253-
"ResourceRecords": [{"Value": record_value}],
254-
},
291+
"ResourceRecordSet": resource_record_set,
255292
}
256293
]
257294
}
@@ -281,7 +318,9 @@ def delete_dns_record(self, record_id: str, domain: str) -> bool:
281318
"""Delete a DNS record.
282319
283320
Args:
284-
record_id: Format is "name:type" since Route53 doesn't have persistent IDs
321+
record_id:
322+
Standard: "name:type"
323+
Weighted: "name:type:set_identifier"
285324
domain: The domain name (for zone lookup)
286325
"""
287326
hosted_zone_id = self._ensure_hosted_zone_id(domain)
@@ -292,10 +331,15 @@ def delete_dns_record(self, record_id: str, domain: str) -> bool:
292331
)
293332
return False
294333

295-
# Parse record_id to get name and type
296-
try:
297-
record_name, record_type = record_id.split(":", 1)
298-
except ValueError:
334+
# Parse record_id
335+
# Format can be "name:type" OR "name:type:set_identifier"
336+
parts = record_id.split(":")
337+
if len(parts) == 2:
338+
record_name, record_type = parts
339+
set_identifier = None
340+
elif len(parts) == 3:
341+
record_name, record_type, set_identifier = parts
342+
else:
299343
print(f"Invalid record_id format: {record_id}", file=sys.stderr)
300344
return False
301345

@@ -306,12 +350,27 @@ def delete_dns_record(self, record_id: str, domain: str) -> bool:
306350

307351
for page in paginator.paginate(HostedZoneId=hosted_zone_id):
308352
for record_set in page["ResourceRecordSets"]:
309-
if (
310-
record_set["Name"] == record_name
311-
and record_set["Type"] == record_type
312-
):
353+
# Basic Match
354+
name_match = record_set["Name"] == record_name
355+
type_match = record_set["Type"] == record_type
356+
357+
# Identifier Match (for weighted records)
358+
id_match = True
359+
if set_identifier:
360+
# If we asked for a specific ID, the record must match it
361+
if record_set.get("SetIdentifier") != set_identifier:
362+
id_match = False
363+
364+
# If we have a set_identifier in the record but none in record_id,
365+
# we must be careful. However, standard deletion usually implies
366+
# non-weighted. If the user passes a simple ID for a weighted record,
367+
# this logic might need to be stricter, but for now strict match on
368+
# requested ID is safest.
369+
370+
if name_match and type_match and id_match:
313371
record_set_to_delete = record_set
314372
break
373+
315374
if record_set_to_delete:
316375
break
317376

@@ -348,10 +407,6 @@ def delete_dns_record(self, record_id: str, domain: str) -> bool:
348407
def create_caa_record(self, caa_record: CAARecord) -> bool:
349408
"""
350409
Create or merge a CAA record set on the apex of the Route53 hosted zone.
351-
352-
- Ignores the specific subdomain in caa_record.name for placement
353-
- Uses it only to locate the correct hosted zone
354-
- Merges hard-coded issuers with any existing CAA values on the apex
355410
"""
356411
# Ensure we know which hosted zone this belongs to
357412
hosted_zone_id = self._ensure_hosted_zone_id(caa_record.name)

0 commit comments

Comments
 (0)