diff --git a/.github/actions/e2e-ready/action.yml b/.github/actions/e2e-ready/action.yml index 502e561c..3c75f388 100644 --- a/.github/actions/e2e-ready/action.yml +++ b/.github/actions/e2e-ready/action.yml @@ -26,6 +26,49 @@ runs: /home/runner/.kube/config > backend/kubeconfig.yaml chmod 644 backend/kubeconfig.yaml + - name: Install Kueue + shell: bash + run: | + KUEUE_VERSION="${KUEUE_VERSION:-v0.16.1}" + KUEUE_MANIFEST_SHA256="${KUEUE_MANIFEST_SHA256:-3201a66ff731be440ecfcf3c0fa5979d001b834f68389208fe7ee18017fbcfe8}" + KUEUE_MANIFEST="/tmp/kueue-manifests.yaml" + curl -fsSL -o "$KUEUE_MANIFEST" "https://github.com/kubernetes-sigs/kueue/releases/download/${KUEUE_VERSION}/manifests.yaml" + echo "${KUEUE_MANIFEST_SHA256} ${KUEUE_MANIFEST}" | sha256sum -c - + kubectl apply --server-side -f "$KUEUE_MANIFEST" + rm -f "$KUEUE_MANIFEST" + kubectl wait --for=condition=Available --timeout=120s \ + deployment/kueue-controller-manager -n kueue-system + kubectl apply --server-side -f - <<'EOF' + apiVersion: kueue.x-k8s.io/v1beta1 + kind: ResourceFlavor + metadata: + name: default-flavor + --- + apiVersion: kueue.x-k8s.io/v1beta1 + kind: ClusterQueue + metadata: + name: executor-queue + spec: + namespaceSelector: {} + resourceGroups: + - coveredResources: ["cpu", "memory"] + flavors: + - name: default-flavor + resources: + - name: cpu + nominalQuota: "32" + - name: memory + nominalQuota: "4Gi" + --- + apiVersion: kueue.x-k8s.io/v1beta1 + kind: LocalQueue + metadata: + name: executor-queue + namespace: integr8scode + spec: + clusterQueue: executor-queue + EOF + - name: Use test environment config shell: bash run: | diff --git a/.github/workflows/release-deploy.yml b/.github/workflows/release-deploy.yml index a72a3e07..a1d213ba 100644 --- a/.github/workflows/release-deploy.yml +++ b/.github/workflows/release-deploy.yml @@ -129,11 +129,14 @@ jobs: MAILJET_FROM_ADDRESS: ${{ secrets.MAILJET_FROM_ADDRESS }} MAILJET_HOST: ${{ secrets.MAILJET_HOST }} GRAFANA_ALERT_RECIPIENTS: ${{ secrets.GRAFANA_ALERT_RECIPIENTS }} + REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }} + MONGO_ROOT_USER: ${{ secrets.MONGO_ROOT_USER }} + MONGO_ROOT_PASSWORD: ${{ secrets.MONGO_ROOT_PASSWORD }} with: host: ${{ secrets.DEPLOY_HOST }} username: ${{ secrets.DEPLOY_USER }} key: ${{ secrets.DEPLOY_SSH_KEY }} - envs: GHCR_TOKEN,GHCR_USER,IMAGE_TAG,GRAFANA_ADMIN_USER,GRAFANA_ADMIN_PASSWORD,MAILJET_API_KEY,MAILJET_SECRET_KEY,MAILJET_FROM_ADDRESS,MAILJET_HOST,GRAFANA_ALERT_RECIPIENTS + envs: GHCR_TOKEN,GHCR_USER,IMAGE_TAG,GRAFANA_ADMIN_USER,GRAFANA_ADMIN_PASSWORD,MAILJET_API_KEY,MAILJET_SECRET_KEY,MAILJET_FROM_ADDRESS,MAILJET_HOST,GRAFANA_ALERT_RECIPIENTS,REDIS_PASSWORD,MONGO_ROOT_USER,MONGO_ROOT_PASSWORD command_timeout: 10m script: | set -e @@ -153,6 +156,9 @@ jobs: export MAILJET_FROM_ADDRESS="$MAILJET_FROM_ADDRESS" export GF_SMTP_HOST="$MAILJET_HOST" export GRAFANA_ALERT_RECIPIENTS="$GRAFANA_ALERT_RECIPIENTS" + export REDIS_PASSWORD="$REDIS_PASSWORD" + export MONGO_ROOT_USER="$MONGO_ROOT_USER" + export MONGO_ROOT_PASSWORD="$MONGO_ROOT_PASSWORD" docker compose pull docker compose up -d --remove-orphans --no-build --wait --wait-timeout 180 diff --git a/.github/workflows/stack-tests.yml b/.github/workflows/stack-tests.yml index e2e2c713..d023820b 100644 --- a/.github/workflows/stack-tests.yml +++ b/.github/workflows/stack-tests.yml @@ -31,6 +31,8 @@ env: KAFKA_IMAGE: confluentinc/cp-kafka:7.8.2 K3S_VERSION: v1.32.11+k3s1 K3S_INSTALL_SHA256: d75e014f2d2ab5d30a318efa5c326f3b0b7596f194afcff90fa7a7a91166d5f7 + KUEUE_VERSION: v0.16.1 + KUEUE_MANIFEST_SHA256: 3201a66ff731be440ecfcf3c0fa5979d001b834f68389208fe7ee18017fbcfe8 jobs: # Fast unit tests (no infrastructure needed) diff --git a/backend/app/core/security.py b/backend/app/core/security.py index 93e5c4dc..6a70321d 100644 --- a/backend/app/core/security.py +++ b/backend/app/core/security.py @@ -6,7 +6,8 @@ import jwt from fastapi import Request from fastapi.security import OAuth2PasswordBearer -from passlib.context import CryptContext +from pwdlib import PasswordHash +from pwdlib.hashers.bcrypt import BcryptHasher from app.core.metrics import SecurityMetrics from app.domain.user import CSRFValidationError, InvalidCredentialsError @@ -20,17 +21,15 @@ def __init__(self, settings: Settings, security_metrics: SecurityMetrics) -> Non self.settings = settings self._security_metrics = security_metrics # --8<-- [start:password_hashing] - self.pwd_context = CryptContext( - schemes=["bcrypt"], - deprecated="auto", - bcrypt__rounds=self.settings.BCRYPT_ROUNDS, - ) + self._password_hash = PasswordHash(( + BcryptHasher(rounds=self.settings.BCRYPT_ROUNDS), + )) def verify_password(self, plain_password: str, hashed_password: str) -> bool: - return self.pwd_context.verify(plain_password, hashed_password) # type: ignore + return self._password_hash.verify(plain_password, hashed_password) def get_password_hash(self, password: str) -> str: - return self.pwd_context.hash(password) # type: ignore + return self._password_hash.hash(password) # --8<-- [end:password_hashing] # --8<-- [start:create_access_token] diff --git a/backend/app/services/k8s_worker/pod_builder.py b/backend/app/services/k8s_worker/pod_builder.py index 1579fdee..5c0ee4ea 100644 --- a/backend/app/services/k8s_worker/pod_builder.py +++ b/backend/app/services/k8s_worker/pod_builder.py @@ -106,6 +106,8 @@ def _build_pod_spec( containers=[container], restart_policy="Never", active_deadline_seconds=timeout, + runtime_class_name=self._settings.K8S_POD_RUNTIME_CLASS_NAME, + host_users=False, # User namespace isolation — remaps container UIDs to unprivileged host UIDs volumes=[ k8s_client.V1Volume( name="script-volume", @@ -155,6 +157,7 @@ def _build_pod_metadata( ) -> k8s_client.V1ObjectMeta: """Build pod metadata with saga tracking""" labels = {"app": "integr8s", "component": "executor", "execution-id": execution_id, "language": language} + labels["kueue.x-k8s.io/queue-name"] = "executor-queue" labels["user-id"] = user_id[:63] # K8s label value limit diff --git a/backend/app/services/k8s_worker/worker.py b/backend/app/services/k8s_worker/worker.py index 14fbff23..8b56ab56 100644 --- a/backend/app/services/k8s_worker/worker.py +++ b/backend/app/services/k8s_worker/worker.py @@ -1,7 +1,6 @@ import asyncio import time from pathlib import Path -from typing import Any import structlog from kubernetes_asyncio import client as k8s_client @@ -56,6 +55,7 @@ def __init__( # Kubernetes clients created from ApiClient self.v1 = k8s_client.CoreV1Api(api_client) self.apps_v1 = k8s_client.AppsV1Api(api_client) + self.networking_v1 = k8s_client.NetworkingV1Api(api_client) # Components self.pod_builder = PodBuilder(settings=settings) @@ -63,7 +63,6 @@ def __init__( # State tracking self._active_creations: set[str] = set() - self._creation_semaphore = asyncio.Semaphore(self._settings.K8S_MAX_CONCURRENT_PODS) self.logger.info(f"KubernetesWorker initialized for namespace {self._settings.K8S_NAMESPACE}") @@ -104,52 +103,51 @@ async def handle_delete_pod_command(self, command: DeletePodCommandEvent) -> Non async def _create_pod_for_execution(self, command: CreatePodCommandEvent) -> None: """Create pod for execution""" - async with self._creation_semaphore: - execution_id = command.execution_id - self._active_creations.add(execution_id) - self.metrics.update_active_pod_creations(len(self._active_creations)) + execution_id = command.execution_id + self._active_creations.add(execution_id) + self.metrics.update_active_pod_creations(len(self._active_creations)) - start_time = time.time() + start_time = time.time() - try: - script_content = command.script - entrypoint_content = await self._get_entrypoint_script() + try: + script_content = command.script + entrypoint_content = await self._get_entrypoint_script() - # Create ConfigMap - config_map = self.pod_builder.build_config_map( - command=command, script_content=script_content, entrypoint_content=entrypoint_content - ) + # Create ConfigMap + config_map = self.pod_builder.build_config_map( + command=command, script_content=script_content, entrypoint_content=entrypoint_content + ) - await self._create_config_map(config_map) + await self._create_config_map(config_map) - pod = self.pod_builder.build_pod_manifest(command=command) - created_pod = await self._create_pod(pod) + pod = self.pod_builder.build_pod_manifest(command=command) + created_pod = await self._create_pod(pod) - # Set ownerReference so K8s garbage-collects the ConfigMap when the pod is deleted - if created_pod and created_pod.metadata and created_pod.metadata.uid: - await self._set_configmap_owner(config_map, created_pod) + # Set ownerReference so K8s garbage-collects the ConfigMap when the pod is deleted + if created_pod and created_pod.metadata and created_pod.metadata.uid: + await self._set_configmap_owner(config_map, created_pod) - # Publish PodCreated event - await self._publish_pod_created(command, pod) + # Publish PodCreated event + await self._publish_pod_created(command, pod) - # Update metrics - duration = time.time() - start_time - self.metrics.record_k8s_pod_creation_duration(duration, command.language) - self.metrics.record_k8s_pod_created("success", command.language) + # Update metrics + duration = time.time() - start_time + self.metrics.record_k8s_pod_creation_duration(duration, command.language) + self.metrics.record_k8s_pod_created("success", command.language) - self.logger.info( - f"Successfully created pod {pod.metadata.name} for execution {execution_id}. " - f"Duration: {duration:.2f}s" - ) + self.logger.info( + f"Successfully created pod {pod.metadata.name} for execution {execution_id}. " + f"Duration: {duration:.2f}s" + ) - except Exception as e: - self.logger.error(f"Failed to create pod for execution {execution_id}: {e}", exc_info=True) - self.metrics.record_k8s_pod_created("failed", "unknown") - await self._publish_pod_creation_failed(command, str(e)) + except Exception as e: + self.logger.error(f"Failed to create pod for execution {execution_id}: {e}", exc_info=True) + self.metrics.record_k8s_pod_created("failed", "unknown") + await self._publish_pod_creation_failed(command, str(e)) - finally: - self._active_creations.discard(execution_id) - self.metrics.update_active_pod_creations(len(self._active_creations)) + finally: + self._active_creations.discard(execution_id) + self.metrics.update_active_pod_creations(len(self._active_creations)) async def _get_entrypoint_script(self) -> str: """Get entrypoint script content""" @@ -252,62 +250,123 @@ async def _publish_pod_creation_failed(self, command: CreatePodCommandEvent, err ) await self.producer.produce(event_to_produce=event, key=command.execution_id) + async def ensure_namespace_security(self) -> None: + """Apply security controls to the executor namespace at startup. + + Creates: + - Default-deny NetworkPolicy for executor pods (blocks lateral movement and exfiltration) + - Pod Security Admission labels (Restricted profile) + """ + namespace = self._settings.K8S_NAMESPACE + await self._ensure_executor_network_policy(namespace) + await self._apply_psa_labels(namespace) + + async def _ensure_executor_network_policy(self, namespace: str) -> None: + """Create or update default-deny NetworkPolicy for executor pods.""" + policy_name = "executor-deny-all" + + policy = k8s_client.V1NetworkPolicy( + api_version="networking.k8s.io/v1", + kind="NetworkPolicy", + metadata=k8s_client.V1ObjectMeta( + name=policy_name, + namespace=namespace, + labels={"app": "integr8s", "component": "security"}, + ), + spec=k8s_client.V1NetworkPolicySpec( + pod_selector=k8s_client.V1LabelSelector(match_labels={"component": "executor"}), + policy_types=["Ingress", "Egress"], + ingress=[], + egress=[], + ), + ) + + await self.networking_v1.patch_namespaced_network_policy( # type: ignore[call-arg] + name=policy_name, namespace=namespace, body=policy, + field_manager="integr8s", force=True, + _content_type="application/apply-patch+yaml", + ) + self.logger.info(f"NetworkPolicy '{policy_name}' applied in namespace {namespace}") + + async def _apply_psa_labels(self, namespace: str) -> None: + """Apply Pod Security Admission labels to the executor namespace.""" + psa_labels = { + "pod-security.kubernetes.io/enforce": "restricted", + "pod-security.kubernetes.io/enforce-version": "latest", + "pod-security.kubernetes.io/warn": "restricted", + "pod-security.kubernetes.io/audit": "restricted", + } + + await self.v1.patch_namespace(name=namespace, body={"metadata": {"labels": psa_labels}}) + self.logger.info(f"Pod Security Admission labels applied to namespace {namespace}") + async def ensure_image_pre_puller_daemonset(self) -> None: """Ensure the runtime image pre-puller DaemonSet exists.""" daemonset_name = "runtime-image-pre-puller" namespace = self._settings.K8S_NAMESPACE try: - init_containers = [] + init_containers: list[k8s_client.V1Container] = [] all_images = {config.image for lang in RUNTIME_REGISTRY.values() for config in lang.values()} + psa_security_context = k8s_client.V1SecurityContext( + allow_privilege_escalation=False, + capabilities=k8s_client.V1Capabilities(drop=["ALL"]), + run_as_non_root=True, + run_as_user=65534, + seccomp_profile=k8s_client.V1SeccompProfile(type="RuntimeDefault"), + ) + + minimal_resources = k8s_client.V1ResourceRequirements( + requests={"cpu": "10m", "memory": "8Mi"}, + limits={"cpu": "100m", "memory": "32Mi"}, + ) + for i, image_ref in enumerate(sorted(all_images)): sanitized_image_ref = image_ref.split("/")[-1].replace(":", "-").replace(".", "-").replace("_", "-") self.logger.info(f"DAEMONSET: before: {image_ref} -> {sanitized_image_ref}") - container_name = f"pull-{i}-{sanitized_image_ref}" - init_containers.append( - { - "name": container_name, - "image": image_ref, - "command": ["/bin/sh", "-c", f'echo "Image {image_ref} pulled."'], - "imagePullPolicy": "Always", - } - ) - - manifest: dict[str, Any] = { - "apiVersion": "apps/v1", - "kind": "DaemonSet", - "metadata": {"name": daemonset_name, "namespace": namespace}, - "spec": { - "selector": {"matchLabels": {"name": daemonset_name}}, - "template": { - "metadata": {"labels": {"name": daemonset_name}}, - "spec": { - "initContainers": init_containers, - "containers": [{"name": "pause", "image": "registry.k8s.io/pause:3.9"}], - "tolerations": [{"operator": "Exists"}], - }, - }, - "updateStrategy": {"type": "RollingUpdate"}, - }, - } - - try: - await self.apps_v1.read_namespaced_daemon_set(name=daemonset_name, namespace=namespace) - self.logger.info(f"DaemonSet '{daemonset_name}' exists. Replacing to ensure it is up-to-date.") - await self.apps_v1.replace_namespaced_daemon_set( - name=daemonset_name, namespace=namespace, body=manifest # type: ignore[arg-type] - ) - self.logger.info(f"DaemonSet '{daemonset_name}' replaced successfully.") - except ApiException as e: - if e.status == 404: - self.logger.info(f"DaemonSet '{daemonset_name}' not found. Creating...") - await self.apps_v1.create_namespaced_daemon_set( - namespace=namespace, body=manifest # type: ignore[arg-type] - ) - self.logger.info(f"DaemonSet '{daemonset_name}' created successfully.") - else: - raise + init_containers.append(k8s_client.V1Container( + name=f"pull-{i}-{sanitized_image_ref}", + image=image_ref, + command=["/bin/sh", "-c", f'echo "Image {image_ref} pulled."'], + image_pull_policy="Always", + security_context=psa_security_context, + resources=minimal_resources, + )) + + daemonset = k8s_client.V1DaemonSet( + api_version="apps/v1", + kind="DaemonSet", + metadata=k8s_client.V1ObjectMeta(name=daemonset_name, namespace=namespace), + spec=k8s_client.V1DaemonSetSpec( + selector=k8s_client.V1LabelSelector(match_labels={"name": daemonset_name}), + template=k8s_client.V1PodTemplateSpec( + metadata=k8s_client.V1ObjectMeta(labels={"name": daemonset_name}), + spec=k8s_client.V1PodSpec( + init_containers=init_containers, + containers=[k8s_client.V1Container( + name="pause", image="registry.k8s.io/pause:3.9", + security_context=psa_security_context, + resources=minimal_resources, + )], + tolerations=[k8s_client.V1Toleration(operator="Exists")], + security_context=k8s_client.V1PodSecurityContext( + run_as_non_root=True, + run_as_user=65534, + seccomp_profile=k8s_client.V1SeccompProfile(type="RuntimeDefault"), + ), + ), + ), + update_strategy=k8s_client.V1DaemonSetUpdateStrategy(type="RollingUpdate"), + ), + ) + + await self.apps_v1.patch_namespaced_daemon_set( # type: ignore[call-arg] + name=daemonset_name, namespace=namespace, body=daemonset, + field_manager="integr8s", force=True, + _content_type="application/apply-patch+yaml", + ) + self.logger.info(f"DaemonSet '{daemonset_name}' applied successfully") except ApiException as e: self.logger.error(f"K8s API error applying DaemonSet '{daemonset_name}': {e.reason}", exc_info=True) diff --git a/backend/app/services/pod_monitor/config.py b/backend/app/services/pod_monitor/config.py index 33c7d497..25a900b9 100644 --- a/backend/app/services/pod_monitor/config.py +++ b/backend/app/services/pod_monitor/config.py @@ -15,7 +15,7 @@ class PodMonitorConfig: # Watch settings label_selector: str = "app=integr8s,component=executor" field_selector: str | None = None - watch_timeout_seconds: int = 300 # 5 minutes + watch_timeout_seconds: int = 30 # 30 seconds — short enough for APScheduler 5s interval # Monitoring settings enable_metrics: bool = True diff --git a/backend/app/services/pod_monitor/monitor.py b/backend/app/services/pod_monitor/monitor.py index 2542b92c..93881d7b 100644 --- a/backend/app/services/pod_monitor/monitor.py +++ b/backend/app/services/pod_monitor/monitor.py @@ -6,14 +6,22 @@ import structlog from kubernetes_asyncio import client as k8s_client from kubernetes_asyncio import watch as k8s_watch +from kubernetes_asyncio.client.rest import ApiException from app.core.metrics import KubernetesMetrics from app.core.utils import StringEnum +from app.domain.enums import EventType from app.domain.events import DomainEvent from app.services.kafka_event_service import KafkaEventService from app.services.pod_monitor.config import PodMonitorConfig from app.services.pod_monitor.event_mapper import PodEventMapper, WatchEventType +_TERMINAL_EVENT_TYPES: frozenset[str] = frozenset({ + EventType.EXECUTION_COMPLETED, + EventType.EXECUTION_FAILED, + EventType.EXECUTION_TIMEOUT, +}) + # Type aliases type ResourceVersion = str type KubeEvent = dict[str, Any] @@ -175,10 +183,12 @@ async def _process_pod_event(self, event: PodEvent) -> None: # Map to application events app_events = await self._event_mapper.map_pod_event(event.pod, event.event_type) - # Publish events for app_event in app_events: await self._publish_event(app_event, event.pod) + if any(e.event_type in _TERMINAL_EVENT_TYPES for e in app_events): + await self._delete_pod(event.pod) + if app_events: self.logger.info( f"Processed {event.event_type} event for pod {pod_name} " @@ -195,14 +205,28 @@ async def _process_pod_event(self, event: PodEvent) -> None: async def _publish_event(self, event: DomainEvent, pod: k8s_client.V1Pod) -> None: """Publish event to Kafka and store in events collection.""" - try: - execution_id = getattr(event, "execution_id", None) or event.aggregate_id - key = str(execution_id or (pod.metadata.name if pod.metadata else "unknown")) + execution_id = getattr(event, "execution_id", None) or event.aggregate_id + key = str(execution_id or (pod.metadata.name if pod.metadata else "unknown")) - await self._kafka_event_service.publish_event(event=event, key=key) + await self._kafka_event_service.publish_event(event=event, key=key) - phase = pod.status.phase if pod.status else "Unknown" - self._metrics.record_pod_monitor_event_published(event.event_type, phase) + phase = pod.status.phase if pod.status else "Unknown" + self._metrics.record_pod_monitor_event_published(event.event_type, phase) - except Exception as e: - self.logger.error(f"Error publishing event: {e}", exc_info=True) + async def _delete_pod(self, pod: k8s_client.V1Pod) -> None: + """Delete a pod after its data has been fully extracted. + + Frees the Kueue quota slot so gated executor pods can be admitted. + The ConfigMap is garbage-collected automatically via ownerReference. + """ + pod_name = pod.metadata.name + try: + await self._v1.delete_namespaced_pod( + name=pod_name, namespace=pod.metadata.namespace, grace_period_seconds=0, + ) + self.logger.info(f"Deleted completed pod {pod_name}") + except ApiException as e: + if e.status == 404: + self.logger.debug(f"Pod {pod_name} already deleted") + else: + self.logger.warning(f"Failed to delete pod {pod_name}: {e.reason}") diff --git a/backend/app/services/runtime_settings.py b/backend/app/services/runtime_settings.py index eb342389..7d577b5d 100644 --- a/backend/app/services/runtime_settings.py +++ b/backend/app/services/runtime_settings.py @@ -51,6 +51,5 @@ def _build_toml_defaults(self) -> SystemSettings: max_timeout_seconds=s.K8S_POD_EXECUTION_TIMEOUT, memory_limit=s.K8S_POD_MEMORY_LIMIT, cpu_limit=s.K8S_POD_CPU_LIMIT, - max_concurrent_executions=s.K8S_MAX_CONCURRENT_PODS, session_timeout_minutes=s.ACCESS_TOKEN_EXPIRE_MINUTES, ) diff --git a/backend/app/settings.py b/backend/app/settings.py index 61dbcaca..abdc1846 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -68,9 +68,6 @@ def __init__( # Kubernetes namespace for execution pods K8S_NAMESPACE: str = "integr8scode" - # Maximum concurrent pod creations allowed by k8s worker - K8S_MAX_CONCURRENT_PODS: int = 10 - # Settings for Kubernetes resource limits and requests K8S_POD_CPU_LIMIT: str = "1000m" K8S_POD_MEMORY_LIMIT: str = "128Mi" @@ -78,6 +75,7 @@ def __init__( K8S_POD_MEMORY_REQUEST: str = "128Mi" K8S_POD_EXECUTION_TIMEOUT: int = 300 # in seconds K8S_POD_PRIORITY_CLASS_NAME: str | None = None + K8S_POD_RUNTIME_CLASS_NAME: str | None = None # e.g. "gvisor" for sandboxed execution SUPPORTED_RUNTIMES: dict[str, LanguageInfoDomain] = Field(default_factory=lambda: RUNTIME_MATRIX) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 8b646ab3..8576fed5 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -70,7 +70,7 @@ dependencies = [ "opentelemetry-util-http==0.60b1", "packaging==24.1", "beanie==2.0.1", - "passlib==1.7.4", + "pwdlib[bcrypt]==0.2.1", "pathspec==0.12.1", "prometheus-fastapi-instrumentator==7.0.0", "prometheus_client==0.21.0", diff --git a/backend/scripts/seed_users.py b/backend/scripts/seed_users.py index e4f5d6d1..f3e9349d 100755 --- a/backend/scripts/seed_users.py +++ b/backend/scripts/seed_users.py @@ -18,15 +18,15 @@ from app.settings import Settings from bson import ObjectId -from passlib.context import CryptContext +from pwdlib import PasswordHash +from pwdlib.hashers.bcrypt import BcryptHasher from pymongo.asynchronous.database import AsyncDatabase from pymongo.asynchronous.mongo_client import AsyncMongoClient -pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - async def upsert_user( db: AsyncDatabase[dict[str, Any]], + pwd_hasher: PasswordHash, username: str, email: str, password: str, @@ -42,7 +42,7 @@ async def upsert_user( {"username": username}, { "$set": { - "hashed_password": pwd_context.hash(password), + "hashed_password": pwd_hasher.hash(password), "role": role, "is_superuser": is_superuser, "is_active": True, @@ -58,7 +58,7 @@ async def upsert_user( "user_id": str(ObjectId()), "username": username, "email": email, - "hashed_password": pwd_context.hash(password), + "hashed_password": pwd_hasher.hash(password), "role": role, "is_active": True, "is_superuser": is_superuser, @@ -70,6 +70,7 @@ async def upsert_user( async def seed_users(settings: Settings) -> None: """Seed default users using provided settings for MongoDB connection.""" + pwd_hasher = PasswordHash((BcryptHasher(rounds=settings.BCRYPT_ROUNDS),)) default_password = os.environ.get("DEFAULT_USER_PASSWORD", "user123") admin_password = os.environ.get("ADMIN_USER_PASSWORD", "admin123") @@ -80,6 +81,7 @@ async def seed_users(settings: Settings) -> None: # Default user await upsert_user( db, + pwd_hasher, username="user", email="user@integr8scode.com", password=default_password, @@ -90,6 +92,7 @@ async def seed_users(settings: Settings) -> None: # Admin user await upsert_user( db, + pwd_hasher, username="admin", email="admin@integr8scode.com", password=admin_password, diff --git a/backend/secrets.example.toml b/backend/secrets.example.toml index 0f7b41ef..c12eb784 100644 --- a/backend/secrets.example.toml +++ b/backend/secrets.example.toml @@ -15,3 +15,4 @@ SECRET_KEY = "CHANGE_ME_min_32_chars_long_!!!!" MONGODB_URL = "mongodb://root:rootpassword@mongo:27017/integr8scode?authSource=admin" +REDIS_PASSWORD = "redispassword" diff --git a/backend/tests/e2e/test_admin_settings_routes.py b/backend/tests/e2e/test_admin_settings_routes.py index b5d19295..f4114bdb 100644 --- a/backend/tests/e2e/test_admin_settings_routes.py +++ b/backend/tests/e2e/test_admin_settings_routes.py @@ -172,7 +172,6 @@ async def test_reset_system_settings( max_timeout_seconds=test_settings.K8S_POD_EXECUTION_TIMEOUT, memory_limit=test_settings.K8S_POD_MEMORY_LIMIT, cpu_limit=test_settings.K8S_POD_CPU_LIMIT, - max_concurrent_executions=test_settings.K8S_MAX_CONCURRENT_PODS, session_timeout_minutes=test_settings.ACCESS_TOKEN_EXPIRE_MINUTES, ) assert settings == expected diff --git a/backend/tests/unit/services/pod_monitor/test_monitor.py b/backend/tests/unit/services/pod_monitor/test_monitor.py index 06d950a3..583fb8a5 100644 --- a/backend/tests/unit/services/pod_monitor/test_monitor.py +++ b/backend/tests/unit/services/pod_monitor/test_monitor.py @@ -403,5 +403,6 @@ async def produce( pod = make_pod(name="no-meta-pod", phase="Pending") pod.metadata = None # type: ignore[assignment] - # Should not raise - errors are caught and logged - await pm._publish_event(event, pod) + # Exception propagates — _process_pod_event's broad except handles it + with pytest.raises(RuntimeError, match="Publish failed"): + await pm._publish_event(event, pod) diff --git a/backend/tests/unit/services/test_runtime_settings.py b/backend/tests/unit/services/test_runtime_settings.py index 62b3bcb6..fcf78b5d 100644 --- a/backend/tests/unit/services/test_runtime_settings.py +++ b/backend/tests/unit/services/test_runtime_settings.py @@ -19,7 +19,6 @@ def _make_settings() -> Settings: "K8S_POD_EXECUTION_TIMEOUT": 30, "K8S_POD_MEMORY_LIMIT": "128Mi", "K8S_POD_CPU_LIMIT": "1000m", - "K8S_MAX_CONCURRENT_PODS": 5, "ACCESS_TOKEN_EXPIRE_MINUTES": 60, }) @@ -58,7 +57,7 @@ async def test_passes_toml_defaults_to_repo() -> None: assert defaults.max_timeout_seconds == 30 assert defaults.memory_limit == "128Mi" assert defaults.cpu_limit == "1000m" - assert defaults.max_concurrent_executions == 5 + assert defaults.max_concurrent_executions == 10 assert defaults.session_timeout_minutes == 60 diff --git a/backend/uv.lock b/backend/uv.lock index 6bd53ded..f8c8f62a 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -253,6 +253,56 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, ] +[[package]] +name = "bcrypt" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" }, + { url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" }, + { url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" }, + { url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" }, + { url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" }, + { url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" }, + { url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" }, + { url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" }, + { url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" }, + { url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" }, + { url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" }, + { url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" }, + { url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" }, + { url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" }, + { url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" }, + { url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" }, + { url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" }, + { url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" }, + { url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" }, + { url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" }, + { url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" }, + { url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" }, + { url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" }, +] + [[package]] name = "beanie" version = "2.0.1" @@ -1257,13 +1307,13 @@ dependencies = [ { name = "opentelemetry-semantic-conventions" }, { name = "opentelemetry-util-http" }, { name = "packaging" }, - { name = "passlib" }, { name = "pathspec" }, { name = "prometheus-client" }, { name = "prometheus-fastapi-instrumentator" }, { name = "propcache" }, { name = "protobuf" }, { name = "psutil" }, + { name = "pwdlib", extra = ["bcrypt"] }, { name = "pyasn1" }, { name = "pyasn1-modules" }, { name = "pydantic" }, @@ -1417,13 +1467,13 @@ requires-dist = [ { name = "opentelemetry-semantic-conventions", specifier = "==0.60b1" }, { name = "opentelemetry-util-http", specifier = "==0.60b1" }, { name = "packaging", specifier = "==24.1" }, - { name = "passlib", specifier = "==1.7.4" }, { name = "pathspec", specifier = "==0.12.1" }, { name = "prometheus-client", specifier = "==0.21.0" }, { name = "prometheus-fastapi-instrumentator", specifier = "==7.0.0" }, { name = "propcache", specifier = "==0.4.1" }, { name = "protobuf", specifier = "==6.33.5" }, { name = "psutil", specifier = "==7.2.2" }, + { name = "pwdlib", extras = ["bcrypt"], specifier = "==0.2.1" }, { name = "pyasn1", specifier = "==0.6.2" }, { name = "pyasn1-modules", specifier = "==0.4.2" }, { name = "pydantic", specifier = "==2.9.2" }, @@ -2321,15 +2371,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/08/aa/cc0199a5f0ad350994d660967a8efb233fe0416e4639146c089643407ce6/packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124", size = 53985, upload-time = "2024-06-09T23:19:21.909Z" }, ] -[[package]] -name = "passlib" -version = "1.7.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" }, -] - [[package]] name = "pathspec" version = "0.12.1" @@ -2588,6 +2629,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, ] +[[package]] +name = "pwdlib" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/a0/9daed437a6226f632a25d98d65d60ba02bdafa920c90dcb6454c611ead6c/pwdlib-0.2.1.tar.gz", hash = "sha256:9a1d8a8fa09a2f7ebf208265e55d7d008103cbdc82b9e4902ffdd1ade91add5e", size = 11699, upload-time = "2024-08-19T06:48:59.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/f3/0dae5078a486f0fdf4d4a1121e103bc42694a9da9bea7b0f2c63f29cfbd3/pwdlib-0.2.1-py3-none-any.whl", hash = "sha256:1823dc6f22eae472b540e889ecf57fd424051d6a4023ec0bcf7f0de2d9d7ef8c", size = 8082, upload-time = "2024-08-19T06:49:00.997Z" }, +] + +[package.optional-dependencies] +bcrypt = [ + { name = "bcrypt" }, +] + [[package]] name = "pyasn1" version = "0.6.2" diff --git a/backend/workers/run_k8s_worker.py b/backend/workers/run_k8s_worker.py index b69a84c9..4e47d5b6 100644 --- a/backend/workers/run_k8s_worker.py +++ b/backend/workers/run_k8s_worker.py @@ -44,8 +44,9 @@ async def run() -> None: async def init_k8s_worker() -> None: worker = await container.get(KubernetesWorker) + await worker.ensure_namespace_security() await worker.ensure_image_pre_puller_daemonset() - logger.info("KubernetesWorker initialized with pre-puller daemonset") + logger.info("KubernetesWorker initialized with namespace security and pre-puller daemonset") app = FastStream(broker, on_startup=[init_k8s_worker], on_shutdown=[container.close]) await app.run() diff --git a/cert-generator/setup-k8s.sh b/cert-generator/setup-k8s.sh index cee3dcb0..19dbe9fd 100644 --- a/cert-generator/setup-k8s.sh +++ b/cert-generator/setup-k8s.sh @@ -89,6 +89,51 @@ echo "Connected to Kubernetes" # Create namespace kubectl create namespace integr8scode --dry-run=client -o yaml | kubectl apply -f - +# Install Kueue (scheduling-gate based quota management) +KUEUE_VERSION="${KUEUE_VERSION:-v0.16.1}" +KUEUE_MANIFEST_SHA256="${KUEUE_MANIFEST_SHA256:-3201a66ff731be440ecfcf3c0fa5979d001b834f68389208fe7ee18017fbcfe8}" +echo "Installing Kueue ${KUEUE_VERSION}..." +KUEUE_MANIFEST="$(mktemp /tmp/kueue-manifests.XXXXXXXXXX)" +curl -fsSL -o "$KUEUE_MANIFEST" "https://github.com/kubernetes-sigs/kueue/releases/download/${KUEUE_VERSION}/manifests.yaml" +echo "${KUEUE_MANIFEST_SHA256} ${KUEUE_MANIFEST}" | sha256sum -c - +kubectl apply --server-side -f "$KUEUE_MANIFEST" +rm -f "$KUEUE_MANIFEST" +kubectl wait --for=condition=Available --timeout=120s \ + deployment/kueue-controller-manager -n kueue-system + +# Kueue resources: ResourceFlavor + ClusterQueue + LocalQueue +kubectl apply --server-side -f - <<'KUEUE_EOF' +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ResourceFlavor +metadata: + name: default-flavor +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: ClusterQueue +metadata: + name: executor-queue +spec: + namespaceSelector: {} + resourceGroups: + - coveredResources: ["cpu", "memory"] + flavors: + - name: default-flavor + resources: + - name: cpu + nominalQuota: "32" + - name: memory + nominalQuota: "4Gi" +--- +apiVersion: kueue.x-k8s.io/v1beta1 +kind: LocalQueue +metadata: + name: executor-queue + namespace: integr8scode +spec: + clusterQueue: executor-queue +KUEUE_EOF +echo "Kueue installed and configured" + # Create ServiceAccount kubectl apply -f - <` tags (XSS). Nonce is per-request, generated by nginx. +- **`style-src-elem 'nonce-...'`** — blocks injected ` - + -
-
-
- +
+
+ + Loading…
- diff --git a/frontend/src/app.css b/frontend/src/app.css index cacacc20..98a0b3df 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -579,4 +579,45 @@ @apply px-2 py-1 text-sm border border-border-input dark:border-dark-border-input rounded bg-surface-overlay dark:bg-dark-surface-overlay text-fg-default dark:text-dark-fg-default; } + + /* ANSI color classes for ansi_up (class-based mode) */ + .ansi-black-fg { color: #000; } + .ansi-red-fg { color: #C00; } + .ansi-green-fg { color: #0C0; } + .ansi-yellow-fg { color: #C50; } + .ansi-blue-fg { color: #00C; } + .ansi-magenta-fg { color: #C0C; } + .ansi-cyan-fg { color: #0CC; } + .ansi-white-fg { color: #CCC; } + .ansi-bright-black-fg { color: #555; } + .ansi-bright-red-fg { color: #F55; } + .ansi-bright-green-fg { color: #5F5; } + .ansi-bright-yellow-fg { color: #FF5; } + .ansi-bright-blue-fg { color: #55F; } + .ansi-bright-magenta-fg { color: #F5F; } + .ansi-bright-cyan-fg { color: #5FF; } + .ansi-bright-white-fg { color: #FFF; } + .ansi-black-bg { background-color: #000; } + .ansi-red-bg { background-color: #C00; } + .ansi-green-bg { background-color: #0C0; } + .ansi-yellow-bg { background-color: #C50; } + .ansi-blue-bg { background-color: #00C; } + .ansi-magenta-bg { background-color: #C0C; } + .ansi-cyan-bg { background-color: #0CC; } + .ansi-white-bg { background-color: #CCC; } + .ansi-bright-black-bg { background-color: #555; } + .ansi-bright-red-bg { background-color: #F55; } + .ansi-bright-green-bg { background-color: #5F5; } + .ansi-bright-yellow-bg { background-color: #FF5; } + .ansi-bright-blue-bg { background-color: #55F; } + .ansi-bright-magenta-bg { background-color: #F5F; } + .ansi-bright-cyan-bg { background-color: #5FF; } + .ansi-bright-white-bg { background-color: #FFF; } + .ansi-bold { font-weight: bold; } + .ansi-italic { font-style: italic; } + .ansi-underline { text-decoration: underline; } + + :is(.dark) .ansi-black-fg { color: #555; } + :is(.dark) .ansi-white-fg { color: #e2e8f0; } + :is(.dark) .ansi-bright-white-fg { color: #FFF; } } diff --git a/frontend/src/components/editor/CodeMirrorEditor.svelte b/frontend/src/components/editor/CodeMirrorEditor.svelte index 41c2538d..53f8421e 100644 --- a/frontend/src/components/editor/CodeMirrorEditor.svelte +++ b/frontend/src/components/editor/CodeMirrorEditor.svelte @@ -23,6 +23,10 @@ let container: HTMLElement; let view: EditorView | null = null; + function getCspNonce(): string { + return document.querySelector('meta[name="csp-nonce"]')?.getAttribute('content') ?? ''; + } + const themeCompartment = new Compartment(); const fontSizeCompartment = new Compartment(); const tabSizeCompartment = new Compartment(); @@ -36,6 +40,7 @@ function getStaticExtensions() { return [ + EditorView.cspNonce.of(getCspNonce()), lineNumbersCompartment.of(settings.show_line_numbers ? lineNumbers() : []), highlightActiveLineGutter(), highlightActiveLine(), diff --git a/frontend/src/components/editor/OutputPanel.svelte b/frontend/src/components/editor/OutputPanel.svelte index b8fe9794..c89ab718 100644 --- a/frontend/src/components/editor/OutputPanel.svelte +++ b/frontend/src/components/editor/OutputPanel.svelte @@ -3,7 +3,7 @@ import type { ExecutionPhase } from '$lib/editor'; import Spinner from '$components/Spinner.svelte'; import { AlertTriangle, FileText, Copy } from '@lucide/svelte'; - import AnsiToHtml from 'ansi-to-html'; + import { AnsiUp } from 'ansi_up'; import DOMPurify from 'dompurify'; import { toast } from 'svelte-sonner'; @@ -13,18 +13,14 @@ error: string | null; } = $props(); - const ansiConverter = new AnsiToHtml({ - fg: '#000', bg: '#FFF', newline: true, escapeXML: true, stream: false, - colors: { - 0: '#000', 1: '#C00', 2: '#0C0', 3: '#C50', 4: '#00C', 5: '#C0C', 6: '#0CC', 7: '#CCC', - 8: '#555', 9: '#F55', 10: '#5F5', 11: '#FF5', 12: '#55F', 13: '#F5F', 14: '#5FF', 15: '#FFF' - } - }); + const ansiConverter = new AnsiUp(); + ansiConverter.use_classes = true; + ansiConverter.escape_html = true; function sanitize(html: string): string { return DOMPurify.sanitize(html, { ALLOWED_TAGS: ['span', 'br', 'div'], - ALLOWED_ATTR: ['class', 'style'] + ALLOWED_ATTR: ['class'] }); } @@ -99,7 +95,7 @@

Output:

-
{@html sanitize(ansiConverter.toHtml(result.stdout || ''))}
+
{@html sanitize(ansiConverter.ansi_to_html(result.stdout))}