diff --git a/src/app/project/schemas.py b/src/app/project/schemas.py index db0434f..20b4c42 100644 --- a/src/app/project/schemas.py +++ b/src/app/project/schemas.py @@ -3,8 +3,11 @@ from pydantic import BaseModel, ConfigDict, Field, constr +from src.common.config.resource_limit import get_resource_limit_config from src.core.client.application import ApplicationHealthStatus +_limits = get_resource_limit_config() + class ProjectMemberRole(str): OWNER = "OWNER" MEMBER = "MEMBER" @@ -19,20 +22,20 @@ class ProjectCreate(BaseModel): max_cpu: float = Field( 0.1, ge=0.1, - le=4.0, + le=_limits.max_cpu, description="vCPU 기준 (기본 0.1v)", ) max_memory: float = Field( 0.5, ge=0.5, - le=1.0, - description="GB 단위 (512MB ~ 1024MB, 기본 0.5GB)", + le=_limits.max_memory, + description=f"GB 단위 (512MB ~ {int(_limits.max_memory * 1024)}MB, 기본 0.5GB)", ) max_disk: float = Field( 2.0, ge=2.0, - le=50.0, - description="GB 단위 (2GB ~ 50GB, 기본 2GB)", + le=_limits.max_disk, + description=f"GB 단위 (2GB ~ {int(_limits.max_disk)}GB, 기본 2GB)", ) @@ -58,20 +61,20 @@ class ProjectResourceUpdate(BaseModel): max_cpu: float | None = Field( None, ge=0.1, - le=4.0, + le=_limits.max_cpu, description="vCPU 기준 (0.1v ~ 4v)", ) max_memory: float | None = Field( None, ge=0.5, - le=1.0, - description="GB 단위 (512MB ~ 1024MB)", + le=_limits.max_memory, + description=f"GB 단위 (512MB ~ {int(_limits.max_memory * 1024)}MB)", ) max_disk: float | None = Field( None, ge=2.0, - le=50.0, - description="GB 단위 (2GB ~ 50GB, 늘리기만 가능)", + le=_limits.max_disk, + description=f"GB 단위 (2GB ~ {int(_limits.max_disk)}GB, 늘리기만 가능)", ) diff --git a/src/common/config/resource_limit.py b/src/common/config/resource_limit.py new file mode 100644 index 0000000..dbd0842 --- /dev/null +++ b/src/common/config/resource_limit.py @@ -0,0 +1,25 @@ +from functools import lru_cache + +from pydantic_settings import SettingsConfigDict + +from src.common.config.settings import LoggedSettings, register_config +from src.common.const.vault import VAULT_ENV_FILE + + +class ResourceLimitConfig(LoggedSettings): + model_config = SettingsConfigDict( + env_prefix="PROJECT_RESOURCE_", + extra="ignore", + env_file=VAULT_ENV_FILE, + env_file_encoding="utf-8", + ) + + max_cpu: float = 4.0 + max_memory: float = 2.0 + max_disk: float = 50.0 + + +@register_config +@lru_cache +def get_resource_limit_config() -> ResourceLimitConfig: + return ResourceLimitConfig() diff --git a/src/infra/client/project_resource_impl.py b/src/infra/client/project_resource_impl.py index 80968b6..709b69c 100644 --- a/src/infra/client/project_resource_impl.py +++ b/src/infra/client/project_resource_impl.py @@ -48,23 +48,12 @@ async def _request( try: response = await coro except Exception as e: - logger.exception( - "[ProjectResourceClient] %s 요청 예외 발생: project_id=%s, url=%s", - operation, - project_id, - url, - ) + logger.error(f"리소스 서버 연결 실패: {self.base_url}, error: {e}") raise ResourceServerException() from e - - try: - status = getattr(response, "status", None) - ok = getattr(response, "ok", status is not None and 200 <= status < 400) - logger.info( - "[ProjectResourceClient] %s 응답: status=%s, project_id=%s, url=%s", - operation, - status, - project_id, - url, + if not response.ok: + logger.error(f"리소스 서버 응답 오류: {self.base_url}, status: {response.status}") + raise ResourceServerException( + f"리소스 서버 응답 오류: {response.status}" ) if not ok: body = "" @@ -189,11 +178,7 @@ async def get_usage( try: response = await self.http_client.get(url) except Exception as e: - logger.exception( - "[ProjectResourceClient] get_usage 요청 예외 발생: project_id=%s, url=%s", - project.id, - url, - ) + logger.error(f"리소스 서버 연결 실패: {self.base_url}, error: {e}") raise ResourceServerException() from e try: @@ -204,16 +189,7 @@ async def get_usage( url, ) if response.status != 200: - body = "" - if hasattr(response, "text"): - body = await response.text() - logger.warning( - "[ProjectResourceClient] get_usage 비정상 응답: status=%s, project_id=%s, url=%s, body=%s", - response.status, - project.id, - url, - body[:500], - ) + logger.error(f"리소스 서버 응답 오류: {self.base_url}, status: {response.status}") raise ResourceServerException( f"리소스 서버 응답 오류: {response.status}" ) @@ -253,11 +229,7 @@ async def get_usage( except ResourceServerException: raise except Exception as e: - logger.exception( - "[ProjectResourceClient] get_usage 응답 처리 예외 발생: project_id=%s, url=%s", - project.id, - url, - ) + logger.error(f"리소스 서버 응답 처리 오류: {self.base_url}, error: {e}") raise ResourceServerException() from e finally: response.release() diff --git a/tests/test_project_schemas.py b/tests/test_project_schemas.py index 734b10b..6d1f1c0 100644 --- a/tests/test_project_schemas.py +++ b/tests/test_project_schemas.py @@ -14,17 +14,17 @@ def test_project_create_accepts_memory_and_disk_boundaries() -> None: maximum = ProjectCreate( name="beta", max_cpu=1.0, - max_memory=1.0, + max_memory=2.0, max_disk=50.0, ) assert minimum.max_memory == 0.5 assert minimum.max_disk == 2.0 - assert maximum.max_memory == 1.0 + assert maximum.max_memory == 2.0 assert maximum.max_disk == 50.0 -@pytest.mark.parametrize("max_memory", [0.49, 1.01]) +@pytest.mark.parametrize("max_memory", [0.49, 2.01]) def test_project_create_rejects_out_of_range_memory(max_memory: float) -> None: with pytest.raises(ValidationError): ProjectCreate( @@ -48,15 +48,15 @@ def test_project_create_rejects_out_of_range_disk(max_disk: float) -> None: def test_project_resource_update_accepts_memory_and_disk_boundaries() -> None: minimum = ProjectResourceUpdate(max_memory=0.5, max_disk=2.0) - maximum = ProjectResourceUpdate(max_memory=1.0, max_disk=50.0) + maximum = ProjectResourceUpdate(max_memory=2.0, max_disk=50.0) assert minimum.max_memory == 0.5 assert minimum.max_disk == 2.0 - assert maximum.max_memory == 1.0 + assert maximum.max_memory == 2.0 assert maximum.max_disk == 50.0 -@pytest.mark.parametrize("max_memory", [0.49, 1.01]) +@pytest.mark.parametrize("max_memory", [0.49, 2.01]) def test_project_resource_update_rejects_out_of_range_memory(max_memory: float) -> None: with pytest.raises(ValidationError): ProjectResourceUpdate(max_memory=max_memory)