diff --git a/README.md b/README.md index 494e63b..11811fe 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,8 @@ HexaCTF의 Container Control 프로젝트의 일부입니다. ## API -| Endpoint | Method | Description | -| :---------------------------------------------------------: | :----: | :-----------------: | -| [/v1/user-challenges](./md/user-challenge.md) | POST | Challenge 생성 | -| [/v1/user-challenges/delete](./md/user-challenge-delete.md) | POST | Challenge 삭제 | -| [/v1/user-challenges/status](./md/user-challenge-status.md) | GET | Challenge 상태 조회 | - - +| Endpoint | Method | Description | +| :-----------------------------------------------------------: | :----: | :-----------------: | +| [/v1/user-challenges](./docs/user-challenge.md) | POST | Challenge 생성 | +| [/v1/user-challenges/delete](./docs/user-challenge-delete.md) | POST | Challenge 삭제 | +| [/v1/user-challenges/status](./docs/user-challenge-status.md) | GET | Challenge 상태 조회 | diff --git a/__pycache__/app.cpython-310.pyc b/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..a8240b7 Binary files /dev/null and b/__pycache__/app.cpython-310.pyc differ diff --git a/app.py b/app.py index 02c8c4a..3cb5d2a 100644 --- a/app.py +++ b/app.py @@ -1,4 +1,4 @@ -from hexactf.factory import create_app +from challenge_api.factory import create_app app = create_app() diff --git a/hexactf/__init__.py b/challenge_api/__init__.py similarity index 100% rename from hexactf/__init__.py rename to challenge_api/__init__.py diff --git a/challenge_api/__pycache__/__init__.cpython-310.pyc b/challenge_api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..15feec9 Binary files /dev/null and b/challenge_api/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/__pycache__/config.cpython-310.pyc b/challenge_api/__pycache__/config.cpython-310.pyc similarity index 80% rename from hexactf/__pycache__/config.cpython-310.pyc rename to challenge_api/__pycache__/config.cpython-310.pyc index 8e154e6..cc0f9f4 100644 Binary files a/hexactf/__pycache__/config.cpython-310.pyc and b/challenge_api/__pycache__/config.cpython-310.pyc differ diff --git a/hexactf/__pycache__/extensions_manager.cpython-310.pyc b/challenge_api/__pycache__/extensions_manager.cpython-310.pyc similarity index 92% rename from hexactf/__pycache__/extensions_manager.cpython-310.pyc rename to challenge_api/__pycache__/extensions_manager.cpython-310.pyc index 5da603f..73d10fc 100644 Binary files a/hexactf/__pycache__/extensions_manager.cpython-310.pyc and b/challenge_api/__pycache__/extensions_manager.cpython-310.pyc differ diff --git a/challenge_api/__pycache__/factory.cpython-310.pyc b/challenge_api/__pycache__/factory.cpython-310.pyc new file mode 100644 index 0000000..b15f40e Binary files /dev/null and b/challenge_api/__pycache__/factory.cpython-310.pyc differ diff --git a/hexactf/api/__init__.py b/challenge_api/api/__init__.py similarity index 100% rename from hexactf/api/__init__.py rename to challenge_api/api/__init__.py diff --git a/challenge_api/api/__pycache__/__init__.cpython-310.pyc b/challenge_api/api/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..ace4f9b Binary files /dev/null and b/challenge_api/api/__pycache__/__init__.cpython-310.pyc differ diff --git a/challenge_api/api/__pycache__/challenge_api.cpython-310.pyc b/challenge_api/api/__pycache__/challenge_api.cpython-310.pyc new file mode 100644 index 0000000..2026121 Binary files /dev/null and b/challenge_api/api/__pycache__/challenge_api.cpython-310.pyc differ diff --git a/challenge_api/api/challenge_api.py b/challenge_api/api/challenge_api.py new file mode 100644 index 0000000..c91d7aa --- /dev/null +++ b/challenge_api/api/challenge_api.py @@ -0,0 +1,63 @@ +from json import JSONDecodeError +from logging import log +from flask import Blueprint, jsonify, request + +from challenge_api.exceptions.api_exceptions import InvalidRequest +from challenge_api.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError, UserChallengeNotFoundError +from challenge_api.db.repository import UserChallengesRepository, UserChallengeStatusRepository +from challenge_api.extensions.k8s.client import K8sClient +from challenge_api.utils.api_decorators import validate_request_body +from challenge_api.objects.challenge_info import ChallengeInfo + +challenge_bp = Blueprint('challenge', __name__) + +@challenge_bp.route('', methods=['POST']) +@validate_request_body('challenge_id', 'user_id') +def create_challenge(): + """사용자 챌린지 생성""" + # Challenge 관련 정보 가져오기 + res = request.get_json() + challenge_info = ChallengeInfo(**res) + + # 챌린지 생성 + client = K8sClient() + endpoint = client.create(data=challenge_info) + if not endpoint: + raise UserChallengeCreationError(error_msg=f"Failed to create challenge {challenge_info.challenge_id} for user {challenge_info.name} : Endpoint did not exist") + + return jsonify({'data' : {'port': endpoint}}), 200 + +@challenge_bp.route('/delete', methods=['POST']) +@validate_request_body('challenge_id', 'user_id') +def delete_userchallenges(): + """사용자 챌린지 삭제""" + try: + res = request.get_json() + challenge_info = ChallengeInfo(**res) + + # 사용자 챌린지 삭제 + client = K8sClient() + client.delete(challenge_info) + + return jsonify({'message' : '챌린지가 정상적으로 삭제되었습니다.'}), 200 + except JSONDecodeError as e: + log.error("Invalid request format") + raise InvalidRequest(error_msg=str(e)) from e + +@challenge_bp.route('/status', methods=['POST']) +@validate_request_body('challenge_id', 'user_id') +def get_userchallenge_status(): + """사용자 챌린지 최근 상태 조회""" + try: + res = request.get_json() + challenge_info = ChallengeInfo(**res) + + # 사용자 챌린지 상태 조회 + userchallenge_repo = UserChallengesRepository() + userchallenge = userchallenge_repo.get_by_user_challenge_name(challenge_info.name) + + repo = UserChallengeStatusRepository() + status = repo.get_recent_status(userchallenge.idx) + return jsonify({'data': {'port': status.port, 'status': status.status}}), 200 + except Exception as e: + raise UserChallengeNotFoundError(error_msg=str(e)) \ No newline at end of file diff --git a/hexactf/config.py b/challenge_api/config.py similarity index 100% rename from hexactf/config.py rename to challenge_api/config.py diff --git a/hexactf/extensions/db/__init__.py b/challenge_api/db/__init__.py similarity index 62% rename from hexactf/extensions/db/__init__.py rename to challenge_api/db/__init__.py index db783c9..d4223a6 100644 --- a/hexactf/extensions/db/__init__.py +++ b/challenge_api/db/__init__.py @@ -3,4 +3,4 @@ __all__ = ['MariaDBConfig'] from flask_sqlalchemy import SQLAlchemy -from hexactf.extensions.db.config import MariaDBConfig +from challenge_api.db.config import MariaDBConfig diff --git a/challenge_api/db/__pycache__/__init__.cpython-310.pyc b/challenge_api/db/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..15c5796 Binary files /dev/null and b/challenge_api/db/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/db/__pycache__/config.cpython-310.pyc b/challenge_api/db/__pycache__/config.cpython-310.pyc similarity index 88% rename from hexactf/extensions/db/__pycache__/config.cpython-310.pyc rename to challenge_api/db/__pycache__/config.cpython-310.pyc index 69fdfb7..f03e43b 100644 Binary files a/hexactf/extensions/db/__pycache__/config.cpython-310.pyc and b/challenge_api/db/__pycache__/config.cpython-310.pyc differ diff --git a/challenge_api/db/__pycache__/models.cpython-310.pyc b/challenge_api/db/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..b9bcdbb Binary files /dev/null and b/challenge_api/db/__pycache__/models.cpython-310.pyc differ diff --git a/hexactf/extensions/db/__pycache__/repository.cpython-310.pyc b/challenge_api/db/__pycache__/repository.cpython-310.pyc similarity index 100% rename from hexactf/extensions/db/__pycache__/repository.cpython-310.pyc rename to challenge_api/db/__pycache__/repository.cpython-310.pyc diff --git a/hexactf/extensions/db/config.py b/challenge_api/db/config.py similarity index 100% rename from hexactf/extensions/db/config.py rename to challenge_api/db/config.py diff --git a/hexactf/extensions/db/models.py b/challenge_api/db/models.py similarity index 78% rename from hexactf/extensions/db/models.py rename to challenge_api/db/models.py index 235e482..fb07453 100644 --- a/hexactf/extensions/db/models.py +++ b/challenge_api/db/models.py @@ -1,5 +1,6 @@ from datetime import datetime, timedelta -from hexactf.extensions_manager import db +from enum import Enum +from challenge_api.extensions_manager import db from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship @@ -10,7 +11,8 @@ def current_time_kst(): class Users(db.Model): __tablename__ = 'Users' - email = db.Column(db.String(255), primary_key=True, nullable=False, unique=True) + idx = db.Column(db.Integer, primary_key=True, nullable=False, autoincrement=True) + email = db.Column(db.String(255), nullable=False, unique=True) username = db.Column(db.String(20), nullable=False, unique=True) password = db.Column(db.String(255), nullable=False) homepage = db.Column(db.String(255), default='', nullable=False) @@ -83,45 +85,37 @@ class Challenges(db.Model): unlockChallenges = db.Column(db.String(255), default='', nullable=False) isPersistence = db.Column(db.Boolean, default=False, nullable=False) - +# UserChallenges Table Model class UserChallenges(db.Model): - """사용자 챌린지 모델""" __tablename__ = 'UserChallenges' idx = db.Column(db.Integer, primary_key=True, autoincrement=True) - username = db.Column(db.String(20), ForeignKey('Users.username'), nullable=False) - C_idx = db.Column(db.Integer, ForeignKey('Challenges.idx'), nullable=False) + user_idx = db.Column(db.Integer, ForeignKey('Users.idx'), default=0, nullable=False) + C_idx = db.Column(db.Integer, db.ForeignKey('Challenges.idx'), nullable=False) userChallengeName = db.Column(db.String(255), nullable=False) - port = db.Column(db.Integer, nullable=False) - status = db.Column(db.String(50), default='None', nullable=False) createdAt = db.Column(db.DateTime, default=current_time_kst, nullable=False) - # 관계 설정 - user = relationship('Users', backref='challenges') - challenge = relationship('Challenges', backref='user_challenges') - - # __table_args__ = ( - # CheckConstraint('(port == 0) OR (port >= 15000 AND port <= 30000)', name='checkPort'), - # ) - def __init__(self, username: str, C_idx: int, userChallengeName: str, port: int, status: str = 'None'): - self.username = username - self.C_idx = C_idx - self.userChallengeName = userChallengeName - self.port = port - self.status = status +# UserChallenges_status Table Model +class UserChallenges_status(db.Model): + __tablename__ = 'UserChallenges_status' - def to_dict(self) -> dict: - """모델을 딕셔너리로 변환""" - return { - 'idx': self.idx, - 'username': self.username, - 'C_idx': self.C_idx, - 'userChallengeName': self.userChallengeName, - 'port': self.port, - 'status': self.status, - 'createdAt': self.createdAt.isoformat() - } + idx = db.Column(db.Integer, primary_key=True, autoincrement=True) + port = db.Column(db.Integer, nullable=False) + status = db.Column(db.String(20), nullable=False, default='None') + createdAt = db.Column(db.DateTime, default=current_time_kst, nullable=False) + userChallenge_idx = db.Column(db.Integer, db.ForeignKey('UserChallenges.idx'), nullable=False) +# Submissions Table Model +class Submissions(db.Model): + __tablename__ = 'Submissions' - + idx = db.Column(db.Integer, primary_key=True, autoincrement=True) + C_idx = db.Column(db.Integer, ForeignKey('Challenges.idx'), nullable=False) + username = db.Column(db.String(20), ForeignKey('Users.username'), nullable=False) + teamName = db.Column(db.String(20), nullable=False) + title = db.Column(db.String(255), nullable=False) + type = db.Column(db.Boolean, nullable=False) + provided = db.Column(db.String(255), nullable=False) + currentStatus = db.Column(db.Boolean, default=True, nullable=False) + createdAt = db.Column(db.DateTime, default=current_time_kst, nullable=False) diff --git a/challenge_api/db/repository/__init__.py b/challenge_api/db/repository/__init__.py new file mode 100644 index 0000000..8c27135 --- /dev/null +++ b/challenge_api/db/repository/__init__.py @@ -0,0 +1,9 @@ +from .challenge_repo import ChallengeRepository +from .userchallenge_repo import UserChallengesRepository +from .userchallenge_status_repo import UserChallengeStatusRepository + +__all__ = [ + 'ChallengeRepository', + 'UserChallengesRepository', + 'UserChallengeStatusRepository' +] \ No newline at end of file diff --git a/challenge_api/db/repository/__pycache__/__init__.cpython-310.pyc b/challenge_api/db/repository/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..145042a Binary files /dev/null and b/challenge_api/db/repository/__pycache__/__init__.cpython-310.pyc differ diff --git a/challenge_api/db/repository/__pycache__/challenge_repo.cpython-310.pyc b/challenge_api/db/repository/__pycache__/challenge_repo.cpython-310.pyc new file mode 100644 index 0000000..7c81785 Binary files /dev/null and b/challenge_api/db/repository/__pycache__/challenge_repo.cpython-310.pyc differ diff --git a/challenge_api/db/repository/__pycache__/userchallenge_repo.cpython-310.pyc b/challenge_api/db/repository/__pycache__/userchallenge_repo.cpython-310.pyc new file mode 100644 index 0000000..f3efaa2 Binary files /dev/null and b/challenge_api/db/repository/__pycache__/userchallenge_repo.cpython-310.pyc differ diff --git a/challenge_api/db/repository/__pycache__/userchallenge_status_repo.cpython-310.pyc b/challenge_api/db/repository/__pycache__/userchallenge_status_repo.cpython-310.pyc new file mode 100644 index 0000000..626c37e Binary files /dev/null and b/challenge_api/db/repository/__pycache__/userchallenge_status_repo.cpython-310.pyc differ diff --git a/challenge_api/db/repository/challenge_repo.py b/challenge_api/db/repository/challenge_repo.py new file mode 100644 index 0000000..6c4aad2 --- /dev/null +++ b/challenge_api/db/repository/challenge_repo.py @@ -0,0 +1,36 @@ +from challenge_api.db.models import Challenges +from challenge_api.extensions_manager import db +from challenge_api.objects.challenge_info import ChallengeInfo +from challenge_api.exceptions.api_exceptions import InternalServerError +from sqlalchemy.exc import SQLAlchemyError +from typing import Optional + +class ChallengeRepository: + + def __init__(self, session=None): + self.session = session or db.session + + def is_exist(self, challenge_id: int) -> bool: + """ + 챌린지 아이디 존재 여부 확인 + + Args: + challenge_id (int): 챌린지 아이디 + + """ + challenge = Challenges.query.get(challenge_id) + return challenge is not None + + def get_challenge_name(challenge_id: int) -> Optional[str]: + """ + 챌린지 아이디로 챌린지 조회 + + Args: + challenge_id (int): 챌린지 아이디 + + Returns: + str: 챌린지 이름 + """ + challenge = Challenges.query.get(challenge_id) + return challenge.title if challenge else None + \ No newline at end of file diff --git a/challenge_api/db/repository/userchallenge_repo.py b/challenge_api/db/repository/userchallenge_repo.py new file mode 100644 index 0000000..588c467 --- /dev/null +++ b/challenge_api/db/repository/userchallenge_repo.py @@ -0,0 +1,80 @@ +from challenge_api.db.models import UserChallenges +from challenge_api.objects.challenge_info import ChallengeInfo +from challenge_api.exceptions.api_exceptions import InternalServerError +from sqlalchemy.exc import SQLAlchemyError +from typing import Optional + +from challenge_api.extensions_manager import db + +class UserChallengesRepository: + def __init__(self, session=None): + self.session = session or db.session + + def create(self, challenge_info: ChallengeInfo) -> Optional[UserChallenges]: + """새로운 사용자 챌린지 생성 + + Args: + challenge_name (ChallengeName): 챌린지 이름 객체 + + Returns: + Optional[UserChallenges]: 생성된 챌린지, 실패시 None + + Raises: + InternalServerError: DB 작업 실패시 + """ + try: + challenge = UserChallenges( + C_idx=challenge_info.challenge_id, + user_idx=challenge_info.user_id, + userChallengeName=challenge_info.name, + ) + self.session.add(challenge) + self.session.commit() + return challenge + + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e + + def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: + """ + 사용자 챌린지 이름 조회 + + Args: + userChallengeName (str): 사용자 챌린지 이름 + + Returns: + UserChallenges: 사용자 챌린지 + """ + user_challenge = UserChallenges.query.filter_by(userChallengeName=userChallengeName).first() + if not user_challenge: + return None + return user_challenge + + def is_running(self, challenge: UserChallenges) -> bool: + """ + 챌린지 실행 여부 확인 + + Args: + challenge (UserChallenges): 사용자 챌린지 + + Returns: + bool: 챌린지 실행 여부 + """ + return challenge.status == 'Running' + + def is_exist(self, challenge_info: ChallengeInfo) -> bool: + """챌린지 이름 존재 여부 확인 + + Args: + challenge_info (ChallengeInfo): 챌린지 이름 객체 + + Returns: + bool: 존재 여부 + """ + return ( + UserChallenges.query + .filter_by(userChallengeName=challenge_info.name) + .first() is not None + ) + \ No newline at end of file diff --git a/challenge_api/db/repository/userchallenge_status_repo.py b/challenge_api/db/repository/userchallenge_status_repo.py new file mode 100644 index 0000000..d268753 --- /dev/null +++ b/challenge_api/db/repository/userchallenge_status_repo.py @@ -0,0 +1,107 @@ +from challenge_api.db.models import UserChallenges_status, UserChallenges +from challenge_api.extensions_manager import db +from challenge_api.objects.challenge_info import ChallengeInfo +from challenge_api.exceptions.api_exceptions import InternalServerError +from sqlalchemy.exc import SQLAlchemyError +from sqlalchemy import text +from typing import Optional + +class UserChallengeStatusRepository: + def __init__(self, session=None): + self.session = session or db.session + + def create(self, userchallenge_idx: int, port: int) -> Optional[UserChallenges_status]: + """ + 새로운 사용자 챌린지 상태 생성 + + Args: + port (int): 포트 + status (str): 상태 + + Returns: + UserChallenges: 생성된 사용자 챌린지 상태 + """ + try: + # Check if UserChallenge exists + user_challenge = self.session.get(UserChallenges, userchallenge_idx) + if not user_challenge: + raise InternalServerError(error_msg=f"UserChallenge not found with idx: {userchallenge_idx}") + + challenge_status = UserChallenges_status( + port=port, + status="None", + userChallenge_idx=userchallenge_idx + ) + self.session.add(challenge_status) + self.session.commit() + return challenge_status + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error creating challenge status in db: {e}") from e + + def get_recent_status(self, userchallenge_idx: int) -> Optional[UserChallenges_status]: + """ + 최근 사용자 챌린지 상태 조회 + + Args: + name (ChallengeName): 챌린지 이름 객체 + + Returns: + Optional[UserChallenges_status]: 가장 최근 상태, 없으면 None + """ + try: + return UserChallenges_status.query \ + .filter_by(userChallenge_idx=userchallenge_idx) \ + .order_by(UserChallenges_status.createdAt.desc()) \ + .first() + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error getting recent challenge status in db: {e}") from e + + + + def update_status(self,status_idx:int, new_status: str) -> bool: + """ + 사용자 챌린지 상태 업데이트 + + Args: + challenge (UserChallenges): 사용자 챌린지 + new_status (str): 새로운 상태 + + Returns: + bool: 업데이트 성공 여부 + """ + try: + + status = self.session.get(UserChallenges_status, status_idx) + if not status: + raise InternalServerError(error_msg=f"UserChallengeStatus not found with idx: {status_idx}") + status.status = new_status + self.session.commit() + return True + except SQLAlchemyError as e: + self.session.rollback() + raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e + + + def update_port(self, status_idx:int, port: int) -> bool: + """ + 챌린지 포트 업데이트 + + Args: + challenge (UserChallenges): 사용자 챌린지 + port (int): 새로운 포트 + + Returns: + bool: 업데이트 성공 여부 + """ + try: + status = self.session.get(UserChallenges_status, status_idx) + if not status: + raise InternalServerError(error_msg=f"UserChallengeStatus not found with idx: {status_idx}") + status.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 \ No newline at end of file diff --git a/hexactf/exceptions/__init__.py b/challenge_api/exceptions/__init__.py similarity index 100% rename from hexactf/exceptions/__init__.py rename to challenge_api/exceptions/__init__.py diff --git a/challenge_api/exceptions/__pycache__/__init__.cpython-310.pyc b/challenge_api/exceptions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..6e355d4 Binary files /dev/null and b/challenge_api/exceptions/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc b/challenge_api/exceptions/__pycache__/api_exceptions.cpython-310.pyc similarity index 73% rename from hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/api_exceptions.cpython-310.pyc index 5d33390..e8d9b15 100644 Binary files a/hexactf/exceptions/__pycache__/api_exceptions.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/api_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc b/challenge_api/exceptions/__pycache__/base_exceptions.cpython-310.pyc similarity index 76% rename from hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/base_exceptions.cpython-310.pyc index 3ce3945..df461b4 100644 Binary files a/hexactf/exceptions/__pycache__/base_exceptions.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/base_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc b/challenge_api/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc similarity index 68% rename from hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc index 286fe0b..084f560 100644 Binary files a/hexactf/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/challenge_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/error_types.cpython-310.pyc b/challenge_api/exceptions/__pycache__/error_types.cpython-310.pyc similarity index 72% rename from hexactf/exceptions/__pycache__/error_types.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/error_types.cpython-310.pyc index 2cd78f3..b49a623 100644 Binary files a/hexactf/exceptions/__pycache__/error_types.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/error_types.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/handlers.cpython-310.pyc b/challenge_api/exceptions/__pycache__/handlers.cpython-310.pyc similarity index 100% rename from hexactf/exceptions/__pycache__/handlers.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/handlers.cpython-310.pyc diff --git a/hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc b/challenge_api/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc similarity index 81% rename from hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc index f986c6f..1a4524d 100644 Binary files a/hexactf/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/kafka_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc b/challenge_api/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc similarity index 81% rename from hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc rename to challenge_api/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc index 97d7ab7..5250734 100644 Binary files a/hexactf/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc and b/challenge_api/exceptions/__pycache__/userchallenge_exceptions.cpython-310.pyc differ diff --git a/hexactf/exceptions/api_exceptions.py b/challenge_api/exceptions/api_exceptions.py similarity index 87% rename from hexactf/exceptions/api_exceptions.py rename to challenge_api/exceptions/api_exceptions.py index 8fb667d..8523090 100644 --- a/hexactf/exceptions/api_exceptions.py +++ b/challenge_api/exceptions/api_exceptions.py @@ -1,6 +1,6 @@ -from hexactf.exceptions.base_exceptions import CustomBaseException -from hexactf.exceptions.error_types import ApiErrorTypes +from challenge_api.exceptions.base_exceptions import CustomBaseException +from challenge_api.exceptions.error_types import ApiErrorTypes class APIException(CustomBaseException): diff --git a/hexactf/exceptions/base_exceptions.py b/challenge_api/exceptions/base_exceptions.py similarity index 88% rename from hexactf/exceptions/base_exceptions.py rename to challenge_api/exceptions/base_exceptions.py index c0d2229..7cef7a8 100644 --- a/hexactf/exceptions/base_exceptions.py +++ b/challenge_api/exceptions/base_exceptions.py @@ -1,6 +1,6 @@ -# app/utils/exceptions.py +# challenge_api/exceptions/base_exceptions.py -from hexactf.exceptions.error_types import ApiErrorTypes +from challenge_api.exceptions.error_types import ApiErrorTypes class CustomBaseException(Exception): """ diff --git a/hexactf/exceptions/challenge_exceptions.py b/challenge_api/exceptions/challenge_exceptions.py similarity index 82% rename from hexactf/exceptions/challenge_exceptions.py rename to challenge_api/exceptions/challenge_exceptions.py index 427982e..184e683 100644 --- a/hexactf/exceptions/challenge_exceptions.py +++ b/challenge_api/exceptions/challenge_exceptions.py @@ -1,5 +1,5 @@ -from hexactf.exceptions.base_exceptions import CustomBaseException -from hexactf.exceptions.error_types import ApiErrorTypes +from challenge_api.exceptions.base_exceptions import CustomBaseException +from challenge_api.exceptions.error_types import ApiErrorTypes class ChallengeException(CustomBaseException): diff --git a/hexactf/exceptions/error_types.py b/challenge_api/exceptions/error_types.py similarity index 100% rename from hexactf/exceptions/error_types.py rename to challenge_api/exceptions/error_types.py diff --git a/hexactf/exceptions/handlers.py b/challenge_api/exceptions/handlers.py similarity index 85% rename from hexactf/exceptions/handlers.py rename to challenge_api/exceptions/handlers.py index d42670b..5c8ee19 100644 --- a/hexactf/exceptions/handlers.py +++ b/challenge_api/exceptions/handlers.py @@ -1,6 +1,6 @@ from flask import jsonify -from hexactf.exceptions.base_exceptions import CustomBaseException +from challenge_api.exceptions.base_exceptions import CustomBaseException def register_error_handler(app): """CustomBaseException을 처리하는 에러 핸들러""" diff --git a/hexactf/exceptions/kafka_exceptions.py b/challenge_api/exceptions/kafka_exceptions.py similarity index 92% rename from hexactf/exceptions/kafka_exceptions.py rename to challenge_api/exceptions/kafka_exceptions.py index fb0bc7b..f76a019 100644 --- a/hexactf/exceptions/kafka_exceptions.py +++ b/challenge_api/exceptions/kafka_exceptions.py @@ -1,5 +1,5 @@ -from hexactf.exceptions.base_exceptions import CustomBaseException -from hexactf.exceptions.error_types import ApiErrorTypes +from challenge_api.exceptions.base_exceptions import CustomBaseException +from challenge_api.exceptions.error_types import ApiErrorTypes class QueueException(CustomBaseException): """Queue(Kafka) 관련 기본 예외""" diff --git a/hexactf/exceptions/userchallenge_exceptions.py b/challenge_api/exceptions/userchallenge_exceptions.py similarity index 93% rename from hexactf/exceptions/userchallenge_exceptions.py rename to challenge_api/exceptions/userchallenge_exceptions.py index 83da20e..985cd52 100644 --- a/hexactf/exceptions/userchallenge_exceptions.py +++ b/challenge_api/exceptions/userchallenge_exceptions.py @@ -1,6 +1,6 @@ -from hexactf.exceptions.base_exceptions import CustomBaseException -from hexactf.exceptions.error_types import ApiErrorTypes +from challenge_api.exceptions.base_exceptions import CustomBaseException +from challenge_api.exceptions.error_types import ApiErrorTypes class UserChallengeException(CustomBaseException): """UserChallenge 관련 기본 예외""" diff --git a/hexactf/extensions/__init__.py b/challenge_api/extensions/__init__.py similarity index 100% rename from hexactf/extensions/__init__.py rename to challenge_api/extensions/__init__.py diff --git a/challenge_api/extensions/__pycache__/__init__.cpython-310.pyc b/challenge_api/extensions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..921f307 Binary files /dev/null and b/challenge_api/extensions/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/k8s/__init__.py b/challenge_api/extensions/k8s/__init__.py similarity index 100% rename from hexactf/extensions/k8s/__init__.py rename to challenge_api/extensions/k8s/__init__.py diff --git a/challenge_api/extensions/k8s/__pycache__/__init__.cpython-310.pyc b/challenge_api/extensions/k8s/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..056b174 Binary files /dev/null and b/challenge_api/extensions/k8s/__pycache__/__init__.cpython-310.pyc differ diff --git a/challenge_api/extensions/k8s/__pycache__/client.cpython-310.pyc b/challenge_api/extensions/k8s/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..74ba58e Binary files /dev/null and b/challenge_api/extensions/k8s/__pycache__/client.cpython-310.pyc differ diff --git a/challenge_api/extensions/k8s/client.py b/challenge_api/extensions/k8s/client.py new file mode 100644 index 0000000..930437d --- /dev/null +++ b/challenge_api/extensions/k8s/client.py @@ -0,0 +1,233 @@ +import os +import re +import time + +from kubernetes import client, config, watch + +from challenge_api.exceptions.challenge_exceptions import ChallengeNotFound +from challenge_api.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError +from challenge_api.db.repository import ChallengeRepository, UserChallengesRepository, UserChallengeStatusRepository +from challenge_api.objects.challenge_info import ChallengeInfo +from challenge_api.utils.namebuilder import NameBuilder +MAX_RETRIES = 3 +SLEEP_INTERVAL = 2 + +class K8sClient: + """ + Client class for managing Kubernetes Custom Resources + + Creates, deletes, and manages the state of Challenge Custom Resources. + Automatically loads configuration to run either inside or outside the cluster. + """ + + def __init__(self): + try: + config.load_incluster_config() + except config.ConfigException: + config.load_kube_config() + + self.custom_api = client.CustomObjectsApi() + self.core_api = client.CoreV1Api() + + + def create(self, data:ChallengeInfo, namespace="challenge") -> int: + """ + Challenge Custom Resource를 생성하고 NodePort를 반환합니다. + + Args: + data (ChallengeRequest): Challenge 생성 요청 데이터 + namespace (str): Challenge를 생성할 네임스페이스 (기본값: "default") + + Returns: + int: 할당된 NodePort 번호 + + Raises: + ChallengeNotFound: Challenge ID에 해당하는 Challenge가 없을 때 + UserChallengeCreationError: Challenge Custom Resource 생성에 실패했을 때 + """ + + # Repository + userchallenge_repo = UserChallengesRepository() + userchallenge_status_repo = UserChallengeStatusRepository() + + # user id를 숫자 대신에 문자로 표현 + challenge_id, user_id = data.challenge_id, str(data.user_id) + + namebuilder = NameBuilder(challenge_id=challenge_id, user_id=user_id) + challenge_info= namebuilder.build() + + # Database에 UserChallenge 생성 + + if not userchallenge_repo.is_exist(challenge_info): + userchallenge = userchallenge_repo.create(challenge_info) + else: + userchallenge = userchallenge_repo.get_by_user_challenge_name(challenge_info.name) + recent = userchallenge_status_repo.get_recent_status(userchallenge.idx) + # 이미 실행 중인 Challenge가 있으면 데이터베이스에 저장된 포트 번호 반환 + if recent and recent.status == 'Running': + return recent.port + + + # 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 생성 및 검증 + + # Namespace 존재 여부 확인 + # TODO: 환경 체크 로직 분리 + # try: + # self.core_api.read_namespace(namespace) + # except Exception as e: + # raise UserChallengeCreationError(error_msg=str(e)) + + # 공백의 경우 하이픈으로 변환 + # TODO: Definition 이름에 따른 정규화 로직 추가 + # challenge_definition = self._normalize_k8s_name(challenge_definition) + # valid_username = self._normalize_k8s_name(username) + # Challenge manifest 생성 + challenge_manifest = { + "apiVersion": "apps.hexactf.io/v2alpha1", + "kind": "Challenge", + "metadata": { + "name": challenge_info.name, + "labels": { + "apps.hexactf.io/challengeId": str(challenge_id), + "apps.hexactf.io/userId": user_id + } + }, + "spec": { + "namespace": namespace, + "definition": challenge_definition + } + } + + challenge = self.custom_api.create_namespaced_custom_object( + group="apps.hexactf.io", + version="v2alpha1", + namespace=namespace, + plural="challenges", + body=challenge_manifest + ) + + status = None + endpoint = 0 + field_selector = f"apps.hexactf.io/challengeId={challenge_id},apps.hexactf.io/userId={user_id}" + w = watch.Watch() + for event in w.stream(self.custom_api.list_namespaced_custom_object, + group="apps.hexactf.io", + version="v2alpha1", + namespace=namespace, + label_selector=field_selector, + plural="challenges", + ): + obj = event['object'] + + if obj.get('status', {}).get('currentStatus', {}).get('status',"") == 'Running': + status = event['object']['status'] + endpoint = status.get('endpoint') + w.stop() + break + + + # 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['metadata']['name'] + # ) + # status = challenge.get('status', {}) + # endpoint = status.get('endpoint') + + # NodePort 업데이트 + if not endpoint: + raise UserChallengeCreationError(error_msg=f"Failed to get NodePort for Challenge: {challenge_info.name}") + + return endpoint + + + # TODO: 별도의 Validator로 분리 + # def _normalize_k8s_name(self, name: str) -> str: + # """ + # Kubernetes 리소스 이름을 유효한 형식으로 변환 (소문자 + 공백을 하이픈으로 변경) + + # Args: + # name (str): 원본 이름 + + # Returns: + # str: 변환된 Kubernetes 리소스 이름 + # """ + # if not name or len(name) > 253: + # raise ValueError("이름이 비어있거나 길이가 253자를 초과함") + + # name = name.lower() + # name = re.sub(r'[^a-z0-9-]+', '-', name) + # name = re.sub(r'-+', '-', name) + # name = name.strip('-') + + # # 최종 길이 검사 (1~253자) + # if not name or len(name) > 253: + # raise ValueError(f"변환 후에도 유효하지 않은 Kubernetes 리소스 이름: {name}") + + # return name + + # def _is_valid_k8s_name(self, name: str) -> bool: + # """ + # Kubernetes 리소스 이름 유효성 검사 + + # Args: + # name (str): 검사할 이름 + + # Returns: + # bool: 유효한 이름인지 여부 + # """ + + # # 소문자로 변환 + # name = name.lower() + + # # Kubernetes naming convention 검사 + # if not name or len(name) > 253: + # return False + + # # DNS-1123 label 규칙 검사 + # import re + # pattern = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + # return bool(re.match(pattern, name)) + + def delete(self, challenge_info: ChallengeInfo, namespace="challenge"): + """ + Challenge Custom Resource를 삭제합니다. + + Args: + challenge_info (ChallengeInfo): Challenge 삭제 요청 데이터 + namespace (str): Challenge가 생성된 네임스페이스 (기본값: "default") + + Raises: + UserChallengeDeletionError: Challenge 삭제에 실패했을 때 + """ + + # UserChallenge 조회 + namebuilder = NameBuilder(challenge_id=challenge_info.challenge_id, user_id=challenge_info.user_id) + challenge_info = namebuilder.build() + user_challenge_repo = UserChallengesRepository() + user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_info.name) + if not user_challenge: + raise UserChallengeDeletionError(error_msg=f"Deletion : UserChallenge not found: {challenge_info.name}") + + # 사용자 챌린지(컨테이너) 삭제 + try: + self.custom_api.delete_namespaced_custom_object( + group="apps.hexactf.io", + version="v2alpha1", + namespace=namespace, + plural="challenges", + name=challenge_info.name + ) + + except Exception as e: + raise UserChallengeDeletionError(error_msg=str(e)) from e + diff --git a/challenge_api/extensions/kafka/__init__.py b/challenge_api/extensions/kafka/__init__.py new file mode 100644 index 0000000..3b5dfc2 --- /dev/null +++ b/challenge_api/extensions/kafka/__init__.py @@ -0,0 +1,9 @@ +__version__ = '1.0.0' + +__all__ = [ + 'KafkaEventConsumer', + 'KafkaConfig', +] + +from challenge_api.extensions.kafka.config import KafkaConfig +from challenge_api.extensions.kafka.consumer import KafkaEventConsumer diff --git a/challenge_api/extensions/kafka/__pycache__/__init__.cpython-310.pyc b/challenge_api/extensions/kafka/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..35f1296 Binary files /dev/null and b/challenge_api/extensions/kafka/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc b/challenge_api/extensions/kafka/__pycache__/config.cpython-310.pyc similarity index 82% rename from hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc rename to challenge_api/extensions/kafka/__pycache__/config.cpython-310.pyc index ae7b145..c16a251 100644 Binary files a/hexactf/extensions/kafka/__pycache__/config.cpython-310.pyc and b/challenge_api/extensions/kafka/__pycache__/config.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc b/challenge_api/extensions/kafka/__pycache__/consumer.cpython-310.pyc similarity index 91% rename from hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc rename to challenge_api/extensions/kafka/__pycache__/consumer.cpython-310.pyc index 1353c0a..512fd29 100644 Binary files a/hexactf/extensions/kafka/__pycache__/consumer.cpython-310.pyc and b/challenge_api/extensions/kafka/__pycache__/consumer.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc b/challenge_api/extensions/kafka/__pycache__/handler.cpython-310.pyc similarity index 84% rename from hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc rename to challenge_api/extensions/kafka/__pycache__/handler.cpython-310.pyc index ace2d47..dd47b5f 100644 Binary files a/hexactf/extensions/kafka/__pycache__/handler.cpython-310.pyc and b/challenge_api/extensions/kafka/__pycache__/handler.cpython-310.pyc differ diff --git a/hexactf/extensions/kafka/config.py b/challenge_api/extensions/kafka/config.py similarity index 100% rename from hexactf/extensions/kafka/config.py rename to challenge_api/extensions/kafka/config.py diff --git a/hexactf/extensions/kafka/consumer.py b/challenge_api/extensions/kafka/consumer.py similarity index 84% rename from hexactf/extensions/kafka/consumer.py rename to challenge_api/extensions/kafka/consumer.py index bbb8171..eb126d1 100644 --- a/hexactf/extensions/kafka/consumer.py +++ b/challenge_api/extensions/kafka/consumer.py @@ -3,17 +3,18 @@ from typing import Any, Dict from kafka import KafkaConsumer import json -from hexactf.exceptions.kafka_exceptions import QueueProcessingError +from challenge_api.exceptions.kafka_exceptions import QueueProcessingError class StatusMessage: """상태 메시지를 표현하는 클래스""" - def __init__(self, user: str, problemId: str, newStatus: str, timestamp: str): + def __init__(self, userId: str, problemId: str, newStatus: str, timestamp: str, endpoint: str = None): # 메시지의 기본 속성들을 초기화 - self.user = user # 사용자 ID + self.userId = userId # 사용자 ID self.problemId = problemId # 문제 ID self.newStatus = newStatus # 새로운 상태 self.timestamp = timestamp # 타임스탬프 + self.endpoint = endpoint # 엔드포인트 @classmethod def from_json(cls, data: Dict[str, Any]) -> 'StatusMessage': @@ -26,15 +27,16 @@ def from_json(cls, data: Dict[str, Any]) -> 'StatusMessage': StatusMessage 인스턴스 """ return cls( - user=data['user'], + userId=data['userId'], problemId=data['problemId'], newStatus=data['newStatus'], - timestamp=data['timestamp'] + timestamp=data['timestamp'], + endpoint=data.get('endpoint') # endpoint가 없을 수 있으므로 get 사용 ) def __str__(self) -> str: """객체를 문자열로 표현""" - return f"StatusMessage(user={self.user}, problemId={self.problemId}, newStatus={self.newStatus}, timestamp={self.timestamp})" + return f"StatusMessage(userId={self.userId}, problemId={self.problemId}, newStatus={self.newStatus}, timestamp={self.timestamp}, endpoint={self.endpoint})" class KafkaEventConsumer: """Kafka 이벤트 소비자 클래스""" diff --git a/challenge_api/extensions/kafka/handler.py b/challenge_api/extensions/kafka/handler.py new file mode 100644 index 0000000..736b9e3 --- /dev/null +++ b/challenge_api/extensions/kafka/handler.py @@ -0,0 +1,81 @@ +import logging +from typing import Any, Dict +from challenge_api.exceptions.kafka_exceptions import QueueProcessingError +from challenge_api.db.repository import UserChallengesRepository, UserChallengeStatusRepository +from challenge_api.objects.challenge_info import ChallengeInfo + +logger = logging.getLogger(__name__) + +class MessageHandler: + VALID_STATUSES = {'Pending', 'Running', 'Deleted', 'Error'} + + @staticmethod + def validate_message(message: Dict[str, Any]) -> tuple[str, str, str, str, str]: + """ + Kafka 메세지의 필수 필드를 검증하고 반환 + + Args: + message (Dict[str, Any]): Kafka 메세지 + + Returns: + tuple[str, str, str, str, str]: 사용자 이름, 챌린지 ID, 새로운 상태, 타임스탬프, 엔드포인트 + """ + try: + user_id = message.userId + problem_id = message.problemId + new_status = message.newStatus + endpoint = message.endpoint + timestamp = message.timestamp + except AttributeError: + user_id = message['userId'] + problem_id = message['problemId'] + new_status = message['newStatus'] + timestamp = message['timestamp'] + endpoint = message.get('endpoint') # endpoint가 없을 수 있으므로 get 사용 + + if not all([user_id, problem_id, new_status, timestamp]): + raise QueueProcessingError(error_msg=f"Kafka Error : Missing required fields in message: {message}") + + if new_status not in MessageHandler.VALID_STATUSES: + raise QueueProcessingError(error_msg=f"Kafka Error : Invalid status in message: {new_status}") + + return user_id, problem_id, new_status, timestamp, endpoint + + @staticmethod + def handle_message(message: Dict[str, Any]): + """ + Consume한 Kafka message 내용을 DB에 반영 + + Args: + message: Kafka 메세지 + """ + try: + user_id, challenge_id, new_status, _, endpoint = MessageHandler.validate_message(message) + challenge_info = ChallengeInfo(challenge_id=int(challenge_id), user_id=int(user_id)) + challenge_name = challenge_info.name + + # 상태 정보 업데이트 + userchallenge_repo = UserChallengesRepository() + status_repo = UserChallengeStatusRepository() + + if userchallenge_repo.is_exist(challenge_info): + userchallenge = userchallenge_repo.get_by_user_challenge_name(challenge_name) + recent_status = status_repo.get_recent_status(userchallenge.idx) + + if recent_status is None or new_status == 'Pending': + # 상태가 없으면 새로 생성 + recent_status = status_repo.create(userchallenge_idx=userchallenge.idx, port=int(endpoint) if endpoint else 0) + + elif new_status == 'Running' and endpoint: + # Running 상태이고 endpoint가 있으면 포트 업데이트 + status_repo.update_port(recent_status.idx, int(endpoint)) + + status_repo.update_status(recent_status.idx, new_status) + logger.info(f"Updated status for challenge {challenge_name} to {new_status} with endpoint {endpoint}") + else: + logger.warning(f"Challenge {challenge_name} does not exist") + + except ValueError as e: + raise QueueProcessingError(error_msg=f"Invalid message format {str(e)}") from e + except Exception as e: + raise QueueProcessingError(error_msg=f"Kafka Error: {str(e)}") from e diff --git a/hexactf/extensions_manager.py b/challenge_api/extensions_manager.py similarity index 97% rename from hexactf/extensions_manager.py rename to challenge_api/extensions_manager.py index 07eee54..2e751db 100644 --- a/hexactf/extensions_manager.py +++ b/challenge_api/extensions_manager.py @@ -4,7 +4,7 @@ from typing import Optional, Callable from flask import Flask from flask_sqlalchemy import SQLAlchemy -from hexactf.extensions.kafka import KafkaConfig, KafkaEventConsumer +from challenge_api.extensions.kafka import KafkaConfig, KafkaEventConsumer class FlaskKafkaConsumer: """Flask 애플리케이션에서 Kafka 메시지 소비를 관리하는 클래스""" diff --git a/hexactf/factory.py b/challenge_api/factory.py similarity index 94% rename from hexactf/factory.py rename to challenge_api/factory.py index 9b0bb44..dce6148 100644 --- a/hexactf/factory.py +++ b/challenge_api/factory.py @@ -6,12 +6,12 @@ from datetime import datetime from typing import Any, Dict, Type -from hexactf.api.challenge_api import challenge_bp -from hexactf.config import Config -from hexactf.exceptions.base_exceptions import CustomBaseException -from hexactf.extensions.kafka.handler import MessageHandler -from hexactf.extensions_manager import kafka_consumer, db -from hexactf.monitoring.loki_logger import FlaskLokiLogger +from challenge_api.api.challenge_api import challenge_bp +from challenge_api.config import Config +from challenge_api.exceptions.base_exceptions import CustomBaseException +from challenge_api.extensions.kafka.handler import MessageHandler +from challenge_api.extensions_manager import kafka_consumer, db +# rom challenge_api.monitoring.loki_logger import FlaskLokiLogger def start_kafka_consumer(app): """Start Kafka consumer in a separate thread""" diff --git a/hexactf/monitoring/__init__.py b/challenge_api/monitoring/__init__.py similarity index 100% rename from hexactf/monitoring/__init__.py rename to challenge_api/monitoring/__init__.py diff --git a/challenge_api/monitoring/__pycache__/__init__.cpython-310.pyc b/challenge_api/monitoring/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..8ed8ef3 Binary files /dev/null and b/challenge_api/monitoring/__pycache__/__init__.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc b/challenge_api/monitoring/__pycache__/async_handler.cpython-310.pyc similarity index 74% rename from hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc rename to challenge_api/monitoring/__pycache__/async_handler.cpython-310.pyc index dbe3f71..d833adb 100644 Binary files a/hexactf/monitoring/__pycache__/async_handler.cpython-310.pyc and b/challenge_api/monitoring/__pycache__/async_handler.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc b/challenge_api/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc similarity index 100% rename from hexactf/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc rename to challenge_api/monitoring/__pycache__/ctf_metrics_collector.cpython-310.pyc diff --git a/challenge_api/monitoring/__pycache__/loki_logger.cpython-310.pyc b/challenge_api/monitoring/__pycache__/loki_logger.cpython-310.pyc new file mode 100644 index 0000000..24ee1ed Binary files /dev/null and b/challenge_api/monitoring/__pycache__/loki_logger.cpython-310.pyc differ diff --git a/hexactf/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc b/challenge_api/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc similarity index 100% rename from hexactf/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc rename to challenge_api/monitoring/__pycache__/system_metrics_collector.cpython-310.pyc diff --git a/hexactf/monitoring/async_handler.py b/challenge_api/monitoring/async_handler.py similarity index 100% rename from hexactf/monitoring/async_handler.py rename to challenge_api/monitoring/async_handler.py diff --git a/hexactf/monitoring/ctf_metrics_collector.py b/challenge_api/monitoring/ctf_metrics_collector.py similarity index 100% rename from hexactf/monitoring/ctf_metrics_collector.py rename to challenge_api/monitoring/ctf_metrics_collector.py diff --git a/challenge_api/monitoring/loki_logger.py b/challenge_api/monitoring/loki_logger.py new file mode 100644 index 0000000..cb8f3eb --- /dev/null +++ b/challenge_api/monitoring/loki_logger.py @@ -0,0 +1,102 @@ +# import time +# import json +# import logging +# import traceback + +# from monitoring.async_handler import AsyncHandler +# from logging_loki import LokiHandler + +# class FlaskLokiLogger: +# def __init__(self, app_name,loki_url: str): +# self.app_name = app_name +# self.logger = self._setup_logger(loki_url) + +# def _setup_logger(self, loki_url: str) -> logging.Logger: +# """Loki 로거 설정""" +# tags = { +# "app": self.app_name +# } + +# handler = LokiHandler( +# url=loki_url, +# tags=tags, +# version="1", +# ) + +# handler.setFormatter(LokiJsonFormatter()) +# async_handler = AsyncHandler(handler) +# logger = logging.getLogger(self.app_name) +# logger.setLevel(logging.DEBUG) +# logger.addHandler(async_handler) +# return logger + + + +# class LokiJsonFormatter(logging.Formatter): +# def format(self, record): +# try: +# # 현재 타임스탬프 (나노초 단위) +# timestamp_ns = str(int(time.time() * 1e9)) + +# # record에서 직접 labels와 content 추출 +# # tags = getattr(record, 'tags', {}) +# content = getattr(record, 'content', {}) + +# # 기본 로그 정보 추가 +# base_content = { +# "message": record.getMessage(), +# "level": record.levelname, +# } + +# # 예외 정보 추가 (있는 경우) +# if record.exc_info: +# base_content["exception"] = { +# "type": str(record.exc_info[0]), +# "message": str(record.exc_info[1]), +# "traceback": traceback.format_exception(*record.exc_info) +# } + + +# # content에 기본 로그 정보 병합 +# full_content = {**base_content, **content} + +# # 로그 구조 생성 +# log_entry = { +# "timestamp": timestamp_ns, +# "content": full_content +# } + +# # JSON으로 변환 +# return json.dumps(log_entry, ensure_ascii=False, default=str) + +# except Exception as e: +# # 포맷팅 중 오류 발생 시 대체 로그 +# fallback_entry = { +# "timestamp": str(int(time.time() * 1e9)), +# "labels": {"error": "FORMATTING_FAILURE"}, +# "content": { +# "original_message": record.getMessage(), +# "formatting_error": str(e), +# "record_details": str(getattr(record, '__dict__', 'No __dict__')) +# } +# } +# return json.dumps(fallback_entry) + +# def _serialize_dict(self, data, max_depth=3, current_depth=0): +# """재귀적으로 dict을 직렬화""" +# if current_depth >= max_depth: +# return "" +# if isinstance(data, dict): +# return { +# key: self._serialize_dict(value, max_depth, current_depth + 1) +# for key, value in data.items() +# } +# elif isinstance(data, (list, tuple, set)): +# return [ +# self._serialize_dict(item, max_depth, current_depth + 1) +# for item in data +# ] +# elif hasattr(data, "__dict__"): +# return self._serialize_dict(data.__dict__, max_depth, current_depth + 1) +# else: +# return data \ No newline at end of file diff --git a/hexactf/monitoring/system_metrics_collector.py b/challenge_api/monitoring/system_metrics_collector.py similarity index 100% rename from hexactf/monitoring/system_metrics_collector.py rename to challenge_api/monitoring/system_metrics_collector.py diff --git a/tests/units/__init__.py b/challenge_api/objects/__init__.py similarity index 100% rename from tests/units/__init__.py rename to challenge_api/objects/__init__.py diff --git a/challenge_api/objects/__pycache__/__init__.cpython-310.pyc b/challenge_api/objects/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..cf366ed Binary files /dev/null and b/challenge_api/objects/__pycache__/__init__.cpython-310.pyc differ diff --git a/challenge_api/objects/__pycache__/challenge_info.cpython-310.pyc b/challenge_api/objects/__pycache__/challenge_info.cpython-310.pyc new file mode 100644 index 0000000..9f9a362 Binary files /dev/null and b/challenge_api/objects/__pycache__/challenge_info.cpython-310.pyc differ diff --git a/challenge_api/objects/challenge_info.py b/challenge_api/objects/challenge_info.py new file mode 100644 index 0000000..e3bb6a0 --- /dev/null +++ b/challenge_api/objects/challenge_info.py @@ -0,0 +1,11 @@ +from pydantic import BaseModel + +class ChallengeInfo(BaseModel): + challenge_id: int + user_id: int + + @property + def name(self) -> str: + return f"challenge-{self.challenge_id}-{self.user_id}" + + \ No newline at end of file diff --git a/pytest.ini b/challenge_api/pytest.ini similarity index 100% rename from pytest.ini rename to challenge_api/pytest.ini diff --git a/tests/units/.gitignore b/challenge_api/tests/units/.gitignore similarity index 100% rename from tests/units/.gitignore rename to challenge_api/tests/units/.gitignore diff --git a/tests/units/db/__init__.py b/challenge_api/tests/units/__init__.py similarity index 100% rename from tests/units/db/__init__.py rename to challenge_api/tests/units/__init__.py diff --git a/tests/units/exceptions/__init__.py b/challenge_api/tests/units/db/__init__.py similarity index 100% rename from tests/units/exceptions/__init__.py rename to challenge_api/tests/units/db/__init__.py diff --git a/challenge_api/tests/units/db/conftest.py b/challenge_api/tests/units/db/conftest.py new file mode 100644 index 0000000..d4bb209 --- /dev/null +++ b/challenge_api/tests/units/db/conftest.py @@ -0,0 +1 @@ +from .fixtures import * diff --git a/challenge_api/tests/units/db/fixtures/__init__.py b/challenge_api/tests/units/db/fixtures/__init__.py new file mode 100644 index 0000000..7c2c0f4 --- /dev/null +++ b/challenge_api/tests/units/db/fixtures/__init__.py @@ -0,0 +1,4 @@ +from .db import * +from .challenge import * +from .userchallenge import * +from .userchallenge_status import * diff --git a/tests/units/kafka/__init__.py b/challenge_api/tests/units/db/fixtures/challenge.py similarity index 100% rename from tests/units/kafka/__init__.py rename to challenge_api/tests/units/db/fixtures/challenge.py diff --git a/tests/units/db/conftest.py b/challenge_api/tests/units/db/fixtures/db.py similarity index 75% rename from tests/units/db/conftest.py rename to challenge_api/tests/units/db/fixtures/db.py index ca4a675..ac87c46 100644 --- a/tests/units/db/conftest.py +++ b/challenge_api/tests/units/db/fixtures/db.py @@ -1,11 +1,9 @@ -import os -from flask import Flask import pytest +from flask import Flask from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker, scoped_session -from hexactf.extensions.db.models import UserChallenges -from hexactf.extensions_manager import db +from extensions_manager import db class TestDB: """Test database setup for integration testing""" @@ -38,11 +36,4 @@ def test_db(): test_db_instance = TestDB() with test_db_instance.app.app_context(): yield test_db_instance - test_db_instance.close() - -# autouse 명시적으로 call 하지 않아도 사용할 수 있는 옵션 -@pytest.fixture(scope="session", autouse=True) -def set_test_env(): - """Automatically set TEST_MODE=true for all tests""" - os.environ["TEST_MODE"] = "true" - \ No newline at end of file + test_db_instance.close() \ No newline at end of file diff --git a/challenge_api/tests/units/db/fixtures/userchallenge.py b/challenge_api/tests/units/db/fixtures/userchallenge.py new file mode 100644 index 0000000..030dc5b --- /dev/null +++ b/challenge_api/tests/units/db/fixtures/userchallenge.py @@ -0,0 +1,23 @@ +import pytest + +from db.models import UserChallenges +from objects.challenge_info import ChallengeInfo + +@pytest.fixture +def fake_challenge_info(): + return ChallengeInfo( + challenge_id=1, + user_id=1, + name="challenge-1-1" + ) + + + +@pytest.fixture +def fake_userchallenge(): + return UserChallenges( + user_idx=1, + C_idx=1, + userChallengeName="challenge-1-1", + createdAt="2021-01-01 00:00:00" + ) diff --git a/challenge_api/tests/units/db/fixtures/userchallenge_status.py b/challenge_api/tests/units/db/fixtures/userchallenge_status.py new file mode 100644 index 0000000..23a45ec --- /dev/null +++ b/challenge_api/tests/units/db/fixtures/userchallenge_status.py @@ -0,0 +1,10 @@ +import pytest + +from db.models import UserChallenges_status + +@pytest.fixture +def fake_userchallenge_status(): + return UserChallenges_status( + port=1, + userChallenge_idx=1 + ) \ No newline at end of file diff --git a/challenge_api/tests/units/db/repository/test_userchallenge_repo.py b/challenge_api/tests/units/db/repository/test_userchallenge_repo.py new file mode 100644 index 0000000..644139c --- /dev/null +++ b/challenge_api/tests/units/db/repository/test_userchallenge_repo.py @@ -0,0 +1,45 @@ +import pytest +from exceptions.api_exceptions import InternalServerError +from db.models import UserChallenges +from db.repository import UserChallengesRepository + +# =============================================== +# create 테스트 +# =============================================== +def test_userchallenge_create_success(test_db, fake_challenge_info): + """Test successful user challenge creation""" + # when + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + # given + challenge = user_challenges_repo.create(fake_challenge_info) + + # then + assert challenge is not None + assert challenge.user_idx == fake_challenge_info.user_id + assert challenge.C_idx == fake_challenge_info.challenge_id + assert challenge.userChallengeName == fake_challenge_info.name + + +# =============================================== +# is_exist 테스트 +# =============================================== +def test_userchallenge_is_exist_success(test_db, fake_challenge_info): + """Test successful user challenge existence check""" + # when + session = test_db.get_session() + user_challenges_repo = UserChallengesRepository(session=session) + + # given + user_challenges_repo.create(fake_challenge_info) + + # then + assert user_challenges_repo.is_exist(fake_challenge_info) is True + + + + + + + \ No newline at end of file diff --git a/challenge_api/tests/units/db/repository/test_userchallenge_status.py b/challenge_api/tests/units/db/repository/test_userchallenge_status.py new file mode 100644 index 0000000..bbf4657 --- /dev/null +++ b/challenge_api/tests/units/db/repository/test_userchallenge_status.py @@ -0,0 +1,49 @@ + +import pytest +from exceptions.api_exceptions import InternalServerError +from db.repository import UserChallengeStatusRepository, UserChallengesRepository +# =============================================== +# create 테스트 +# =============================================== +def test_userchallenge_status_create_success(test_db, fake_challenge_info, fake_userchallenge_status): + """Test successful user challenge status creation""" + # when + session = test_db.get_session() + userchallenge_status_repo = UserChallengeStatusRepository(session=session) + + # given + user_challenges_repo = UserChallengesRepository(session=session) + user_challenges_repo.create(fake_challenge_info) + status = userchallenge_status_repo.create(fake_userchallenge_status.userChallenge_idx, fake_userchallenge_status.port) + + # then + assert status is not None + assert status.port == fake_userchallenge_status.port + assert status.status == "None" + assert status.userChallenge_idx == fake_userchallenge_status.userChallenge_idx + +def test_userchallenge_status_create_fail_userchallenge_not_found(test_db, fake_userchallenge_status): + """Test failed user challenge status creation when user challenge not found""" + # when + session = test_db.get_session() + userchallenge_status_repo = UserChallengeStatusRepository(session=session) + + # then + with pytest.raises(InternalServerError): + # given + userchallenge_status_repo.create( + fake_userchallenge_status.userChallenge_idx, + fake_userchallenge_status.port + ) + +# =============================================== +# get_recent_status 테스트 +# =============================================== +def test_userchallenge_status_get_recent_status_success(test_db, fake_challenge_info, fake_userchallenge_status): + """Test successful user challenge status retrieval""" + pass + + + + + \ No newline at end of file diff --git a/challenge_api/tests/units/exceptions/__init__.py b/challenge_api/tests/units/exceptions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/exceptions/conftest.py b/challenge_api/tests/units/exceptions/conftest.py similarity index 72% rename from tests/units/exceptions/conftest.py rename to challenge_api/tests/units/exceptions/conftest.py index c4ce273..5c62261 100644 --- a/tests/units/exceptions/conftest.py +++ b/challenge_api/tests/units/exceptions/conftest.py @@ -2,9 +2,9 @@ from flask import Flask import pytest -from hexactf.exceptions.api_exceptions import InternalServerError, InvalidRequest -from hexactf.exceptions.handlers import register_error_handler -from hexactf.factory import create_app +from exceptions.api_exceptions import InternalServerError, InvalidRequest +from exceptions.handlers import register_error_handler +from factory import create_app def create_app(): app = Flask(__name__) diff --git a/tests/units/exceptions/test_handlers.py b/challenge_api/tests/units/exceptions/test_handlers.py similarity index 91% rename from tests/units/exceptions/test_handlers.py rename to challenge_api/tests/units/exceptions/test_handlers.py index d8c0574..7666eb0 100644 --- a/tests/units/exceptions/test_handlers.py +++ b/challenge_api/tests/units/exceptions/test_handlers.py @@ -1,4 +1,4 @@ -from hexactf.exceptions.error_types import ApiErrorTypes +from exceptions.error_types import ApiErrorTypes def test_invalid_request_handler(client): diff --git a/challenge_api/tests/units/kafka/__init__.py b/challenge_api/tests/units/kafka/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/units/kafka/conftest.py b/challenge_api/tests/units/kafka/conftest.py similarity index 87% rename from tests/units/kafka/conftest.py rename to challenge_api/tests/units/kafka/conftest.py index 4a9c423..b62247e 100644 --- a/tests/units/kafka/conftest.py +++ b/challenge_api/tests/units/kafka/conftest.py @@ -1,9 +1,9 @@ import pytest import json from unittest.mock import MagicMock, patch -from hexactf.exceptions.kafka_exceptions import QueueProcessingError +from exceptions.kafka_exceptions import QueueProcessingError from kafka import KafkaConsumer -from hexactf.extensions.kafka.consumer import KafkaEventConsumer # Ensure it's patched correctly +from extensions.kafka.consumer import KafkaEventConsumer # Ensure it's patched correctly @pytest.fixture def sample_json(): diff --git a/tests/units/kafka/test_consumer.py b/challenge_api/tests/units/kafka/test_consumer.py similarity index 91% rename from tests/units/kafka/test_consumer.py rename to challenge_api/tests/units/kafka/test_consumer.py index deb9b14..aedf801 100644 --- a/tests/units/kafka/test_consumer.py +++ b/challenge_api/tests/units/kafka/test_consumer.py @@ -2,8 +2,8 @@ import pytest -from hexactf.exceptions.kafka_exceptions import QueueProcessingError -from hexactf.extensions.kafka.consumer import KafkaEventConsumer +from exceptions.kafka_exceptions import QueueProcessingError +from extensions.kafka.consumer import KafkaEventConsumer def test_consumer_initialization(kafka_event_consumer, kafka_mock): diff --git a/challenge_api/tests/units/utils/test_namebuilder.py b/challenge_api/tests/units/utils/test_namebuilder.py new file mode 100644 index 0000000..ee4319f --- /dev/null +++ b/challenge_api/tests/units/utils/test_namebuilder.py @@ -0,0 +1,100 @@ +import pytest +from unittest.mock import Mock, patch + +from utils.namebuilder import ChallengeNameSet, NameBuilder + +@pytest.fixture +def mock_data(): + return { + 'challenge_id': 123, + 'user_id': 456, + 'expected_name': 'challenge-123-456' + } + +@pytest.fixture +def mock_challenge_name_set(): + with patch('utils.namebuilder.ChallengeNameSet') as mock: + mock_instance = Mock() + mock.return_value = mock_instance + yield mock, mock_instance + +def test_challenge_name_set(mock_data, mock_challenge_name_set): + # Given + mock_class, mock_instance = mock_challenge_name_set + mock_instance.name = mock_data['expected_name'] + mock_instance.is_valid_name.return_value = True + + # When + name_set = ChallengeNameSet( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + + # Then + assert name_set.name == mock_data['expected_name'] + assert name_set.is_valid_name() is True + mock_class.assert_called_once_with( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + +def test_challenge_name_set_invalid(mock_data, mock_challenge_name_set): + # Given + mock_class, mock_instance = mock_challenge_name_set + mock_instance.name = mock_data['expected_name'] + mock_instance.is_valid_name.return_value = False + + # When + name_set = ChallengeNameSet( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + + # Then + assert name_set.is_valid_name() is False + mock_class.assert_called_once_with( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + +def test_name_builder(mock_data, mock_challenge_name_set): + # Given + mock_class, mock_instance = mock_challenge_name_set + mock_instance.name = mock_data['expected_name'] + mock_instance.is_valid_name.return_value = True + + # When + builder = NameBuilder( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + result = builder.build() + + # Then + assert result is not None + assert result.name == mock_data['expected_name'] + assert result.is_valid_name() is True + mock_class.assert_called_once_with( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + +def test_name_builder_invalid(mock_data, mock_challenge_name_set): + # Given + mock_class, mock_instance = mock_challenge_name_set + mock_instance.name = mock_data['expected_name'] + mock_instance.is_valid_name.return_value = False + + # When + builder = NameBuilder( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) + result = builder.build() + + # Then + assert result is None + mock_class.assert_called_once_with( + challenge_id=mock_data['challenge_id'], + user_id=mock_data['user_id'] + ) \ No newline at end of file diff --git a/challenge_api/utils/__init__.py b/challenge_api/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/challenge_api/utils/__pycache__/__init__.cpython-310.pyc b/challenge_api/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3aa0a95 Binary files /dev/null and b/challenge_api/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/challenge_api/utils/__pycache__/api_decorators.cpython-310.pyc b/challenge_api/utils/__pycache__/api_decorators.cpython-310.pyc new file mode 100644 index 0000000..98d821a Binary files /dev/null and b/challenge_api/utils/__pycache__/api_decorators.cpython-310.pyc differ diff --git a/challenge_api/utils/__pycache__/namebuilder.cpython-310.pyc b/challenge_api/utils/__pycache__/namebuilder.cpython-310.pyc new file mode 100644 index 0000000..64d4435 Binary files /dev/null and b/challenge_api/utils/__pycache__/namebuilder.cpython-310.pyc differ diff --git a/challenge_api/utils/api_decorators.py b/challenge_api/utils/api_decorators.py new file mode 100644 index 0000000..a2753d8 --- /dev/null +++ b/challenge_api/utils/api_decorators.py @@ -0,0 +1,27 @@ +from functools import wraps +from flask import request +from challenge_api.exceptions.api_exceptions import InvalidRequest + +def validate_request_body(*required_fields): + """ + Request body의 유효성을 검사하는 데코레이터 + + Args: + required_fields: 필수로 존재해야 하는 필드명들 + """ + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + res = request.get_json() + if not res: + raise InvalidRequest(error_msg="Request body is empty or not valid JSON") + + for field in required_fields: + if field not in res: + raise InvalidRequest(error_msg=f"Required field '{field}' is missing in request") + if not res[field]: + raise InvalidRequest(error_msg=f"'{field}' is empty or not valid") + + return f(*args, **kwargs) + return decorated_function + return decorator \ No newline at end of file diff --git a/challenge_api/utils/namebuilder.py b/challenge_api/utils/namebuilder.py new file mode 100644 index 0000000..ce912d6 --- /dev/null +++ b/challenge_api/utils/namebuilder.py @@ -0,0 +1,31 @@ +from typing import Optional +import re + +from challenge_api.objects.challenge_info import ChallengeInfo + +class NameBuilder: + def __init__(self, challenge_id: int, user_id: int): + self._challenge_id = challenge_id + self._user_id = user_id + + def _is_valid_name(self, name:str) -> bool: + """Kubernetes 리소스 이름 유효성 검사""" + name = name.lower() + if not name or len(name) > 253: + return False + pattern = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' + return bool(re.match(pattern, name)) + + def build(self) -> Optional[ChallengeInfo]: + """ + 챌린지 이름 빌더 + + """ + # TODO: Error Handling 추가 필요 + challenge_info = ChallengeInfo(challenge_id=self._challenge_id, user_id=self._user_id) + if not self._is_valid_name(challenge_info.name): + return None + return challenge_info + + + \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 07e2815..4a1348d 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -115,7 +115,7 @@ services: read_only: true environment: DEVELOPMENT: "true" - METRICS_ADDR: ":8081" + METRICS_ADDR: ":8079" KAFKA_BROKERS: "localhost:9093" LOG_LEVEL: "debug" KUBECONFIG: "/home/nonroot/.kube/config" diff --git a/md/user-challenge-delete.md b/docs/user-challenge-delete.md similarity index 100% rename from md/user-challenge-delete.md rename to docs/user-challenge-delete.md diff --git a/md/user-challenge-status.md b/docs/user-challenge-status.md similarity index 100% rename from md/user-challenge-status.md rename to docs/user-challenge-status.md diff --git a/md/user-challenge.md b/docs/user-challenge.md similarity index 100% rename from md/user-challenge.md rename to docs/user-challenge.md diff --git a/hexactf/__pycache__/__init__.cpython-310.pyc b/hexactf/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index b6c39ee..0000000 Binary files a/hexactf/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/__pycache__/factory.cpython-310.pyc b/hexactf/__pycache__/factory.cpython-310.pyc deleted file mode 100644 index f493417..0000000 Binary files a/hexactf/__pycache__/factory.cpython-310.pyc and /dev/null differ diff --git a/hexactf/api/__pycache__/__init__.cpython-310.pyc b/hexactf/api/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 7f4f927..0000000 Binary files a/hexactf/api/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/api/__pycache__/challenge_api.cpython-310.pyc b/hexactf/api/__pycache__/challenge_api.cpython-310.pyc deleted file mode 100644 index 47b0b8d..0000000 Binary files a/hexactf/api/__pycache__/challenge_api.cpython-310.pyc and /dev/null differ diff --git a/hexactf/api/challenge_api.py b/hexactf/api/challenge_api.py deleted file mode 100644 index efca40c..0000000 --- a/hexactf/api/challenge_api.py +++ /dev/null @@ -1,95 +0,0 @@ -from json import JSONDecodeError -from logging import log -from flask import Blueprint, jsonify, request -import yaml - -from hexactf.exceptions.api_exceptions import InvalidRequest -from hexactf.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError, UserChallengeNotFoundError -from hexactf.extensions.db.repository import UserChallengesRepository -from hexactf.extensions.k8s.client import K8sClient - -challenge_bp = Blueprint('challenge', __name__) - -@challenge_bp.route('', methods=['POST']) -def create_challenge(): - """사용자 챌린지 생성""" - # Challenge 관련 정보 가져오기 - res = request.get_json() - if not res: - 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 'username' not in res: - raise InvalidRequest(error_msg="Required field 'username' is missing in request") - username = res['username'] - # 챌린지 생성 - 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} : Endpoint did not exist") - - return jsonify({'data' : {'port': endpoint}}), 200 - - -@challenge_bp.route('/delete', methods=['POST']) -def delete_userchallenges(): - try: - """ - 사용자 챌린지 삭제 - """ - # Challenge 관련 정보 가져오기 - res = request.get_json() - if not res: - raise UserChallengeDeletionError(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 'username' not in res: - raise InvalidRequest(error_msg="Required field 'username' is missing in request") - username = res['username'] - - # 사용자 챌린지 삭제 - client = K8sClient() - client.delete_userchallenge(username, challenge_id) - - return jsonify({'message' : '챌린지가 정상적으로 삭제되었습니다.'}), 200 - except JSONDecodeError as e: - log.error("Invalid request format") - raise InvalidRequest(error_msg=str(e)) from e - -@challenge_bp.route('/status', methods=['POST']) -def get_userchallenge_status(): - """ 사용자 챌린지 상태 조회 """ - try: - # Challenge 관련 정보 가져오기 - res = request.get_json() - if not res: - 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="'username' is empty or not valid") - - # 사용자 챌린지 상태 조회 - repo = UserChallengesRepository() - status_dict = repo.get_status(challenge_id, username) - if status_dict is None: - raise UserChallengeNotFoundError(error_msg=f"User challenge not found for {username} and {challenge_id}") - return jsonify({'data': status_dict}), 200 - except Exception as e: - raise UserChallengeNotFoundError(error_msg=str(e)) \ No newline at end of file diff --git a/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc b/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index a2667fa..0000000 Binary files a/hexactf/exceptions/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 7046e17..0000000 Binary files a/hexactf/extensions/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 674d459..0000000 Binary files a/hexactf/extensions/db/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/db/__pycache__/models.cpython-310.pyc b/hexactf/extensions/db/__pycache__/models.cpython-310.pyc deleted file mode 100644 index 07f2619..0000000 Binary files a/hexactf/extensions/db/__pycache__/models.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/db/repository.py b/hexactf/extensions/db/repository.py deleted file mode 100644 index f2ada3f..0000000 --- a/hexactf/extensions/db/repository.py +++ /dev/null @@ -1,151 +0,0 @@ -import logging -import os -from typing import Optional -from sqlalchemy.exc import SQLAlchemyError -from sqlalchemy.sql import text -from hexactf.exceptions.api_exceptions import InternalServerError -from hexactf.extensions_manager import db -from hexactf.extensions.db.models import Challenges, UserChallenges - -class UserChallengesRepository: - def __init__(self, session=None): - self.session = session or db.session - - def create(self, username: str, C_idx: int, userChallengeName: str, - port: int, status: str = 'None') -> Optional[UserChallenges]: - """ - 새로운 사용자 챌린지 생성 - - Args: - username (str): 사용자 이름 - C_idx (int): 챌린지 ID - userChallengeName (str): 챌린지 이름 - port (int): 챌린지 포트 - status (str): 챌린지 상태 - - Returns: - UserChallenges: 생성된 챌린지 - """ - try: - challenge = UserChallenges( - username=username, - C_idx=C_idx, - userChallengeName=userChallengeName, - port=port, - status=status - ) - self.session.add(challenge) - self.session.commit() - return challenge - except SQLAlchemyError as e: - - self.session.rollback() - raise InternalServerError(error_msg=f"Error creating challenge in db: {e}") from e - - def get_by_user_challenge_name(self, userChallengeName: str) -> Optional[UserChallenges]: - """ - 사용자 챌린지 이름 조회 - - Args: - userChallengeName (str): 사용자 챌린지 이름 - - Returns: - UserChallenges: 사용자 챌린지 - """ - user_challenge = UserChallenges.query.filter_by(userChallengeName=userChallengeName).first() - if not user_challenge: - return None - return user_challenge - - - def update_status(self, challenge: UserChallenges, new_status: str) -> bool: - """ - 사용자 챌린지 상태 업데이트 - - Args: - challenge (UserChallenges): 사용자 챌린지 - new_status (str): 새로운 상태 - - Returns: - bool: 업데이트 성공 여부 - """ - try: - if os.getenv("TEST_MODE") != "true": - db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - fresh_challenge = self.session.merge(challenge) - self.session.refresh(fresh_challenge) - fresh_challenge.status = new_status - self.session.commit() - return True - except SQLAlchemyError as e: - self.session.rollback() - raise InternalServerError(error_msg=f"Error updating challenge status: {e}") from e - - - def update_port(self, challenge: UserChallenges, port: int) -> bool: - """ - 챌린지 포트 업데이트 - - Args: - challenge (UserChallenges): 사용자 챌린지 - port (int): 새로운 포트 - - Returns: - bool: 업데이트 성공 여부 - """ - try: - db.session.execute(text("SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED")) - fresh_challenge = self.session.merge(challenge) - self.session.refresh(fresh_challenge) - fresh_challenge.port = port - self.session.commit() - return True - except SQLAlchemyError as e: - db.session.rollback() - raise InternalServerError(error_msg=f"Error updating challenge port: {e}") from e - def is_running(self, challenge: UserChallenges) -> bool: - """ - 챌린지 실행 여부 확인 - - Args: - challenge (UserChallenges): 사용자 챌린지 - - Returns: - bool: 챌린지 실행 여부 - """ - return challenge.status == 'Running' - - def get_status(self, challenge_id, username) -> Optional[dict]: - """ - 챌린지 상태 조회 - - Args: - challenge_id (int): 챌린지 아이디 - username (str): 사용자 이름 - - Returns: - str: 챌린지 상태 - """ - challenge = UserChallenges.query.filter_by(C_idx=challenge_id, username=username).first() - if not challenge: - return None - - if challenge.status == 'Running': - return {'status': challenge.status, 'port': int(challenge.port)} - return {'status': challenge.status} - - -class ChallengeRepository: - @staticmethod - def get_challenge_name(challenge_id: int) -> Optional[str]: - """ - 챌린지 아이디로 챌린지 조회 - - Args: - challenge_id (int): 챌린지 아이디 - - Returns: - str: 챌린지 이름 - """ - challenge = Challenges.query.get(challenge_id) - return challenge.title if challenge else None \ No newline at end of file diff --git a/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 804a337..0000000 Binary files a/hexactf/extensions/k8s/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc b/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc deleted file mode 100644 index 4b80942..0000000 Binary files a/hexactf/extensions/k8s/__pycache__/client.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/k8s/client.py b/hexactf/extensions/k8s/client.py deleted file mode 100644 index 59e189c..0000000 --- a/hexactf/extensions/k8s/client.py +++ /dev/null @@ -1,217 +0,0 @@ -import os -import re -import time - -from kubernetes import client, config - -from hexactf.exceptions.challenge_exceptions import ChallengeNotFound -from hexactf.exceptions.userchallenge_exceptions import UserChallengeCreationError, UserChallengeDeletionError -from hexactf.extensions.db.repository import ChallengeRepository, UserChallengesRepository - - -MAX_RETRIES = 3 -SLEEP_INTERVAL = 2 - - -class K8sClient: - """ - Kubernetes Custom Resource 관리를 위한 클라이언트 클래스 - - Challenge Custom Resource를 생성, 삭제하고 상태를 관리합니다. - 클러스터 내부 또는 외부에서 실행될 수 있도록 설정을 자동으로 로드합니다. - """ - - def __init__(self): - try: - config.load_incluster_config() - except config.ConfigException: - config.load_kube_config() - - 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: - 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 - ) - - time.sleep(5) - # status 값 가져오기 - status = challenge.get('status', {}) - endpoint = status.get('endpoint') - - # 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['metadata']['name'] - ) - status = challenge.get('status', {}) - endpoint = status.get('endpoint') - - # NodePort 업데이트 - 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)) - 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: - """ - Kubernetes 리소스 이름을 유효한 형식으로 변환 (소문자 + 공백을 하이픈으로 변경) - - Args: - name (str): 원본 이름 - - Returns: - str: 변환된 Kubernetes 리소스 이름 - """ - if not name or len(name) > 253: - raise ValueError("이름이 비어있거나 길이가 253자를 초과함") - - name = name.lower() - name = re.sub(r'[^a-z0-9-]+', '-', name) - name = re.sub(r'-+', '-', name) - name = name.strip('-') - - # 최종 길이 검사 (1~253자) - if not name or len(name) > 253: - raise ValueError(f"변환 후에도 유효하지 않은 Kubernetes 리소스 이름: {name}") - - return name - - def _is_valid_k8s_name(self, name: str) -> bool: - """ - Kubernetes 리소스 이름 유효성 검사 - - Args: - name (str): 검사할 이름 - - Returns: - bool: 유효한 이름인지 여부 - """ - - # 소문자로 변환 - name = name.lower() - - # Kubernetes naming convention 검사 - if not name or len(name) > 253: - return False - - # DNS-1123 label 규칙 검사 - import re - pattern = r'^[a-z0-9]([-a-z0-9]*[a-z0-9])?$' - return bool(re.match(pattern, name)) - - def delete_userchallenge(self, username, challenge_id, namespace="default"): - """ - Challenge Custom Resource를 삭제합니다. - - Args: - username (str): Challenge를 생성한 사용자 이름 - challenge_id (str): 삭제할 Challenge의 ID - namespace (str): Challenge가 생성된 네임스페이스 (기본값: "default") - - Raises: - UserChallengeDeletionError: Challenge 삭제에 실패했을 때 - """ - - # UserChallenge 조회 - challenge_name = f"challenge-{challenge_id}-{username.lower()}" - challenge_name = self._normalize_k8s_name(challenge_name) - user_challenge_repo = UserChallengesRepository() - user_challenge = user_challenge_repo.get_by_user_challenge_name(challenge_name) - if not user_challenge: - raise UserChallengeDeletionError(error_msg=f"Deletion : UserChallenge not found: {challenge_name}") - - # 사용자 챌린지(컨테이너) 삭제 - try: - self.custom_api.delete_namespaced_custom_object( - group="apps.hexactf.io", - version="v1alpha1", - namespace=namespace, - plural="challenges", - name=challenge_name - ) - - except Exception as e: - raise UserChallengeDeletionError(error_msg=str(e)) from e - diff --git a/hexactf/extensions/kafka/__init__.py b/hexactf/extensions/kafka/__init__.py deleted file mode 100644 index 04c5eca..0000000 --- a/hexactf/extensions/kafka/__init__.py +++ /dev/null @@ -1,9 +0,0 @@ -__version__ = '1.0.0' - -__all__ = [ - 'KafkaEventConsumer', - 'KafkaConfig', -] - -from hexactf.extensions.kafka.config import KafkaConfig -from hexactf.extensions.kafka.consumer import KafkaEventConsumer diff --git a/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc b/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index 7f77c3e..0000000 Binary files a/hexactf/extensions/kafka/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/extensions/kafka/handler.py b/hexactf/extensions/kafka/handler.py deleted file mode 100644 index 5d045b5..0000000 --- a/hexactf/extensions/kafka/handler.py +++ /dev/null @@ -1,68 +0,0 @@ -import logging -from typing import Any, Dict -from hexactf.exceptions.kafka_exceptions import QueueProcessingError -from hexactf.extensions.db.repository import UserChallengesRepository - -logger = logging.getLogger(__name__) - -class MessageHandler: - VALID_STATUSES = {'Creating', 'Running', 'Deleted', 'Error'} - - @staticmethod - def validate_message(message: Dict[str, Any]) -> tuple[str, str, str, str]: - """ - Kafka 메세지의 필수 필드를 검증하고 반환 - - Args: - message (Dict[str, Any]): Kafka 메세지 - - Returns: - tuple[str, str, str, str]: 사용자 이름, 챌린지 ID, 새로운 상태, 타임스탬프 - """ - try: - username = message.user - problem_id = message.problemId - new_status = message.newStatus - timestamp = message.timestamp - except AttributeError: - username = message['user'] - problem_id = message['problemId'] - new_status = message['newStatus'] - timestamp = message['timestamp'] - - if not all([username, problem_id, new_status, timestamp]): - raise QueueProcessingError(error_msg=f"Kafka Error : Missing required fields in message: {message}") - - if new_status not in MessageHandler.VALID_STATUSES: - raise QueueProcessingError(error_msg=f"Kafka Error : Invalid status in message: {new_status}") - - return username, problem_id, new_status, timestamp - - @staticmethod - def handle_message(message: Dict[str, Any]): - """ - Consume한 Kafka message 내용을 DB에 반영 - - Args: - message: Kafka 메세지 - """ - try: - username, challenge_id, new_status, _ = MessageHandler.validate_message(message) - challenge_name = challenge_name = f"challenge-{challenge_id}-{username.lower()}" - - # 상태 정보 업데이트 - repo = UserChallengesRepository() - user_challenge = repo.get_by_user_challenge_name(challenge_name) - logger.debug(f"Found user challenge {challenge_name} : {user_challenge}") - if user_challenge is None: - repo.create(username, challenge_id, challenge_name, 0, new_status) - logger.debug(f"Updating status of challenge {challenge_name} to {new_status}") - - success = repo.update_status(user_challenge, new_status) - if not success: - raise QueueProcessingError(error_msg=f"Kafka Error : Failed to update challenge status: {challenge_name}") - - except ValueError as e: - raise QueueProcessingError(error_msg=f"Invalid message format {str(e)}") from e - except Exception as e: - raise QueueProcessingError(error_msg=f"Kafka Error: {str(e)}") from e diff --git a/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc b/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc deleted file mode 100644 index b714999..0000000 Binary files a/hexactf/monitoring/__pycache__/__init__.cpython-310.pyc and /dev/null differ diff --git a/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc b/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc deleted file mode 100644 index f039ded..0000000 Binary files a/hexactf/monitoring/__pycache__/loki_logger.cpython-310.pyc and /dev/null differ diff --git a/hexactf/monitoring/loki_logger.py b/hexactf/monitoring/loki_logger.py deleted file mode 100644 index 43a7b81..0000000 --- a/hexactf/monitoring/loki_logger.py +++ /dev/null @@ -1,102 +0,0 @@ -import time -import json -import logging -import traceback - -from hexactf.monitoring.async_handler import AsyncHandler -from logging_loki import LokiHandler - -class FlaskLokiLogger: - def __init__(self, app_name,loki_url: str): - self.app_name = app_name - self.logger = self._setup_logger(loki_url) - - def _setup_logger(self, loki_url: str) -> logging.Logger: - """Loki 로거 설정""" - tags = { - "app": self.app_name - } - - handler = LokiHandler( - url=loki_url, - tags=tags, - version="1", - ) - - handler.setFormatter(LokiJsonFormatter()) - async_handler = AsyncHandler(handler) - logger = logging.getLogger(self.app_name) - logger.setLevel(logging.DEBUG) - logger.addHandler(async_handler) - return logger - - - -class LokiJsonFormatter(logging.Formatter): - def format(self, record): - try: - # 현재 타임스탬프 (나노초 단위) - timestamp_ns = str(int(time.time() * 1e9)) - - # record에서 직접 labels와 content 추출 - # tags = getattr(record, 'tags', {}) - content = getattr(record, 'content', {}) - - # 기본 로그 정보 추가 - base_content = { - "message": record.getMessage(), - "level": record.levelname, - } - - # 예외 정보 추가 (있는 경우) - if record.exc_info: - base_content["exception"] = { - "type": str(record.exc_info[0]), - "message": str(record.exc_info[1]), - "traceback": traceback.format_exception(*record.exc_info) - } - - - # content에 기본 로그 정보 병합 - full_content = {**base_content, **content} - - # 로그 구조 생성 - log_entry = { - "timestamp": timestamp_ns, - "content": full_content - } - - # JSON으로 변환 - return json.dumps(log_entry, ensure_ascii=False, default=str) - - except Exception as e: - # 포맷팅 중 오류 발생 시 대체 로그 - fallback_entry = { - "timestamp": str(int(time.time() * 1e9)), - "labels": {"error": "FORMATTING_FAILURE"}, - "content": { - "original_message": record.getMessage(), - "formatting_error": str(e), - "record_details": str(getattr(record, '__dict__', 'No __dict__')) - } - } - return json.dumps(fallback_entry) - - def _serialize_dict(self, data, max_depth=3, current_depth=0): - """재귀적으로 dict을 직렬화""" - if current_depth >= max_depth: - return "" - if isinstance(data, dict): - return { - key: self._serialize_dict(value, max_depth, current_depth + 1) - for key, value in data.items() - } - elif isinstance(data, (list, tuple, set)): - return [ - self._serialize_dict(item, max_depth, current_depth + 1) - for item in data - ] - elif hasattr(data, "__dict__"): - return self._serialize_dict(data.__dict__, max_depth, current_depth + 1) - else: - return data \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7a84812..857b425 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,4 +13,6 @@ python-logging-loki==0.3.1 flask-prometheus-metrics==1.0.0 psutil +pydantic +pytest pytest-mock==3.14.0 diff --git a/sample/ubuntu-challenge.yaml b/sample/ubuntu-challenge.yaml index 8a35f1c..6f09f0d 100644 --- a/sample/ubuntu-challenge.yaml +++ b/sample/ubuntu-challenge.yaml @@ -1,13 +1,11 @@ -apiVersion: apps.hexactf.io/v1alpha1 +apiVersion: apps.hexactf.io/v2alpha1 kind: Challenge metadata: name: ubuntu-instance-1 - namespace: default + namespace: challenge labels: apps.hexactf.io/challengeId: "1" - apps.hexactf.io/user: "test" + apps.hexactf.io/userId: "1" spec: - # Challenge가 생성될 namespace - namespace: default # 사용할 ChallengeDefinition의 이름 definition: ubuntu-basic \ No newline at end of file diff --git a/tests/units/db/test_userchallenges_repo.py b/tests/units/db/test_userchallenges_repo.py deleted file mode 100644 index a571b90..0000000 --- a/tests/units/db/test_userchallenges_repo.py +++ /dev/null @@ -1,173 +0,0 @@ -import pytest -from hexactf.exceptions.api_exceptions import InternalServerError -from hexactf.extensions.db.models import UserChallenges -from hexactf.extensions.db.repository import UserChallengesRepository - -# ============================ -# create_challenge -# ============================ - -def test_create_challenge_success(test_db): - """Test successful challenge creation""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = user_challenges_repo.create( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000 - ) - - assert challenge is not None - assert challenge.username == "test_user" - assert challenge.C_idx == 1 - assert challenge.userChallengeName == "test_challenge" - assert challenge.port == 30000 - -def test_create_challenge_None_value_failed(test_db): - """Test challenge creation failure due to database error""" - - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - with pytest.raises(InternalServerError): - user_challenges_repo.create( - username=None, - C_idx=1, - userChallengeName="test_challenge", - port=30000 - ) - -# ============================ -# get_by_user_challenge_name -# ============================ - -def test_get_by_user_challenge_name_success(test_db): - """Test retrieving a challenge by name""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000 - ) - session.add(challenge) - session.commit() - - result = user_challenges_repo.get_by_user_challenge_name("test_challenge") - assert result == challenge, "Challenge should be retrieved by name" - -def test_get_by_user_challenge_name_Invalid_name_failed(test_db): - """Test retrieving a challenge by name""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000 - ) - session.add(challenge) - session.commit() - - result = user_challenges_repo.get_by_user_challenge_name("invalid_challenge") - assert result is None, "Invalid challenge name should return None" - - -# ============================ -# get_by_user_challenge_name -# ============================ - -def test_update_status_success(test_db): - """Test updating challenge status""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000, - status="Running" - ) - session.add(challenge) - session.commit() - - success = user_challenges_repo.update_status(challenge, "Deleted") - assert success - assert challenge.status == "Deleted" - -# ============================ -# is_running -# ============================ - -def test_is_running_success(test_db): - """Test checking if challenge is running""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000, - status="Running" - ) - - assert user_challenges_repo.is_running(challenge) - challenge.status = "Stopped" - assert not user_challenges_repo.is_running(challenge) - -# ============================ -# get_status -# ============================ - -def test_get_status_success(test_db): - """Test retrieving challenge status""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000, - status="Running" - ) - session.add(challenge) - session.commit() - - result = user_challenges_repo.get_status(1, "test_user") - assert result == {"status": "Running", "port": 30000} - - challenge.status = "Deleted" - session.commit() - result = user_challenges_repo.get_status(1, "test_user") - assert result == {"status": "Deleted"} - -def test_get_status_invalid_challenge_failed(test_db): - """Test retrieving challenge status""" - session = test_db.get_session() - user_challenges_repo = UserChallengesRepository(session=session) - - challenge = UserChallenges( - username="test_user", - C_idx=1, - userChallengeName="test_challenge", - port=30000, - status="Running" - ) - session.add(challenge) - session.commit() - - # 잘못된 challenge id - result = user_challenges_repo.get_status(2, "test_user") - assert result is None - - # 잘못된 username - result2 = user_challenges_repo.get_status(1, "wrong_user") - assert result2 is None \ No newline at end of file