From de2042fa93af9e6b56caa3eb07599fcba5fff95d Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:49:13 +0900 Subject: [PATCH 01/18] =?UTF-8?q?[Fix]=20Internal=20server=20error=20type?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/exceptions/error_types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/exceptions/error_types.py b/app/exceptions/error_types.py index 5410f7b..d7f28eb 100644 --- a/app/exceptions/error_types.py +++ b/app/exceptions/error_types.py @@ -52,3 +52,4 @@ class ApiErrorTypes(str, Enum): INTERNAL_ERROR = "INTERNAL_ERROR" # 내부 서버 에러 DATABASE_ERROR = "DATABASE_ERROR" # 데이터베이스 에러 EXTERNAL_SERVICE_ERROR = "EXTERNAL_SERVICE_ERROR" # 외부 서비스 에러 + INTERNAL_SERVER_ERROR = "INTERNAL_SERVER_ERROR" # 서버 내부 에러 From 52bbfabe75643627df04274d6309ae8033c3dc14 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:56:11 +0900 Subject: [PATCH 02/18] [Fix] update-port Lock --- app/extensions/db/repository.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index a5e0176..0b07762 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -91,7 +91,8 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: bool: 업데이트 성공 여부 """ try: - challenge.port = port + locked_challenge = self.session.query(UserChallenges).filter_by(idx=challenge.idx).with_for_update().one() + locked_challenge.port = port self.session.commit() return True except SQLAlchemyError as e: From 3ea516e875f9551eb00540a93451d7002b0727aa Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 11:58:27 +0900 Subject: [PATCH 03/18] =?UTF-8?q?[Feat]=20/status=20request=20body=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/challenge.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/api/challenge.py b/app/api/challenge.py index 12b024a..76bb47e 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -69,20 +69,22 @@ def get_userchallenge_status(): # Challenge 관련 정보 가져오기 res = request.get_json() if not res: - raise UserChallengeDeletionError(error_msg="Request body is empty or not valid JSON") + raise InvalidRequest(error_msg="Request body is empty or not valid JSON") if 'challenge_id' not in res: raise InvalidRequest(error_msg="Required field 'challenge_id' is missing in request") + challenge_id = res['challenge_id'] + if not challenge_id: + raise InvalidRequest(error_msg="'challenge_id' is empty or not valid") if 'username' not in res: raise InvalidRequest(error_msg="Required field 'username' is missing in request") - username = res['username'] if not username: - raise InvalidRequest(error_msg="Required field 'username' is missing in request") - + raise InvalidRequest(error_msg="'username' is empty or not valid") + # 사용자 챌린지 상태 조회 repo = UserChallengesRepository() status = repo.get_status(challenge_id, username) From 1e8b8fae803d8e797e77461dc5390d254774192b Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:27:03 +0900 Subject: [PATCH 04/18] =?UTF-8?q?[Feat]=20/status=20exception=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/challenge.py b/app/api/challenge.py index 76bb47e..3c674ed 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -92,4 +92,4 @@ def get_userchallenge_status(): raise UserChallengeNotFoundError(error_msg=f"User challenge not found for {username} and {challenge_id}") return jsonify({'data': {'status': status}}), 200 except Exception as e: - raise UserChallengeNotFoundError(error_msg=str(e)) from e \ No newline at end of file + raise UserChallengeNotFoundError(error_msg=str(e)) \ No newline at end of file From 286fa99ca9ed11274bd4bb56446306fb9c28b14c Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:33:35 +0900 Subject: [PATCH 05/18] =?UTF-8?q?[Feat]=20update=20port=20=EB=8F=99?= =?UTF-8?q?=EC=8B=9C=EC=84=B1=20=EC=A0=9C=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 34 +++++++++++++++++++++++++-------- app/extensions/k8s/client.py | 13 +++++++------ 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 0b07762..4ab15eb 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -1,5 +1,7 @@ import logging +from sqlite3 import OperationalError +import time from typing import List, Optional from sqlalchemy.exc import SQLAlchemyError from app.exceptions.api import InternalServerError @@ -90,15 +92,31 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: Returns: bool: 업데이트 성공 여부 """ - try: - locked_challenge = self.session.query(UserChallenges).filter_by(idx=challenge.idx).with_for_update().one() - locked_challenge.port = port - self.session.commit() - return True - except SQLAlchemyError as e: - self.session.rollback() - raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e + max_retries = 3 # 최대 3번 재시도 + for attempt in range(max_retries): + try: + with self.session.begin_nested(): # 명확한 트랜잭션 관리 + locked_challenge = ( + self.session.query(UserChallenges) + .filter_by(idx=challenge.idx) + .with_for_update(nowait=True) # 다른 트랜잭션이 잠근 경우 즉시 실패 + .one() + ) + locked_challenge.port = port + self.session.commit() + return True # 업데이트 성공 시 반환 + + except OperationalError as e: # 트랜잭션 충돌 발생 시 + self.session.rollback() + if attempt < max_retries - 1: + time.sleep(2) # 다음 시도를 위해 2초 대기 + else: + raise InternalServerError(error_msg=f"Error updating challenge port after {max_retries} attempts: {e}") from e + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e + def is_running(self, challenge: UserChallenges) -> bool: """ 챌린지 실행 여부 확인 diff --git a/app/extensions/k8s/client.py b/app/extensions/k8s/client.py index 87a6612..3ffffc7 100644 --- a/app/extensions/k8s/client.py +++ b/app/extensions/k8s/client.py @@ -110,8 +110,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") status = challenge.get('status', {}) endpoint = status.get('endpoint') - # status가 아직 설정되지 않았을 수 있으므로, 필요한 경우 다시 조회 - if not status: + max_retries = 5 + for _ in range(max_retries): time.sleep(3) challenge = self.custom_api.get_namespaced_custom_object( group="apps.hexactf.io", @@ -122,11 +122,12 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") ) status = challenge.get('status', {}) endpoint = status.get('endpoint') - - # NodePort 업데이트 - if not endpoint: + + if endpoint: + break + else: raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") - + success = user_challenge_repo.update_port(user_challenge, int(endpoint)) if not success: raise UserChallengeCreationError(error_msg=f"Failed to update UserChallenge with NodePort: {endpoint}") From b3f5915004d4f704f3bce32fb7b5e621882f4ebb Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:38:18 +0900 Subject: [PATCH 06/18] =?UTF-8?q?[Feat]=20update=20port=20=ED=8A=B8?= =?UTF-8?q?=EB=9E=9C=EC=9E=AD=EC=85=98=20track=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 35 +++++++++------------------------ 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 4ab15eb..71e9e67 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -1,7 +1,5 @@ import logging -from sqlite3 import OperationalError -import time from typing import List, Optional from sqlalchemy.exc import SQLAlchemyError from app.exceptions.api import InternalServerError @@ -92,31 +90,16 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: Returns: bool: 업데이트 성공 여부 """ - max_retries = 3 # 최대 3번 재시도 - for attempt in range(max_retries): - try: - with self.session.begin_nested(): # 명확한 트랜잭션 관리 - locked_challenge = ( - self.session.query(UserChallenges) - .filter_by(idx=challenge.idx) - .with_for_update(nowait=True) # 다른 트랜잭션이 잠근 경우 즉시 실패 - .one() - ) - locked_challenge.port = port - self.session.commit() - return True # 업데이트 성공 시 반환 - - except OperationalError as e: # 트랜잭션 충돌 발생 시 - self.session.rollback() - if attempt < max_retries - 1: - time.sleep(2) # 다음 시도를 위해 2초 대기 - else: - raise InternalServerError(error_msg=f"Error updating challenge port after {max_retries} attempts: {e}") from e + try: + challenge.port = port + self.session.add(challenge) + self.session.flush() + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - except SQLAlchemyError as e: - self.session.rollback() - raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - def is_running(self, challenge: UserChallenges) -> bool: """ 챌린지 실행 여부 확인 From 46231419a82e185e274196bc190572e36472d449 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:45:22 +0900 Subject: [PATCH 07/18] =?UTF-8?q?[Feat]=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/k8s/client.py | 168 +++++++++++++++++++++++++++++------ 1 file changed, 141 insertions(+), 27 deletions(-) diff --git a/app/extensions/k8s/client.py b/app/extensions/k8s/client.py index 3ffffc7..5aab279 100644 --- a/app/extensions/k8s/client.py +++ b/app/extensions/k8s/client.py @@ -1,6 +1,7 @@ import os import re import time +import sys from kubernetes import client, config @@ -31,27 +32,25 @@ def __init__(self): self.custom_api = client.CustomObjectsApi() self.core_api = client.CoreV1Api() - def create_challenge_resource(self, challenge_id, username, namespace="default") -> int: """ Challenge Custom Resource를 생성하고 NodePort를 반환합니다. - + Args: challenge_id (str): 생성할 Challenge의 ID username (str): Challenge를 생성하는 사용자 이름 namespace (str): Challenge를 생성할 네임스페이스 (기본값: "default") - + Returns: int: 할당된 NodePort 번호 - + Raises: ChallengeNotFound: Challenge ID에 해당하는 Challenge가 없을 때 ChallengeCreationError: Challenge Custom Resource 생성에 실패했을 때 - """ - + user_challenge_repo = UserChallengesRepository() - + # Challenge definition 조회 challenge_definition = ChallengeRepository.get_challenge_name(challenge_id) if not challenge_definition: @@ -68,8 +67,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") self.core_api.read_namespace(namespace) except Exception as e: raise UserChallengeCreationError(error_msg=str(e)) - - # Database에 UserChallenge 생성 + + # Database에 UserChallenge 생성 user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_name) if not user_challenge: user_challenge = user_challenge_repo.create(username, challenge_id, challenge_name, 0) @@ -77,11 +76,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") # 이미 실행 중인 Challenge가 있으면 데이터베이스에 저장된 포트 번호 반환 if user_challenge.status == 'Running': return user_challenge.port - - # 공백의 경우 하이픈으로 변환 - challenge_definition = self._normalize_k8s_name(challenge_definition) - valid_username = self._normalize_k8s_name(username) - # Challenge manifest 생성 + + # Kubernetes Challenge Custom Resource 생성 challenge_manifest = { "apiVersion": "apps.hexactf.io/v1alpha1", "kind": "Challenge", @@ -89,7 +85,7 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") "name": challenge_name, "labels": { "apps.hexactf.io/challengeId": str(challenge_id), - "apps.hexactf.io/user": valid_username + "apps.hexactf.io/user": username } }, "spec": { @@ -97,8 +93,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") "definition": challenge_definition } } - - challenge = self.custom_api.create_namespaced_custom_object( + + self.custom_api.create_namespaced_custom_object( group="apps.hexactf.io", version="v1alpha1", namespace=namespace, @@ -106,33 +102,151 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") body=challenge_manifest ) - # status 값 가져오기 - status = challenge.get('status', {}) - endpoint = status.get('endpoint') + # `status.endpoint`가 업데이트될 때까지 재시도 + max_retries = 10 # 재시도 횟수를 증가 + endpoint = None - max_retries = 5 - for _ in range(max_retries): - time.sleep(3) + for i in range(max_retries): + time.sleep(3) # 첫 번째 요청이 실패하는 경우, 충분한 대기 시간을 설정 challenge = self.custom_api.get_namespaced_custom_object( group="apps.hexactf.io", version="v1alpha1", namespace=namespace, plural="challenges", - name=challenge['metadata']['name'] + name=challenge_name ) status = challenge.get('status', {}) endpoint = status.get('endpoint') if endpoint: + logging.info(f"Challenge {challenge_name} received endpoint: {endpoint}") break - else: + else: + logging.warning(f"Retry {i + 1}/{max_retries}: Waiting for Challenge {challenge_name} to get an endpoint...") + + if not endpoint: raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") - success = user_challenge_repo.update_port(user_challenge, int(endpoint)) + # NodePort 업데이트 (트랜잭션 충돌 방지) + success = False + retry_count = 3 + + for i in range(retry_count): + try: + success = user_challenge_repo.update_port(user_challenge, int(endpoint)) + if success: + break + except Exception as e: + print(f"Failed to update UserChallenge with NodePort: {endpoint}", file=sys.stderr) + time.sleep(2) # DB 충돌이 발생할 경우 재시도 + if not success: raise UserChallengeCreationError(error_msg=f"Failed to update UserChallenge with NodePort: {endpoint}") - + return endpoint + + + # def create_challenge_resource(self, challenge_id, username, namespace="default") -> int: + # """ + # Challenge Custom Resource를 생성하고 NodePort를 반환합니다. + + # Args: + # challenge_id (str): 생성할 Challenge의 ID + # username (str): Challenge를 생성하는 사용자 이름 + # namespace (str): Challenge를 생성할 네임스페이스 (기본값: "default") + + # Returns: + # int: 할당된 NodePort 번호 + + # Raises: + # ChallengeNotFound: Challenge ID에 해당하는 Challenge가 없을 때 + # ChallengeCreationError: Challenge Custom Resource 생성에 실패했을 때 + + # """ + + # user_challenge_repo = UserChallengesRepository() + + # # Challenge definition 조회 + # challenge_definition = ChallengeRepository.get_challenge_name(challenge_id) + # if not challenge_definition: + # raise ChallengeNotFound(error_msg=f"Challenge definition not found for ID: {challenge_id}") + + # # Challenge name 생성 및 검증 + # challenge_name = f"challenge-{challenge_id}-{username.lower()}" + # challenge_name = self._normalize_k8s_name(challenge_name) + # if not self._is_valid_k8s_name(challenge_name): + # raise UserChallengeCreationError(error_msg=f"Invalid challenge name: {challenge_name}") + + # # Namespace 존재 여부 확인 + # try: + # self.core_api.read_namespace(namespace) + # except Exception as e: + # raise UserChallengeCreationError(error_msg=str(e)) + + # # Database에 UserChallenge 생성 + # user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_name) + # if not user_challenge: + # user_challenge = user_challenge_repo.create(username, challenge_id, challenge_name, 0) + # else: + # # 이미 실행 중인 Challenge가 있으면 데이터베이스에 저장된 포트 번호 반환 + # if user_challenge.status == 'Running': + # return user_challenge.port + + # # 공백의 경우 하이픈으로 변환 + # challenge_definition = self._normalize_k8s_name(challenge_definition) + # valid_username = self._normalize_k8s_name(username) + # # Challenge manifest 생성 + # challenge_manifest = { + # "apiVersion": "apps.hexactf.io/v1alpha1", + # "kind": "Challenge", + # "metadata": { + # "name": challenge_name, + # "labels": { + # "apps.hexactf.io/challengeId": str(challenge_id), + # "apps.hexactf.io/user": valid_username + # } + # }, + # "spec": { + # "namespace": namespace, + # "definition": challenge_definition + # } + # } + + # challenge = self.custom_api.create_namespaced_custom_object( + # group="apps.hexactf.io", + # version="v1alpha1", + # namespace=namespace, + # plural="challenges", + # body=challenge_manifest + # ) + + # # status 값 가져오기 + # status = challenge.get('status', {}) + # endpoint = status.get('endpoint') + + # max_retries = 5 + # for _ in range(max_retries): + # time.sleep(3) + # challenge = self.custom_api.get_namespaced_custom_object( + # group="apps.hexactf.io", + # version="v1alpha1", + # namespace=namespace, + # plural="challenges", + # name=challenge['metadata']['name'] + # ) + # status = challenge.get('status', {}) + # endpoint = status.get('endpoint') + + # if endpoint: + # break + # else: + # raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") + + # success = user_challenge_repo.update_port(user_challenge, int(endpoint)) + # if not success: + # raise UserChallengeCreationError(error_msg=f"Failed to update UserChallenge with NodePort: {endpoint}") + + # return endpoint def _normalize_k8s_name(self, name: str) -> str: From 813aacba960520a598bc1c4a910fa58a0af2412c Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:47:38 +0900 Subject: [PATCH 08/18] =?UTF-8?q?[Feat]=20=EC=9E=AC=EC=8B=9C=EB=8F=84=20?= =?UTF-8?q?=ED=9A=9F=EC=88=98=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/k8s/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/k8s/client.py b/app/extensions/k8s/client.py index 5aab279..3fdbb2d 100644 --- a/app/extensions/k8s/client.py +++ b/app/extensions/k8s/client.py @@ -119,10 +119,10 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") endpoint = status.get('endpoint') if endpoint: - logging.info(f"Challenge {challenge_name} received endpoint: {endpoint}") + print(f"Challenge {challenge_name} received endpoint: {endpoint}", file=sys.stderr) break else: - logging.warning(f"Retry {i + 1}/{max_retries}: Waiting for Challenge {challenge_name} to get an endpoint...") + print(f"Retry {i + 1}/{max_retries}: Waiting for Challenge {challenge_name} to get an endpoint...", file=sys.stderr) if not endpoint: raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") From 813d3f18481efa9ee3a1b5463569def1905df9a8 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 13:54:57 +0900 Subject: [PATCH 09/18] =?UTF-8?q?[Feat]=20sleep=205=EC=B4=88=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/k8s/client.py | 174 ++++++----------------------------- 1 file changed, 30 insertions(+), 144 deletions(-) diff --git a/app/extensions/k8s/client.py b/app/extensions/k8s/client.py index 3fdbb2d..0eeed02 100644 --- a/app/extensions/k8s/client.py +++ b/app/extensions/k8s/client.py @@ -1,7 +1,6 @@ import os import re import time -import sys from kubernetes import client, config @@ -32,25 +31,27 @@ def __init__(self): self.custom_api = client.CustomObjectsApi() self.core_api = client.CoreV1Api() + def create_challenge_resource(self, challenge_id, username, namespace="default") -> int: """ Challenge Custom Resource를 생성하고 NodePort를 반환합니다. - + Args: challenge_id (str): 생성할 Challenge의 ID username (str): Challenge를 생성하는 사용자 이름 namespace (str): Challenge를 생성할 네임스페이스 (기본값: "default") - + Returns: int: 할당된 NodePort 번호 - + Raises: ChallengeNotFound: Challenge ID에 해당하는 Challenge가 없을 때 ChallengeCreationError: Challenge Custom Resource 생성에 실패했을 때 - """ + """ + user_challenge_repo = UserChallengesRepository() - + # Challenge definition 조회 challenge_definition = ChallengeRepository.get_challenge_name(challenge_id) if not challenge_definition: @@ -67,8 +68,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") self.core_api.read_namespace(namespace) except Exception as e: raise UserChallengeCreationError(error_msg=str(e)) - - # Database에 UserChallenge 생성 + + # Database에 UserChallenge 생성 user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_name) if not user_challenge: user_challenge = user_challenge_repo.create(username, challenge_id, challenge_name, 0) @@ -76,8 +77,11 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") # 이미 실행 중인 Challenge가 있으면 데이터베이스에 저장된 포트 번호 반환 if user_challenge.status == 'Running': return user_challenge.port - - # Kubernetes Challenge Custom Resource 생성 + + # 공백의 경우 하이픈으로 변환 + challenge_definition = self._normalize_k8s_name(challenge_definition) + valid_username = self._normalize_k8s_name(username) + # Challenge manifest 생성 challenge_manifest = { "apiVersion": "apps.hexactf.io/v1alpha1", "kind": "Challenge", @@ -85,7 +89,7 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") "name": challenge_name, "labels": { "apps.hexactf.io/challengeId": str(challenge_id), - "apps.hexactf.io/user": username + "apps.hexactf.io/user": valid_username } }, "spec": { @@ -93,8 +97,8 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") "definition": challenge_definition } } - - self.custom_api.create_namespaced_custom_object( + + challenge = self.custom_api.create_namespaced_custom_object( group="apps.hexactf.io", version="v1alpha1", namespace=namespace, @@ -102,151 +106,33 @@ def create_challenge_resource(self, challenge_id, username, namespace="default") body=challenge_manifest ) - # `status.endpoint`가 업데이트될 때까지 재시도 - max_retries = 10 # 재시도 횟수를 증가 - endpoint = None + time.sleep(5) + # status 값 가져오기 + status = challenge.get('status', {}) + endpoint = status.get('endpoint') - for i in range(max_retries): - time.sleep(3) # 첫 번째 요청이 실패하는 경우, 충분한 대기 시간을 설정 + # status가 아직 설정되지 않았을 수 있으므로, 필요한 경우 다시 조회 + if not status: + time.sleep(3) challenge = self.custom_api.get_namespaced_custom_object( group="apps.hexactf.io", version="v1alpha1", namespace=namespace, plural="challenges", - name=challenge_name + name=challenge['metadata']['name'] ) status = challenge.get('status', {}) endpoint = status.get('endpoint') - - if endpoint: - print(f"Challenge {challenge_name} received endpoint: {endpoint}", file=sys.stderr) - break - else: - print(f"Retry {i + 1}/{max_retries}: Waiting for Challenge {challenge_name} to get an endpoint...", file=sys.stderr) - + + # NodePort 업데이트 if not endpoint: raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") - - # NodePort 업데이트 (트랜잭션 충돌 방지) - success = False - retry_count = 3 - - for i in range(retry_count): - try: - success = user_challenge_repo.update_port(user_challenge, int(endpoint)) - if success: - break - except Exception as e: - print(f"Failed to update UserChallenge with NodePort: {endpoint}", file=sys.stderr) - time.sleep(2) # DB 충돌이 발생할 경우 재시도 - + + success = user_challenge_repo.update_port(user_challenge, int(endpoint)) if not success: raise UserChallengeCreationError(error_msg=f"Failed to update UserChallenge with NodePort: {endpoint}") - - return endpoint - - # def create_challenge_resource(self, challenge_id, username, namespace="default") -> int: - # """ - # Challenge Custom Resource를 생성하고 NodePort를 반환합니다. - - # Args: - # challenge_id (str): 생성할 Challenge의 ID - # username (str): Challenge를 생성하는 사용자 이름 - # namespace (str): Challenge를 생성할 네임스페이스 (기본값: "default") - - # Returns: - # int: 할당된 NodePort 번호 - - # Raises: - # ChallengeNotFound: Challenge ID에 해당하는 Challenge가 없을 때 - # ChallengeCreationError: Challenge Custom Resource 생성에 실패했을 때 - - # """ - - # user_challenge_repo = UserChallengesRepository() - - # # Challenge definition 조회 - # challenge_definition = ChallengeRepository.get_challenge_name(challenge_id) - # if not challenge_definition: - # raise ChallengeNotFound(error_msg=f"Challenge definition not found for ID: {challenge_id}") - - # # Challenge name 생성 및 검증 - # challenge_name = f"challenge-{challenge_id}-{username.lower()}" - # challenge_name = self._normalize_k8s_name(challenge_name) - # if not self._is_valid_k8s_name(challenge_name): - # raise UserChallengeCreationError(error_msg=f"Invalid challenge name: {challenge_name}") - - # # Namespace 존재 여부 확인 - # try: - # self.core_api.read_namespace(namespace) - # except Exception as e: - # raise UserChallengeCreationError(error_msg=str(e)) - - # # Database에 UserChallenge 생성 - # user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_name) - # if not user_challenge: - # user_challenge = user_challenge_repo.create(username, challenge_id, challenge_name, 0) - # else: - # # 이미 실행 중인 Challenge가 있으면 데이터베이스에 저장된 포트 번호 반환 - # if user_challenge.status == 'Running': - # return user_challenge.port - - # # 공백의 경우 하이픈으로 변환 - # challenge_definition = self._normalize_k8s_name(challenge_definition) - # valid_username = self._normalize_k8s_name(username) - # # Challenge manifest 생성 - # challenge_manifest = { - # "apiVersion": "apps.hexactf.io/v1alpha1", - # "kind": "Challenge", - # "metadata": { - # "name": challenge_name, - # "labels": { - # "apps.hexactf.io/challengeId": str(challenge_id), - # "apps.hexactf.io/user": valid_username - # } - # }, - # "spec": { - # "namespace": namespace, - # "definition": challenge_definition - # } - # } - - # challenge = self.custom_api.create_namespaced_custom_object( - # group="apps.hexactf.io", - # version="v1alpha1", - # namespace=namespace, - # plural="challenges", - # body=challenge_manifest - # ) - - # # status 값 가져오기 - # status = challenge.get('status', {}) - # endpoint = status.get('endpoint') - - # max_retries = 5 - # for _ in range(max_retries): - # time.sleep(3) - # challenge = self.custom_api.get_namespaced_custom_object( - # group="apps.hexactf.io", - # version="v1alpha1", - # namespace=namespace, - # plural="challenges", - # name=challenge['metadata']['name'] - # ) - # status = challenge.get('status', {}) - # endpoint = status.get('endpoint') - - # if endpoint: - # break - # else: - # raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_name}") - - # success = user_challenge_repo.update_port(user_challenge, int(endpoint)) - # if not success: - # raise UserChallengeCreationError(error_msg=f"Failed to update UserChallenge with NodePort: {endpoint}") - - # return endpoint + return endpoint def _normalize_k8s_name(self, name: str) -> str: From 487ca69eb57f27a2c4b7b2246da8c2c10b25b05f Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:03:08 +0900 Subject: [PATCH 10/18] =?UTF-8?q?[Feat]=20update=20port=20flush=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/challenge.py | 2 +- app/extensions/db/repository.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/api/challenge.py b/app/api/challenge.py index 3c674ed..c54ef5c 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -30,7 +30,7 @@ def create_challenge(): client = K8sClient() endpoint = client.create_challenge_resource(challenge_id, username) if not endpoint: - raise UserChallengeCreationError(error_msg=f"Failed to create challenge {challenge_id} for user {username}") + raise UserChallengeCreationError(error_msg=f"Failed to create challenge {challenge_id} for user {username} : Endpoint did not exist") return jsonify({'data' : {'port': endpoint}}), 200 diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 71e9e67..5142ddb 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -93,7 +93,6 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: try: challenge.port = port self.session.add(challenge) - self.session.flush() self.session.commit() return True except SQLAlchemyError as e: From 665382864a9e085a80d49a9bda40861c7c2fefd5 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:10:50 +0900 Subject: [PATCH 11/18] [Feat] session close --- app/extensions/db/repository.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 5142ddb..83e29ff 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -40,6 +40,8 @@ def create(self, username: str, C_idx: int, userChallengeName: str, except SQLAlchemyError as e: self.session.rollback() raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e + finally: + self.session.close() def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: """ @@ -78,6 +80,8 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: # logger.error(f"Error updating challenge status: {e}") self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e + finally: + self.session.close() def update_port(self, challenge: UserChallenges, port: int) -> bool: """ @@ -98,6 +102,8 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: except SQLAlchemyError as e: self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e + finally: + self.session.close() def is_running(self, challenge: UserChallenges) -> bool: """ From a79d70b669d491e36495cc4144c0c07074036bef Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:12:40 +0900 Subject: [PATCH 12/18] [Feat] session close --- app/extensions/db/repository.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 83e29ff..3149747 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -38,10 +38,12 @@ def create(self, username: str, C_idx: int, userChallengeName: str, self.session.commit() return challenge except SQLAlchemyError as e: - self.session.rollback() + if self.session: + self.session.rollback() raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e finally: - self.session.close() + if self.session: + self.session.close() def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: """ @@ -78,10 +80,12 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: return True except SQLAlchemyError as e: # logger.error(f"Error updating challenge status: {e}") - self.session.rollback() + if self.session: + self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e finally: - self.session.close() + if self.session: + self.session.close() def update_port(self, challenge: UserChallenges, port: int) -> bool: """ @@ -100,10 +104,12 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: self.session.commit() return True except SQLAlchemyError as e: - self.session.rollback() + if self.session: + self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e finally: - self.session.close() + if self.session: + self.session.close() def is_running(self, challenge: UserChallenges) -> bool: """ From c5445716ddbdad3f46337a8e984ef2d5569ac55e Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:21:35 +0900 Subject: [PATCH 13/18] =?UTF-8?q?[Feat]=20=EC=A4=91=EB=B3=B5=EB=90=9C=20se?= =?UTF-8?q?ssion=20close=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index 3149747..aa7f628 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -38,12 +38,9 @@ def create(self, username: str, C_idx: int, userChallengeName: str, self.session.commit() return challenge except SQLAlchemyError as e: - if self.session: - self.session.rollback() + + self.session.rollback() raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e - finally: - if self.session: - self.session.close() def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: """ @@ -80,12 +77,10 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: return True except SQLAlchemyError as e: # logger.error(f"Error updating challenge status: {e}") - if self.session: - self.session.rollback() + + self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e - finally: - if self.session: - self.session.close() + def update_port(self, challenge: UserChallenges, port: int) -> bool: """ @@ -98,18 +93,15 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: Returns: bool: 업데이트 성공 여부 """ + challenge.port = port try: - challenge.port = port - self.session.add(challenge) + # self.session.add(challenge) self.session.commit() return True except SQLAlchemyError as e: - if self.session: - self.session.rollback() + self.session.rollback() raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - finally: - if self.session: - self.session.close() + def is_running(self, challenge: UserChallenges) -> bool: """ From 7a6d637ada53855c9da4be9f03c2ac4a74c92ad9 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:26:05 +0900 Subject: [PATCH 14/18] =?UTF-8?q?[Feat]=20=EC=A4=91=EB=B3=B5=EB=90=9C=20se?= =?UTF-8?q?ssion=20merge=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index aa7f628..b132299 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -93,9 +93,13 @@ def update_port(self, challenge: UserChallenges, port: int) -> bool: Returns: bool: 업데이트 성공 여부 """ - challenge.port = port try: - # self.session.add(challenge) + # 1) 먼저 challenge 객체를 세션에 맞게 merge + fresh_challenge = self.session.merge(challenge) + + # 2) merge된 객체에 port 갱신 + fresh_challenge.port = port + self.session.commit() return True except SQLAlchemyError as e: From f8b7fb03a5400a09bac05ddba18db80fadcb729d Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:35:10 +0900 Subject: [PATCH 15/18] =?UTF-8?q?[Feat]=20update=20status=20flush=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/extensions/db/repository.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/extensions/db/repository.py b/app/extensions/db/repository.py index b132299..86b0deb 100644 --- a/app/extensions/db/repository.py +++ b/app/extensions/db/repository.py @@ -71,8 +71,8 @@ def update_status(self, challenge: UserChallenges, new_status: str) -> bool: """ try: challenge.status = new_status - self.session.add(challenge) # Add this line to track the object - self.session.flush() + # self.session.add(challenge) # Add this line to track the object + # self.session.flush() self.session.commit() return True except SQLAlchemyError as e: From cce7e4ca899d37c0115469f6c59738e5c0276a47 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:38:43 +0900 Subject: [PATCH 16/18] [Chore] Debug --- app/api/challenge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/challenge.py b/app/api/challenge.py index c54ef5c..1933d7d 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -68,6 +68,7 @@ def get_userchallenge_status(): try: # Challenge 관련 정보 가져오기 res = request.get_json() + print(res,file=sys.stderr) if not res: raise InvalidRequest(error_msg="Request body is empty or not valid JSON") From 61597c9f8c20588af1846f4082f17cdb90dbd118 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:41:36 +0900 Subject: [PATCH 17/18] [Chore] Debug --- app/api/challenge.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/api/challenge.py b/app/api/challenge.py index 1933d7d..e443c65 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -1,5 +1,6 @@ from json import JSONDecodeError from logging import log +import sys from app.monitoring.ctf_metrics_collector import ChallengeMetricsCollector from flask import Blueprint, jsonify, request From b9cfd632024a4fb39d93ca9bb120899bc1ffcf47 Mon Sep 17 00:00:00 2001 From: S0okJu <65109465+D7MeKz@users.noreply.github.com> Date: Mon, 10 Feb 2025 14:45:38 +0900 Subject: [PATCH 18/18] [Chore] Debug --- app/api/challenge.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/challenge.py b/app/api/challenge.py index e443c65..e61d2cc 100644 --- a/app/api/challenge.py +++ b/app/api/challenge.py @@ -69,7 +69,7 @@ def get_userchallenge_status(): try: # Challenge 관련 정보 가져오기 res = request.get_json() - print(res,file=sys.stderr) + print(f"/status : {res}",file=sys.stderr) if not res: raise InvalidRequest(error_msg="Request body is empty or not valid JSON")