Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions src/app/project/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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)",
)


Expand All @@ -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, 늘리기만 가능)",
)


Expand Down
25 changes: 25 additions & 0 deletions src/common/config/resource_limit.py
Original file line number Diff line number Diff line change
@@ -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()
44 changes: 8 additions & 36 deletions src/infra/client/project_resource_impl.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down Expand Up @@ -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:
Expand All @@ -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}"
)
Expand Down Expand Up @@ -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()
12 changes: 6 additions & 6 deletions tests/test_project_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
Loading