fix(domains): provision routes when domains are linked
Build and publish Docker image / build-and-publish (push) Successful in 10s
Build and publish Docker image / build-and-publish (push) Successful in 10s
This commit is contained in:
1 parent
8cb6820bea
commit
9822e43592
3 files changed
+131
-154
No files matched your search
+68
-70
@@ -12,10 +12,7 @@ from runtime_config import (
|
||||
NODE_IP,
|
||||
logger,
|
||||
)
|
||||
from server_records import get_server_by_id
|
||||
import time
|
||||
from typing import Dict, List, Optional
|
||||
from user_service import get_user_by_id
|
||||
import uuid
|
||||
|
||||
__all__ = ['cleanup_orphaned_ingressroutes', 'create_custom_domain_record', 'create_domain_record', 'create_traefik_ingressroute', 'delete_domain_record', 'delete_traefik_ingressroute', 'get_domain_by_id', 'get_user_custom_domains', 'get_user_domains', 'is_domain_available', 'link_domain_to_server', 'unlink_domain_from_server', 'verify_domain_dns']
|
||||
@@ -23,7 +20,7 @@ __all__ = ['cleanup_orphaned_ingressroutes', 'create_custom_domain_record', 'cre
|
||||
|
||||
|
||||
def create_custom_domain_record(domain_data: Dict) -> Optional[Dict]:
|
||||
"""Create custom domain record in database and create IngressRoute if server_id is provided"""
|
||||
"""Create a custom domain record, then link it if a server was selected."""
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -53,7 +50,7 @@ def create_custom_domain_record(domain_data: Dict) -> Optional[Dict]:
|
||||
domain_data['record_type'],
|
||||
domain_data['ip_address'],
|
||||
domain_data.get('ttl', 3600),
|
||||
domain_data.get('server_id'),
|
||||
None,
|
||||
domain_data.get('created_at', datetime.now().isoformat())
|
||||
))
|
||||
conn.commit()
|
||||
@@ -61,35 +58,9 @@ def create_custom_domain_record(domain_data: Dict) -> Optional[Dict]:
|
||||
cursor.execute('SELECT * FROM user_domains WHERE id = ?', (domain_id,))
|
||||
domain = dict_from_row(cursor.fetchone())
|
||||
|
||||
# IMPORTANT: If server_id is provided, create IngressRoute
|
||||
if domain and domain_data.get('server_id'):
|
||||
# Get user and server information
|
||||
user = get_user_by_id(domain_data['user_id'])
|
||||
server = get_server_by_id(domain_data['server_id'])
|
||||
|
||||
if user and server:
|
||||
namespace = f"user-{user['username']}".lower()
|
||||
service_name = f"{domain_data['server_id']}-service"
|
||||
|
||||
# Get the correct port for the service
|
||||
service_port = 80
|
||||
if server['ports']:
|
||||
ports_data = json.loads(server['ports']) if isinstance(server['ports'], str) else server['ports']
|
||||
for port in ports_data:
|
||||
if port['internal'] == 80:
|
||||
service_port = 80
|
||||
break
|
||||
else:
|
||||
service_port = ports_data[0]['internal'] if ports_data else 80
|
||||
|
||||
# Create IngressRoute for custom domain
|
||||
logger.info(f"Creating IngressRoute for custom domain {domain_data['domain']}")
|
||||
create_traefik_ingressroute(
|
||||
domain=domain_data['domain'],
|
||||
namespace=namespace,
|
||||
service_name=service_name,
|
||||
service_port=service_port
|
||||
)
|
||||
if link_domain_to_server(domain_id, domain_data['server_id']):
|
||||
domain['server_id'] = domain_data['server_id']
|
||||
|
||||
return domain
|
||||
|
||||
@@ -249,7 +220,7 @@ def is_domain_available(domain: str, current_user_id: str) -> Dict:
|
||||
|
||||
|
||||
def create_domain_record(domain_data: Dict) -> Optional[Dict]:
|
||||
"""Create domain record in database"""
|
||||
"""Create a domain record and provision its route when a server is selected."""
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -278,13 +249,17 @@ def create_domain_record(domain_data: Dict) -> Optional[Dict]:
|
||||
domain_data['record_type'],
|
||||
domain_data['ip_address'],
|
||||
domain_data.get('ttl', 3600),
|
||||
domain_data.get('server_id'),
|
||||
None,
|
||||
domain_data.get('created_at', datetime.now().isoformat())
|
||||
))
|
||||
conn.commit()
|
||||
|
||||
cursor.execute('SELECT * FROM user_domains WHERE id = ?', (domain_id,))
|
||||
return dict_from_row(cursor.fetchone())
|
||||
domain = dict_from_row(cursor.fetchone())
|
||||
server_id = domain_data.get('server_id')
|
||||
if domain and server_id and link_domain_to_server(domain_id, server_id):
|
||||
domain['server_id'] = server_id
|
||||
return domain
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating domain record: {e}")
|
||||
@@ -485,22 +460,33 @@ def create_traefik_ingressroute(domain: str, namespace: str, service_name: str,
|
||||
}
|
||||
}
|
||||
|
||||
# Try to delete existing IngressRoute first (if updating)
|
||||
# Replace an existing route atomically so a failed update cannot drop
|
||||
# a domain that is already serving traffic.
|
||||
try:
|
||||
custom_objects_api.delete_namespaced_custom_object(
|
||||
existing = custom_objects_api.get_namespaced_custom_object(
|
||||
group="traefik.io",
|
||||
version="v1alpha1",
|
||||
namespace=namespace,
|
||||
plural="ingressroutes",
|
||||
name=ingressroute_name
|
||||
)
|
||||
logger.info(f"Deleted existing IngressRoute: {ingressroute_name}")
|
||||
time.sleep(1)
|
||||
ingressroute_body['metadata']['resourceVersion'] = existing['metadata']['resourceVersion']
|
||||
custom_objects_api.replace_namespaced_custom_object(
|
||||
group="traefik.io",
|
||||
version="v1alpha1",
|
||||
namespace=namespace,
|
||||
plural="ingressroutes",
|
||||
name=ingressroute_name,
|
||||
body=ingressroute_body
|
||||
)
|
||||
logger.info(f"Successfully updated IngressRoute: {ingressroute_name}")
|
||||
return True
|
||||
except ApiException as e:
|
||||
if e.status != 404:
|
||||
logger.warning(f"Error deleting existing IngressRoute: {e.reason}")
|
||||
logger.error(f"Failed to read existing IngressRoute: {e.status} - {e.reason}")
|
||||
return False
|
||||
|
||||
# Create new IngressRoute
|
||||
# No prior route exists; create it.
|
||||
try:
|
||||
result = custom_objects_api.create_namespaced_custom_object(
|
||||
group="traefik.io",
|
||||
@@ -570,7 +556,7 @@ def delete_traefik_ingressroute(domain: str, namespace: str) -> bool:
|
||||
return False
|
||||
|
||||
def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
||||
"""Link a domain to a server and create IngressRoute"""
|
||||
"""Create the Kubernetes route, then mark the domain as linked."""
|
||||
try:
|
||||
with database_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
@@ -591,6 +577,10 @@ def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
||||
logger.error(f"Server not found: {server_id}")
|
||||
return False
|
||||
|
||||
if server.get('type') not in ('website', 'vps'):
|
||||
logger.error("Domains can only be linked to website and VPS servers")
|
||||
return False
|
||||
|
||||
# Get user details for namespace
|
||||
cursor.execute('SELECT username FROM users WHERE id = ?', (domain['user_id'],))
|
||||
user = cursor.fetchone()
|
||||
@@ -600,7 +590,11 @@ def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
||||
return False
|
||||
|
||||
username = user['username']
|
||||
namespace = f"user-{username}".lower()
|
||||
namespace = (server.get('namespace') or f"user-{username}").lower()
|
||||
domain_host = domain.get('full_domain') or domain.get('domain')
|
||||
if not domain_host:
|
||||
logger.error("Domain record has no hostname: %s", domain_id)
|
||||
return False
|
||||
|
||||
# Determine service name and port
|
||||
service_name = f"{server_id}-service"
|
||||
@@ -621,33 +615,30 @@ def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
||||
else:
|
||||
service_port = 80
|
||||
|
||||
logger.info(f"Linking domain {domain['full_domain']} to server {server['name']}")
|
||||
logger.info(f"Linking domain {domain_host} to server {server['name']}")
|
||||
logger.info(f"Service: {service_name}, Port: {service_port}, Namespace: {namespace}")
|
||||
|
||||
# Update database
|
||||
if not create_traefik_ingressroute(
|
||||
domain=domain_host,
|
||||
namespace=namespace,
|
||||
service_name=service_name,
|
||||
service_port=service_port
|
||||
):
|
||||
logger.error(f"Failed to create IngressRoute for {domain_host}; domain remains unlinked")
|
||||
return False
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE user_domains
|
||||
SET server_id = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (server_id, domain_id))
|
||||
if cursor.rowcount != 1:
|
||||
delete_traefik_ingressroute(domain_host, namespace)
|
||||
conn.rollback()
|
||||
return False
|
||||
conn.commit()
|
||||
|
||||
# Create IngressRoute in Kubernetes
|
||||
if k8s_manager.connected:
|
||||
success = create_traefik_ingressroute(
|
||||
domain=domain['full_domain'],
|
||||
namespace=namespace,
|
||||
service_name=service_name,
|
||||
service_port=service_port
|
||||
)
|
||||
|
||||
if not success:
|
||||
logger.warning(f"Failed to create IngressRoute for {domain['full_domain']}, but database updated")
|
||||
# Don't rollback database - domain is still linked, just no routing yet
|
||||
else:
|
||||
logger.warning("Kubernetes not connected, IngressRoute not created")
|
||||
|
||||
logger.info(f"✅ Successfully linked domain {domain['full_domain']} to server {server['name']}")
|
||||
logger.info(f"Successfully linked domain {domain_host} to server {server['name']}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
@@ -668,21 +659,28 @@ def unlink_domain_from_server(domain_id: str) -> bool:
|
||||
logger.error(f"Domain not found: {domain_id}")
|
||||
return False
|
||||
|
||||
domain_host = domain.get('full_domain') or domain.get('domain')
|
||||
if not domain_host:
|
||||
logger.error("Domain record has no hostname: %s", domain_id)
|
||||
return False
|
||||
|
||||
server = None
|
||||
if domain.get('server_id'):
|
||||
cursor.execute('SELECT namespace FROM servers WHERE id = ?', (domain['server_id'],))
|
||||
server = dict_from_row(cursor.fetchone())
|
||||
|
||||
# Get user details for namespace
|
||||
cursor.execute('SELECT username FROM users WHERE id = ?', (domain['user_id'],))
|
||||
user = cursor.fetchone()
|
||||
|
||||
if user:
|
||||
username = user['username']
|
||||
namespace = f"user-{username}".lower()
|
||||
namespace = ((server or {}).get('namespace') or f"user-{username}").lower()
|
||||
|
||||
# Delete IngressRoute in Kubernetes
|
||||
if k8s_manager.connected:
|
||||
logger.info(f"Deleting IngressRoute for domain: {domain['full_domain']} in namespace: {namespace}")
|
||||
delete_traefik_ingressroute(
|
||||
domain=domain['full_domain'],
|
||||
namespace=namespace
|
||||
)
|
||||
if not delete_traefik_ingressroute(domain=domain_host, namespace=namespace):
|
||||
logger.error("Failed to remove IngressRoute for %s; domain remains linked", domain_host)
|
||||
return False
|
||||
|
||||
# Update database to remove server link
|
||||
cursor.execute('''
|
||||
@@ -692,7 +690,7 @@ def unlink_domain_from_server(domain_id: str) -> bool:
|
||||
''', (domain_id,))
|
||||
conn.commit()
|
||||
|
||||
logger.info(f"✅ Successfully unlinked domain {domain['full_domain']} from server")
|
||||
logger.info(f"Successfully unlinked domain {domain_host} from server")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
|
||||
Reference in new issue
Block a user