701 lines
27 KiB
Python
701 lines
27 KiB
Python
"""Domain persistence, DNS validation, and Kubernetes ingress operations."""
|
|
|
|
|
|
from database_service import database_connection, dict_from_row
|
|
from datetime import datetime
|
|
from domain_runtime import domain_manager
|
|
import json
|
|
from kubernetes.client.rest import ApiException
|
|
from kubernetes_service import custom_objects_api, k8s_manager
|
|
import re
|
|
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']
|
|
|
|
|
|
|
|
def create_custom_domain_record(domain_data: Dict) -> Optional[Dict]:
|
|
"""Create custom domain record in database and create IngressRoute if server_id is provided"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Check if custom domain already exists for this user
|
|
cursor.execute('''
|
|
SELECT id FROM user_domains
|
|
WHERE domain = ? AND user_id = ? AND is_custom = TRUE
|
|
''', (domain_data['domain'], domain_data['user_id']))
|
|
|
|
if cursor.fetchone():
|
|
return None
|
|
|
|
domain_id = f"custom-{str(uuid.uuid4())[:8]}"
|
|
|
|
cursor.execute('''
|
|
INSERT INTO user_domains (id, user_id, domain, is_custom, verified,
|
|
record_type, ip_address, ttl,
|
|
server_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
domain_id,
|
|
domain_data['user_id'],
|
|
domain_data['domain'],
|
|
True, # is_custom
|
|
True, # verified (since we checked)
|
|
domain_data['record_type'],
|
|
domain_data['ip_address'],
|
|
domain_data.get('ttl', 3600),
|
|
domain_data.get('server_id'),
|
|
domain_data.get('created_at', datetime.now().isoformat())
|
|
))
|
|
conn.commit()
|
|
|
|
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
|
|
)
|
|
|
|
return domain
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating custom domain record: {e}")
|
|
return None
|
|
|
|
def get_user_custom_domains(user_id: str) -> List[Dict]:
|
|
"""Get all custom domains for a user"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
cursor.execute('''
|
|
SELECT ud.*, s.name as server_name, s.type as server_type
|
|
FROM user_domains ud
|
|
LEFT JOIN servers s ON ud.server_id = s.id
|
|
WHERE ud.user_id = ? AND ud.is_custom = TRUE
|
|
ORDER BY ud.created_at DESC
|
|
''', (user_id,))
|
|
|
|
domains = [dict_from_row(row) for row in cursor.fetchall()]
|
|
return domains
|
|
except Exception as e:
|
|
logger.error(f"Error getting user custom domains: {e}")
|
|
return []
|
|
|
|
|
|
|
|
|
|
def verify_domain_dns(domain: str) -> Dict:
|
|
"""Verify if a domain points to our server IP and check domain restrictions"""
|
|
try:
|
|
import socket
|
|
|
|
# Check if domain is from restricted domains
|
|
restricted_domains = ['kvj.ovh', 'anchorhost.eu']
|
|
|
|
# Block subdomains of restricted domains
|
|
for restricted in restricted_domains:
|
|
if domain.endswith(f'.{restricted}') or domain == restricted:
|
|
return {
|
|
'verified': False,
|
|
'message': f'Domain from {restricted} cannot be used as custom domain',
|
|
'current_ip': None,
|
|
'blocked': True,
|
|
'restricted_domain': restricted
|
|
}
|
|
|
|
# Check if domain already exists in database (used by another user)
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT ud.*, u.username
|
|
FROM user_domains ud
|
|
JOIN users u ON ud.user_id = u.id
|
|
WHERE ud.domain = ? AND ud.is_custom = TRUE
|
|
LIMIT 1
|
|
''', (domain.lower(),))
|
|
|
|
existing_domain = dict_from_row(cursor.fetchone())
|
|
|
|
if existing_domain:
|
|
return {
|
|
'verified': False,
|
|
'message': f'Domain already used by user {existing_domain["username"]}',
|
|
'current_ip': None,
|
|
'already_used': True,
|
|
'used_by': existing_domain['username']
|
|
}
|
|
except Exception as db_error:
|
|
logger.error(f"Error checking database for domain: {db_error}")
|
|
# Continue with DNS check even if database check fails
|
|
|
|
# Get IP address from domain
|
|
try:
|
|
ip_address = socket.gethostbyname(domain)
|
|
except socket.gaierror:
|
|
return {
|
|
'verified': False,
|
|
'message': 'Domain does not exist or cannot be resolved',
|
|
'current_ip': None
|
|
}
|
|
|
|
# Check if it points to our server
|
|
if ip_address == NODE_IP:
|
|
return {
|
|
'verified': True,
|
|
'message': f'Domain points to our server ({NODE_IP})',
|
|
'ip_address': ip_address
|
|
}
|
|
else:
|
|
return {
|
|
'verified': False,
|
|
'message': f'Domain points to {ip_address}, not our server ({NODE_IP})',
|
|
'current_ip': ip_address
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error verifying domain DNS: {e}")
|
|
return {
|
|
'verified': False,
|
|
'message': f'Error verifying domain: {str(e)}',
|
|
'current_ip': None
|
|
}
|
|
|
|
|
|
def is_domain_available(domain: str, current_user_id: str) -> Dict:
|
|
"""Check if a custom domain is available for use"""
|
|
try:
|
|
domain = domain.lower().strip()
|
|
|
|
# Check restricted domains
|
|
restricted_domains = ['kvj.ovh', 'anchorhost.eu']
|
|
for restricted in restricted_domains:
|
|
if domain.endswith(f'.{restricted}') or domain == restricted:
|
|
return {
|
|
'available': False,
|
|
'message': f'Domain from {restricted} cannot be used as custom domain',
|
|
'reason': 'restricted'
|
|
}
|
|
|
|
# Check if already used by other users
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT ud.*, u.username
|
|
FROM user_domains ud
|
|
JOIN users u ON ud.user_id = u.id
|
|
WHERE ud.domain = ? AND ud.is_custom = TRUE AND ud.user_id != ?
|
|
LIMIT 1
|
|
''', (domain, current_user_id))
|
|
|
|
existing_domain = dict_from_row(cursor.fetchone())
|
|
|
|
if existing_domain:
|
|
return {
|
|
'available': False,
|
|
'message': f'Domain already used by user {existing_domain["username"]}',
|
|
'reason': 'already_used'
|
|
}
|
|
|
|
return {
|
|
'available': True,
|
|
'message': 'Domain is available'
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error checking domain availability: {e}")
|
|
return {
|
|
'available': False,
|
|
'message': f'Error checking domain availability: {str(e)}',
|
|
'reason': 'error'
|
|
}
|
|
|
|
|
|
|
|
def create_domain_record(domain_data: Dict) -> Optional[Dict]:
|
|
"""Create domain record in database"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Check if domain already exists
|
|
cursor.execute('''
|
|
SELECT id FROM user_domains
|
|
WHERE full_domain = ? AND user_id = ?
|
|
''', (domain_data['full_domain'], domain_data['user_id']))
|
|
|
|
if cursor.fetchone():
|
|
return None
|
|
|
|
domain_id = f"domain-{str(uuid.uuid4())[:8]}"
|
|
|
|
cursor.execute('''
|
|
INSERT INTO user_domains (id, user_id, subdomain, full_domain,
|
|
record_type, ip_address, ttl,
|
|
server_id, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
''', (
|
|
domain_id,
|
|
domain_data['user_id'],
|
|
domain_data['subdomain'],
|
|
domain_data['full_domain'],
|
|
domain_data['record_type'],
|
|
domain_data['ip_address'],
|
|
domain_data.get('ttl', 3600),
|
|
domain_data.get('server_id'),
|
|
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())
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error creating domain record: {e}")
|
|
return None
|
|
|
|
def get_user_domains(user_id: str, include_serverless: bool = True) -> List[Dict]:
|
|
"""Get all domains for a user"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
query = '''
|
|
SELECT ud.*, s.name as server_name, s.type as server_type
|
|
FROM user_domains ud
|
|
LEFT JOIN servers s ON ud.server_id = s.id
|
|
WHERE ud.user_id = ?
|
|
'''
|
|
|
|
if not include_serverless:
|
|
query += ' AND ud.server_id IS NOT NULL'
|
|
|
|
query += ' ORDER BY ud.created_at DESC'
|
|
|
|
cursor.execute(query, (user_id,))
|
|
domains = [dict_from_row(row) for row in cursor.fetchall()]
|
|
|
|
return domains
|
|
except Exception as e:
|
|
logger.error(f"Error getting user domains: {e}")
|
|
return []
|
|
|
|
def get_domain_by_id(domain_id: str) -> Optional[Dict]:
|
|
"""Get domain by ID"""
|
|
try:
|
|
domain_id = str(domain_id)
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
cursor.execute('''
|
|
SELECT ud.*, s.name as server_name, s.type as server_type, s.k8s_name
|
|
FROM user_domains ud
|
|
LEFT JOIN servers s ON ud.server_id = s.id
|
|
WHERE ud.id = ?
|
|
''', (domain_id,))
|
|
return dict_from_row(cursor.fetchone())
|
|
except Exception as e:
|
|
logger.error(f"Error getting domain: {e}")
|
|
return None
|
|
|
|
def cleanup_orphaned_ingressroutes():
|
|
"""Clean up orphaned IngressRoutes (domains that no longer exist)"""
|
|
if not k8s_manager.connected:
|
|
return
|
|
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get all user domains from database
|
|
cursor.execute('SELECT ud.full_domain, u.username FROM user_domains ud JOIN users u ON ud.user_id = u.id')
|
|
db_domains = {row['full_domain']: row['username'] for row in cursor.fetchall()}
|
|
|
|
# For each user namespace, check IngressRoutes
|
|
for domain, username in db_domains.items():
|
|
if not domain or not username:
|
|
continue
|
|
namespace = f"user-{username}".lower()
|
|
subdomain = domain.split('.')[0]
|
|
ingressroute_name = f"{subdomain}-ingress"
|
|
|
|
try:
|
|
# Check if IngressRoute exists
|
|
ingress = custom_objects_api.get_namespaced_custom_object(
|
|
group="traefik.io",
|
|
version="v1alpha1",
|
|
namespace=namespace,
|
|
plural="ingressroutes",
|
|
name=ingressroute_name
|
|
)
|
|
|
|
# If domain exists in DB but is not linked to a server, delete the IngressRoute
|
|
cursor.execute('SELECT server_id FROM user_domains WHERE full_domain = ?', (domain,))
|
|
domain_row = cursor.fetchone()
|
|
|
|
if domain_row and not domain_row['server_id']:
|
|
# Domain exists but not linked to any server, delete orphaned IngressRoute
|
|
logger.info(f"Deleting orphaned IngressRoute for unlinked domain: {domain}")
|
|
delete_traefik_ingressroute(domain, namespace)
|
|
|
|
except ApiException as e:
|
|
if e.status == 404:
|
|
# IngressRoute doesn't exist, which is fine
|
|
pass
|
|
else:
|
|
logger.error(f"Error checking IngressRoute for {domain}: {e}")
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error cleaning up orphaned IngressRoutes: {e}")
|
|
|
|
def delete_domain_record(domain_id: str) -> bool:
|
|
"""Delete domain record from database"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get domain info before deleting (for logging/cleanup)
|
|
cursor.execute('SELECT * FROM user_domains WHERE id = ?', (domain_id,))
|
|
domain = dict_from_row(cursor.fetchone())
|
|
|
|
if domain:
|
|
logger.info(f"Deleting domain record: {domain.get('full_domain')} with server_id: {domain.get('server_id')}")
|
|
|
|
# If domain was linked to a server, we should have already unlinked it
|
|
# But just in case, log if there's still a server_id
|
|
if domain.get('server_id'):
|
|
logger.warning(f"Domain {domain['full_domain']} still has server_id {domain['server_id']} during deletion")
|
|
|
|
# Delete the domain record
|
|
cursor.execute('DELETE FROM user_domains WHERE id = ?', (domain_id,))
|
|
conn.commit()
|
|
|
|
deleted = cursor.rowcount > 0
|
|
if deleted and domain:
|
|
logger.info(f"✓ Deleted domain record: {domain.get('full_domain')}")
|
|
|
|
return deleted
|
|
except Exception as e:
|
|
logger.error(f"Error deleting domain record: {e}")
|
|
return False
|
|
|
|
def create_traefik_ingressroute(domain: str, namespace: str, service_name: str, service_port: int) -> bool:
|
|
"""
|
|
Create a Traefik IngressRoute for the domain
|
|
Supports both delegated domains (subdomain.u-username.anchorhost.eu)
|
|
and custom domains (anchor.opime.online)
|
|
"""
|
|
try:
|
|
if not k8s_manager.connected:
|
|
logger.warning("Kubernetes not connected, skipping IngressRoute creation")
|
|
return False
|
|
|
|
# FIXED: Better validation
|
|
if not domain or domain == 'None':
|
|
logger.error(f"Invalid domain name: {domain}")
|
|
return False
|
|
|
|
if not namespace or not service_name or not service_port:
|
|
logger.error(f"Missing required parameters: namespace={namespace}, service={service_name}, port={service_port}")
|
|
return False
|
|
|
|
# For IngressRoute name, sanitize the domain to be K8s-friendly
|
|
# Replace dots with hyphens and remove any invalid characters
|
|
ingressroute_name = domain.lower().replace('.', '-').replace('*', 'wildcard')[:63]
|
|
# Ensure it starts and ends with alphanumeric
|
|
ingressroute_name = re.sub(r'^[^a-z0-9]+|[^a-z0-9]+$', '', ingressroute_name)
|
|
if not ingressroute_name:
|
|
ingressroute_name = f"ingress-{str(uuid.uuid4())[:8]}"
|
|
|
|
logger.info(f"Creating IngressRoute: {ingressroute_name}")
|
|
logger.info(f" Domain: {domain}")
|
|
logger.info(f" Namespace: {namespace}")
|
|
logger.info(f" Service: {service_name}:{service_port}")
|
|
|
|
# Create IngressRoute manifest
|
|
ingressroute_body = {
|
|
'apiVersion': 'traefik.io/v1alpha1',
|
|
'kind': 'IngressRoute',
|
|
'metadata': {
|
|
'name': ingressroute_name,
|
|
'namespace': namespace,
|
|
'labels': {
|
|
'app': service_name,
|
|
'managed-by': 'anchorhost',
|
|
'domain': domain,
|
|
'domain-type': 'custom' if '.' in domain and not domain.endswith(f'.{domain_manager.base_domain}') else 'delegated'
|
|
},
|
|
'annotations': {
|
|
'anchorhost.eu/created-at': datetime.now().isoformat(),
|
|
'anchorhost.eu/domain': domain
|
|
}
|
|
},
|
|
'spec': {
|
|
'entryPoints': ['web', 'websecure'],
|
|
'routes': [
|
|
{
|
|
'match': f"Host(`{domain}`)",
|
|
'kind': 'Rule',
|
|
'services': [
|
|
{
|
|
'name': service_name,
|
|
'port': service_port
|
|
}
|
|
]
|
|
}
|
|
],
|
|
'tls': {
|
|
'certResolver': 'letsencrypt'
|
|
}
|
|
}
|
|
}
|
|
|
|
# Try to delete existing IngressRoute first (if updating)
|
|
try:
|
|
custom_objects_api.delete_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)
|
|
except ApiException as e:
|
|
if e.status != 404:
|
|
logger.warning(f"Error deleting existing IngressRoute: {e.reason}")
|
|
|
|
# Create new IngressRoute
|
|
try:
|
|
result = custom_objects_api.create_namespaced_custom_object(
|
|
group="traefik.io",
|
|
version="v1alpha1",
|
|
namespace=namespace,
|
|
plural="ingressroutes",
|
|
body=ingressroute_body
|
|
)
|
|
|
|
logger.info(f"✅ Successfully created IngressRoute: {ingressroute_name}")
|
|
return True
|
|
|
|
except ApiException as e:
|
|
logger.error(f"Failed to create IngressRoute: {e.status} - {e.reason}")
|
|
if hasattr(e, 'body'):
|
|
logger.error(f"Error details: {e.body}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error creating IngressRoute: {e}", exc_info=True)
|
|
return False
|
|
|
|
def delete_traefik_ingressroute(domain: str, namespace: str) -> bool:
|
|
"""
|
|
Delete a Traefik IngressRoute for the domain
|
|
"""
|
|
try:
|
|
if not k8s_manager.connected:
|
|
logger.warning("Kubernetes not connected, skipping IngressRoute deletion")
|
|
return False
|
|
|
|
# FIXED: Better validation
|
|
if not domain or domain == 'None':
|
|
logger.error(f"Invalid domain name: {domain}")
|
|
return False
|
|
|
|
ingressroute_name = domain.lower().replace('.', '-').replace('*', 'wildcard')[:63]
|
|
ingressroute_name = re.sub(r'^[^a-z0-9]+|[^a-z0-9]+$', '', ingressroute_name)
|
|
if not ingressroute_name:
|
|
logger.error("Invalid domain format: %s", domain)
|
|
return False
|
|
|
|
logger.info(f"Deleting IngressRoute for domain: {domain} in namespace: {namespace}")
|
|
|
|
try:
|
|
custom_objects_api.delete_namespaced_custom_object(
|
|
group="traefik.io",
|
|
version="v1alpha1",
|
|
namespace=namespace,
|
|
plural="ingressroutes",
|
|
name=ingressroute_name
|
|
)
|
|
|
|
logger.info(f"✅ Successfully deleted IngressRoute: {ingressroute_name}")
|
|
return True
|
|
|
|
except ApiException as e:
|
|
if e.status == 404:
|
|
logger.info(f"IngressRoute {ingressroute_name} not found (may have been already deleted)")
|
|
return True
|
|
else:
|
|
logger.error(f"Failed to delete IngressRoute: {e.status} - {e.reason}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error deleting IngressRoute for domain {domain}: {e}", exc_info=True)
|
|
return False
|
|
|
|
def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
|
"""Link a domain to a server and create IngressRoute"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get domain details
|
|
cursor.execute('SELECT * FROM user_domains WHERE id = ?', (domain_id,))
|
|
domain = dict_from_row(cursor.fetchone())
|
|
|
|
if not domain:
|
|
logger.error(f"Domain not found: {domain_id}")
|
|
return False
|
|
|
|
# Get server details
|
|
cursor.execute('SELECT * FROM servers WHERE id = ?', (server_id,))
|
|
server = dict_from_row(cursor.fetchone())
|
|
|
|
if not server:
|
|
logger.error(f"Server not found: {server_id}")
|
|
return False
|
|
|
|
# Get user details for namespace
|
|
cursor.execute('SELECT username FROM users WHERE id = ?', (domain['user_id'],))
|
|
user = cursor.fetchone()
|
|
|
|
if not user:
|
|
logger.error(f"User not found: {domain['user_id']}")
|
|
return False
|
|
|
|
username = user['username']
|
|
namespace = f"user-{username}".lower()
|
|
|
|
# Determine service name and port
|
|
service_name = f"{server_id}-service"
|
|
|
|
# Get the correct port for the service
|
|
if server['ports']:
|
|
ports_data = json.loads(server['ports']) if isinstance(server['ports'], str) else server['ports']
|
|
|
|
# For website/VPS, use port 80 if available, otherwise use first port
|
|
service_port = 80
|
|
for port in ports_data:
|
|
if port['internal'] == 80:
|
|
service_port = 80
|
|
break
|
|
else:
|
|
# Use first available port if 80 not found
|
|
service_port = ports_data[0]['internal'] if ports_data else 80
|
|
else:
|
|
service_port = 80
|
|
|
|
logger.info(f"Linking domain {domain['full_domain']} to server {server['name']}")
|
|
logger.info(f"Service: {service_name}, Port: {service_port}, Namespace: {namespace}")
|
|
|
|
# Update database
|
|
cursor.execute('''
|
|
UPDATE user_domains
|
|
SET server_id = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''', (server_id, domain_id))
|
|
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']}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error linking domain to server: {e}", exc_info=True)
|
|
return False
|
|
|
|
def unlink_domain_from_server(domain_id: str) -> bool:
|
|
"""Unlink a domain from any server and delete IngressRoute"""
|
|
try:
|
|
with database_connection() as conn:
|
|
cursor = conn.cursor()
|
|
|
|
# Get domain details before unlinking
|
|
cursor.execute('SELECT * FROM user_domains WHERE id = ?', (domain_id,))
|
|
domain = dict_from_row(cursor.fetchone())
|
|
|
|
if not domain:
|
|
logger.error(f"Domain not found: {domain_id}")
|
|
return False
|
|
|
|
# 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()
|
|
|
|
# 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
|
|
)
|
|
|
|
# Update database to remove server link
|
|
cursor.execute('''
|
|
UPDATE user_domains
|
|
SET server_id = NULL, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ?
|
|
''', (domain_id,))
|
|
conn.commit()
|
|
|
|
logger.info(f"✅ Successfully unlinked domain {domain['full_domain']} from server")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error unlinking domain: {e}", exc_info=True)
|
|
return False
|