Fix Kubernetes namespaces for dotted usernames
Build and publish Docker image / build-and-publish (push) Successful in 8s
Build and publish Docker image / build-and-publish (push) Successful in 8s
This commit is contained in:
1 parent
522e5c5634
commit
6732ee9d47
8 files changed
+70
-19
No files matched your search
+4
-3
@@ -7,6 +7,7 @@ from domain_runtime import domain_manager
|
|||||||
import json
|
import json
|
||||||
from kubernetes.client.rest import ApiException
|
from kubernetes.client.rest import ApiException
|
||||||
from kubernetes_service import custom_objects_api, k8s_manager
|
from kubernetes_service import custom_objects_api, k8s_manager
|
||||||
|
from namespace_utils import user_namespace
|
||||||
import re
|
import re
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
NODE_IP,
|
NODE_IP,
|
||||||
@@ -325,7 +326,7 @@ def cleanup_orphaned_ingressroutes():
|
|||||||
for domain, username in db_domains.items():
|
for domain, username in db_domains.items():
|
||||||
if not domain or not username:
|
if not domain or not username:
|
||||||
continue
|
continue
|
||||||
namespace = f"user-{username}".lower()
|
namespace = user_namespace(username)
|
||||||
subdomain = domain.split('.')[0]
|
subdomain = domain.split('.')[0]
|
||||||
ingressroute_name = f"{subdomain}-ingress"
|
ingressroute_name = f"{subdomain}-ingress"
|
||||||
|
|
||||||
@@ -590,7 +591,7 @@ def link_domain_to_server(domain_id: str, server_id: str) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
username = user['username']
|
username = user['username']
|
||||||
namespace = (server.get('namespace') or f"user-{username}").lower()
|
namespace = server.get('namespace') or user_namespace(username)
|
||||||
domain_host = domain.get('full_domain') or domain.get('domain')
|
domain_host = domain.get('full_domain') or domain.get('domain')
|
||||||
if not domain_host:
|
if not domain_host:
|
||||||
logger.error("Domain record has no hostname: %s", domain_id)
|
logger.error("Domain record has no hostname: %s", domain_id)
|
||||||
@@ -675,7 +676,7 @@ def unlink_domain_from_server(domain_id: str) -> bool:
|
|||||||
|
|
||||||
if user:
|
if user:
|
||||||
username = user['username']
|
username = user['username']
|
||||||
namespace = ((server or {}).get('namespace') or f"user-{username}").lower()
|
namespace = (server or {}).get('namespace') or user_namespace(username)
|
||||||
|
|
||||||
# Delete IngressRoute in Kubernetes
|
# Delete IngressRoute in Kubernetes
|
||||||
if not delete_traefik_ingressroute(domain=domain_host, namespace=namespace):
|
if not delete_traefik_ingressroute(domain=domain_host, namespace=namespace):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from datetime import datetime
|
|||||||
from kubernetes import client, config
|
from kubernetes import client, config
|
||||||
from kubernetes.client.rest import ApiException
|
from kubernetes.client.rest import ApiException
|
||||||
from kubernetes.stream import stream
|
from kubernetes.stream import stream
|
||||||
|
from namespace_utils import is_valid_namespace_name
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
@@ -62,8 +63,9 @@ class KubernetesManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def create_namespace(self, namespace_name: str, labels: Dict = None) -> bool:
|
def create_namespace(self, namespace_name: str, labels: Dict = None) -> bool:
|
||||||
# Kubernetes namespaces MUST be lowercase
|
if not is_valid_namespace_name(namespace_name):
|
||||||
namespace_name = namespace_name.lower()
|
logger.error("Refusing to create invalid Kubernetes namespace name: %r", namespace_name)
|
||||||
|
return False
|
||||||
|
|
||||||
if not self.connected:
|
if not self.connected:
|
||||||
logger.info(f"⚠ Kubernetes not connected, skipping namespace creation for {namespace_name}")
|
logger.info(f"⚠ Kubernetes not connected, skipping namespace creation for {namespace_name}")
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""Helpers for translating account names to Kubernetes namespace names."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
_DNS_LABEL = re.compile(r"^[a-z0-9](?:[-a-z0-9]*[a-z0-9])?$")
|
||||||
|
_INVALID_DNS_LABEL_CHARS = re.compile(r"[^a-z0-9-]+")
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_namespace_name(namespace_name: str) -> bool:
|
||||||
|
"""Return whether a name meets Kubernetes DNS-label constraints."""
|
||||||
|
return (
|
||||||
|
isinstance(namespace_name, str)
|
||||||
|
and len(namespace_name) <= 63
|
||||||
|
and bool(_DNS_LABEL.fullmatch(namespace_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def user_namespace(username: str) -> str:
|
||||||
|
"""Return a stable, DNS-label-safe namespace for a username.
|
||||||
|
|
||||||
|
Existing valid usernames retain their current namespace. Names that need
|
||||||
|
normalization receive a short digest so normalization cannot silently
|
||||||
|
merge accounts such as ``miki.dz08`` and ``miki-dz08``.
|
||||||
|
"""
|
||||||
|
normalized_username = str(username).strip().lower()
|
||||||
|
if not normalized_username:
|
||||||
|
raise ValueError("Username must not be empty")
|
||||||
|
|
||||||
|
candidate = f"user-{normalized_username}"
|
||||||
|
if is_valid_namespace_name(candidate):
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
safe_username = _INVALID_DNS_LABEL_CHARS.sub("-", normalized_username)
|
||||||
|
safe_username = re.sub(r"-+", "-", safe_username).strip("-") or "user"
|
||||||
|
digest = hashlib.sha256(normalized_username.encode("utf-8")).hexdigest()[:8]
|
||||||
|
suffix = f"-{digest}"
|
||||||
|
max_base_length = 63 - len("user-") - len(suffix)
|
||||||
|
safe_username = safe_username[:max_base_length].rstrip("-") or "user"
|
||||||
|
return f"user-{safe_username}{suffix}"
|
||||||
+2
-1
@@ -6,6 +6,7 @@ from flask import Blueprint, jsonify, request
|
|||||||
import json
|
import json
|
||||||
from urllib.parse import urlsplit
|
from urllib.parse import urlsplit
|
||||||
from kubernetes_service import k8s_manager
|
from kubernetes_service import k8s_manager
|
||||||
|
from namespace_utils import user_namespace
|
||||||
import requests
|
import requests
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
@@ -379,7 +380,7 @@ def download_plugin_to_server():
|
|||||||
plugin_response.close()
|
plugin_response.close()
|
||||||
|
|
||||||
# Upload to server
|
# Upload to server
|
||||||
namespace = f"user-{user['username']}".lower()
|
namespace = user_namespace(user['username'])
|
||||||
pods = k8s_manager.get_pods_for_server(namespace, server_id)
|
pods = k8s_manager.get_pods_for_server(namespace, server_id)
|
||||||
|
|
||||||
if not pods:
|
if not pods:
|
||||||
|
|||||||
+6
-5
@@ -4,6 +4,7 @@
|
|||||||
from database_service import database_connection
|
from database_service import database_connection
|
||||||
from kubernetes import client
|
from kubernetes import client
|
||||||
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
|
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
|
||||||
|
from namespace_utils import user_namespace
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
@@ -59,7 +60,7 @@ def delete_server(server_id: str, user_id: str) -> bool:
|
|||||||
pass
|
pass
|
||||||
return True
|
return True
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
custom_objects_api.delete_namespaced_custom_object(
|
custom_objects_api.delete_namespaced_custom_object(
|
||||||
@@ -165,7 +166,7 @@ def start_server(server_id: str, user_id: str) -> bool:
|
|||||||
update_server_status(server_id, 'Running')
|
update_server_status(server_id, 'Running')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
patch = {'spec': {'runStrategy': 'Always'}}
|
patch = {'spec': {'runStrategy': 'Always'}}
|
||||||
@@ -232,7 +233,7 @@ def stop_server(server_id: str, user_id: str) -> bool:
|
|||||||
update_server_status(server_id, 'Stopped')
|
update_server_status(server_id, 'Stopped')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
patch = {'spec': {'runStrategy': 'Halted'}}
|
patch = {'spec': {'runStrategy': 'Halted'}}
|
||||||
@@ -317,7 +318,7 @@ def restart_server(server_id: str, user_id: str, skip_checks: bool = False) -> b
|
|||||||
update_server_status(server_id, 'Running')
|
update_server_status(server_id, 'Running')
|
||||||
return True
|
return True
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
update_server_status(server_id, 'Restarting')
|
update_server_status(server_id, 'Restarting')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -721,7 +722,7 @@ def adjust_server_parameters(server_id: str, user_id: str, new_cpu: str, new_ram
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
# Update server resources in Kubernetes
|
# Update server resources in Kubernetes
|
||||||
namespace = f"user-{user['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(user['username'])
|
||||||
if k8s_manager.connected:
|
if k8s_manager.connected:
|
||||||
if not update_server_resources_in_k8s(server, new_cpu, new_ram, new_storage, namespace):
|
if not update_server_resources_in_k8s(server, new_cpu, new_ram, new_storage, namespace):
|
||||||
logger.warning(f"Failed to update server resources in K8s, but database was updated")
|
logger.warning(f"Failed to update server resources in K8s, but database was updated")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone
|
|||||||
import json
|
import json
|
||||||
from kubernetes import client
|
from kubernetes import client
|
||||||
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
|
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
|
||||||
|
from namespace_utils import user_namespace
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
@@ -75,7 +76,7 @@ def change_server_version(server_id: str, user_id: str, new_version: str, java_v
|
|||||||
logger.info(f"✓ Changed version for server {server_id} to {new_version} (billing date unchanged)")
|
logger.info(f"✓ Changed version for server {server_id} to {new_version} (billing date unchanged)")
|
||||||
return cursor.rowcount > 0
|
return cursor.rowcount > 0
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
|
|
||||||
if not k8s_manager.connected:
|
if not k8s_manager.connected:
|
||||||
# If K8s not connected, just update database
|
# If K8s not connected, just update database
|
||||||
@@ -368,7 +369,7 @@ def reinstall_server(server_id: str, user_id: str) -> bool:
|
|||||||
if not owner:
|
if not owner:
|
||||||
raise Exception("Server owner not found")
|
raise Exception("Server owner not found")
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
|
|
||||||
if not k8s_manager.connected:
|
if not k8s_manager.connected:
|
||||||
update_server_status(server_id, 'Running')
|
update_server_status(server_id, 'Running')
|
||||||
@@ -1949,7 +1950,7 @@ def create_server(config: Dict, user_id: str, progress_callback=None) -> Dict:
|
|||||||
'ip': NODE_IP,
|
'ip': NODE_IP,
|
||||||
'ports': ports,
|
'ports': ports,
|
||||||
'ownerId': user_id,
|
'ownerId': user_id,
|
||||||
'namespace': f"user-{user['username']}".lower(),
|
'namespace': user_namespace(user['username']),
|
||||||
'k8s_name': server_id,
|
'k8s_name': server_id,
|
||||||
'is_demo': is_demo,
|
'is_demo': is_demo,
|
||||||
'demo_expires_at': demo_expires_at,
|
'demo_expires_at': demo_expires_at,
|
||||||
@@ -1964,7 +1965,7 @@ def create_server(config: Dict, user_id: str, progress_callback=None) -> Dict:
|
|||||||
server = create_server_record(server_data)
|
server = create_server_record(server_data)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
namespace = f"user-{user['username']}".lower()
|
namespace = user_namespace(user['username'])
|
||||||
sanitized_name = config['name'].lower().replace(' ', '-').replace('_', '-')[:20]
|
sanitized_name = config['name'].lower().replace(' ', '-').replace('_', '-')[:20]
|
||||||
server_id = f"{sanitized_name}-{str(uuid.uuid4())[:8]}"
|
server_id = f"{sanitized_name}-{str(uuid.uuid4())[:8]}"
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from database_service import database_connection, dict_from_row
|
from database_service import database_connection, dict_from_row
|
||||||
|
from namespace_utils import user_namespace
|
||||||
import json
|
import json
|
||||||
from runtime_config import (
|
from runtime_config import (
|
||||||
logger,
|
logger,
|
||||||
@@ -116,7 +117,7 @@ def get_server_access_and_namespace(server_id: str, requester_id: str) -> Tuple[
|
|||||||
if not owner:
|
if not owner:
|
||||||
raise Exception("Server owner not found")
|
raise Exception("Server owner not found")
|
||||||
|
|
||||||
namespace = f"user-{owner['username']}".lower()
|
namespace = server.get('namespace') or user_namespace(owner['username'])
|
||||||
return namespace, server
|
return namespace, server
|
||||||
|
|
||||||
def get_user_servers(user_id: str) -> List[Dict]:
|
def get_user_servers(user_id: str) -> List[Dict]:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from kubernetes import client
|
|||||||
from kubernetes.client.rest import ApiException
|
from kubernetes.client.rest import ApiException
|
||||||
from kubernetes.stream import stream
|
from kubernetes.stream import stream
|
||||||
from kubernetes_service import custom_objects_api, k8s_manager, v1
|
from kubernetes_service import custom_objects_api, k8s_manager, v1
|
||||||
|
from namespace_utils import user_namespace
|
||||||
import os
|
import os
|
||||||
import paramiko
|
import paramiko
|
||||||
import pyotp
|
import pyotp
|
||||||
@@ -1263,7 +1264,7 @@ async def handle_register(client_id, data):
|
|||||||
|
|
||||||
if k8s_manager.connected:
|
if k8s_manager.connected:
|
||||||
try:
|
try:
|
||||||
k8s_manager.create_namespace(f"user-{username}".lower())
|
k8s_manager.create_namespace(user_namespace(username))
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -2051,7 +2052,7 @@ async def handle_upload_file(client_id, data):
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
|
|
||||||
namespace = f"user-{user['username']}".lower()
|
namespace = user_namespace(user['username'])
|
||||||
pods = k8s_manager.get_pods_for_server(namespace, server_id)
|
pods = k8s_manager.get_pods_for_server(namespace, server_id)
|
||||||
|
|
||||||
if not pods:
|
if not pods:
|
||||||
@@ -3776,7 +3777,7 @@ async def handle_delete_custom_domain(client_id, data):
|
|||||||
|
|
||||||
# If domain was linked to a server, delete IngressRoute
|
# If domain was linked to a server, delete IngressRoute
|
||||||
if domain.get('server_id'):
|
if domain.get('server_id'):
|
||||||
namespace = f"user-{username}".lower()
|
namespace = user_namespace(username)
|
||||||
# Extract subdomain for IngressRoute name
|
# Extract subdomain for IngressRoute name
|
||||||
domain_name = domain['domain']
|
domain_name = domain['domain']
|
||||||
subdomain = domain_name.split('.')[0] if '.' in domain_name else domain_name
|
subdomain = domain_name.split('.')[0] if '.' in domain_name else domain_name
|
||||||
|
|||||||
Reference in new issue
Block a user