Fix Kubernetes namespaces for dotted usernames
Build and publish Docker image / build-and-publish (push) Successful in 8s

This commit is contained in:
wfalkowski committed 2026-10-20 19:38:31 +00:00
1 parent 522e5c5634
commit 6732ee9d47
8 files changed
+70 -19

No files matched your search

+4 -3
View File
@@ -7,6 +7,7 @@ from domain_runtime import domain_manager
import json
from kubernetes.client.rest import ApiException
from kubernetes_service import custom_objects_api, k8s_manager
from namespace_utils import user_namespace
import re
from runtime_config import (
NODE_IP,
@@ -325,7 +326,7 @@ def cleanup_orphaned_ingressroutes():
for domain, username in db_domains.items():
if not domain or not username:
continue
namespace = f"user-{username}".lower()
namespace = user_namespace(username)
subdomain = domain.split('.')[0]
ingressroute_name = f"{subdomain}-ingress"
@@ -590,7 +591,7 @@ def link_domain_to_server(domain_id: str, server_id: str) -> bool:
return False
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')
if not domain_host:
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:
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
if not delete_traefik_ingressroute(domain=domain_host, namespace=namespace):
+4 -2
View File
@@ -3,6 +3,7 @@ from datetime import datetime
from kubernetes import client, config
from kubernetes.client.rest import ApiException
from kubernetes.stream import stream
from namespace_utils import is_valid_namespace_name
import os
import random
from runtime_config import (
@@ -62,8 +63,9 @@ class KubernetesManager:
return False
def create_namespace(self, namespace_name: str, labels: Dict = None) -> bool:
# Kubernetes namespaces MUST be lowercase
namespace_name = namespace_name.lower()
if not is_valid_namespace_name(namespace_name):
logger.error("Refusing to create invalid Kubernetes namespace name: %r", namespace_name)
return False
if not self.connected:
logger.info(f"⚠ Kubernetes not connected, skipping namespace creation for {namespace_name}")
+43
View File
@@ -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
View File
@@ -6,6 +6,7 @@ from flask import Blueprint, jsonify, request
import json
from urllib.parse import urlsplit
from kubernetes_service import k8s_manager
from namespace_utils import user_namespace
import requests
from werkzeug.utils import secure_filename
from runtime_config import (
@@ -379,7 +380,7 @@ def download_plugin_to_server():
plugin_response.close()
# Upload to server
namespace = f"user-{user['username']}".lower()
namespace = user_namespace(user['username'])
pods = k8s_manager.get_pods_for_server(namespace, server_id)
if not pods:
+6 -5
View File
@@ -4,6 +4,7 @@
from database_service import database_connection
from kubernetes import client
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
from namespace_utils import user_namespace
from runtime_config import (
logger,
)
@@ -59,7 +60,7 @@ def delete_server(server_id: str, user_id: str) -> bool:
pass
return True
namespace = f"user-{owner['username']}".lower()
namespace = server.get('namespace') or user_namespace(owner['username'])
try:
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')
return True
namespace = f"user-{owner['username']}".lower()
namespace = server.get('namespace') or user_namespace(owner['username'])
try:
patch = {'spec': {'runStrategy': 'Always'}}
@@ -232,7 +233,7 @@ def stop_server(server_id: str, user_id: str) -> bool:
update_server_status(server_id, 'Stopped')
return True
namespace = f"user-{owner['username']}".lower()
namespace = server.get('namespace') or user_namespace(owner['username'])
try:
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')
return True
namespace = f"user-{owner['username']}".lower()
namespace = server.get('namespace') or user_namespace(owner['username'])
update_server_status(server_id, 'Restarting')
try:
@@ -721,7 +722,7 @@ def adjust_server_parameters(server_id: str, user_id: str, new_cpu: str, new_ram
conn.commit()
# 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 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")
+5 -4
View File
@@ -6,6 +6,7 @@ from datetime import datetime, timedelta, timezone
import json
from kubernetes import client
from kubernetes_service import apps_v1, custom_objects_api, k8s_manager, v1
from namespace_utils import user_namespace
import random
import re
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)")
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 K8s not connected, just update database
@@ -368,7 +369,7 @@ def reinstall_server(server_id: str, user_id: str) -> bool:
if not owner:
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:
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,
'ports': ports,
'ownerId': user_id,
'namespace': f"user-{user['username']}".lower(),
'namespace': user_namespace(user['username']),
'k8s_name': server_id,
'is_demo': is_demo,
'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)
else:
namespace = f"user-{user['username']}".lower()
namespace = user_namespace(user['username'])
sanitized_name = config['name'].lower().replace(' ', '-').replace('_', '-')[:20]
server_id = f"{sanitized_name}-{str(uuid.uuid4())[:8]}"
+2 -1
View File
@@ -2,6 +2,7 @@
from database_service import database_connection, dict_from_row
from namespace_utils import user_namespace
import json
from runtime_config import (
logger,
@@ -116,7 +117,7 @@ def get_server_access_and_namespace(server_id: str, requester_id: str) -> Tuple[
if not owner:
raise Exception("Server owner not found")
namespace = f"user-{owner['username']}".lower()
namespace = server.get('namespace') or user_namespace(owner['username'])
return namespace, server
def get_user_servers(user_id: str) -> List[Dict]:
+4 -3
View File
@@ -13,6 +13,7 @@ from kubernetes import client
from kubernetes.client.rest import ApiException
from kubernetes.stream import stream
from kubernetes_service import custom_objects_api, k8s_manager, v1
from namespace_utils import user_namespace
import os
import paramiko
import pyotp
@@ -1263,7 +1264,7 @@ async def handle_register(client_id, data):
if k8s_manager.connected:
try:
k8s_manager.create_namespace(f"user-{username}".lower())
k8s_manager.create_namespace(user_namespace(username))
except Exception:
pass
@@ -2051,7 +2052,7 @@ async def handle_upload_file(client_id, data):
})
return
namespace = f"user-{user['username']}".lower()
namespace = user_namespace(user['username'])
pods = k8s_manager.get_pods_for_server(namespace, server_id)
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.get('server_id'):
namespace = f"user-{username}".lower()
namespace = user_namespace(username)
# Extract subdomain for IngressRoute name
domain_name = domain['domain']
subdomain = domain_name.split('.')[0] if '.' in domain_name else domain_name