Refactor backend and add registry build workflow
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
Build and publish Docker image / build-and-publish (push) Successful in 2m45s
This commit is contained in:
commit
c177d7981d
43 files changed
+18124
No files matched your search
@@ -0,0 +1,700 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user