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
|
||||
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):
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
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
@@ -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")
|
||||
|
||||
@@ -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
@@ -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]:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user