Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add resource class id in session launcher #249

Merged
merged 6 commits into from
Jul 1, 2024
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
4 changes: 3 additions & 1 deletion components/renku_data_services/app_config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ def group_repo(self) -> GroupRepository:
def session_repo(self) -> SessionRepository:
"""The DB adapter for sessions."""
if not self._session_repo:
self._session_repo = SessionRepository(session_maker=self.db.async_session_maker, project_authz=self.authz)
self._session_repo = SessionRepository(
session_maker=self.db.async_session_maker, project_authz=self.authz, resource_pools=self.rp_repo
)
return self._session_repo

@property
Expand Down
4 changes: 2 additions & 2 deletions components/renku_data_services/base_api/misc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Common blueprints."""

from collections.abc import Awaitable, Callable
from collections.abc import Awaitable, Callable, Coroutine
from dataclasses import dataclass
from functools import wraps
from typing import Any, NoReturn, ParamSpec, TypeVar, cast
Expand Down Expand Up @@ -48,7 +48,7 @@ async def _get_version(_: Request) -> JSONResponse:
_P = ParamSpec("_P")


def validate_db_ids(f: Callable[_P, Awaitable[_T]]) -> Callable[_P, Awaitable[_T]]:
def validate_db_ids(f: Callable[_P, Awaitable[_T]]) -> Callable[_P, Coroutine[Any, Any, _T]]:
"""Decorator for a Sanic handler that errors out if passed in IDs are outside of the valid range for postgres."""

@wraps(f)
Expand Down
5 changes: 2 additions & 3 deletions components/renku_data_services/crc/blueprints.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,10 +315,9 @@ async def _get(_: Request, user: base_models.APIUser, resource_pool_id: int, cla
def get_no_pool(self) -> BlueprintFactoryResponse:
"""Get a specific class."""

@authenticate(self.authenticator)
@validate_db_ids
async def _get_no_pool(_: Request, user: base_models.APIUser, class_id: int) -> HTTPResponse:
res = await self.repo.get_classes(api_user=user, id=class_id)
async def _get_no_pool(_: Request, class_id: int) -> HTTPResponse:
res = await self.repo.get_classes(api_user=None, id=class_id)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: if the user is not used in this method at all, you can just remove the @authenticate decorator above

Copy link
Contributor Author

@andre-code andre-code Jun 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, included in the latest commit.

if len(res) < 1:
raise errors.MissingResourceError(message=f"The class with id {class_id} cannot be found.")
return json(apispec.ResourceClassWithId.model_validate(res[0]).model_dump(exclude_none=True))
Expand Down
10 changes: 7 additions & 3 deletions components/renku_data_services/crc/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ async def insert_resource_pool(

async def get_classes(
self,
api_user: base_models.APIUser,
api_user: Optional[base_models.APIUser] = None,
id: Optional[int] = None,
name: Optional[str] = None,
resource_pool_id: Optional[int] = None,
Expand All @@ -265,8 +265,12 @@ async def get_classes(
stmt = stmt.where(schemas.ResourceClassORM.id == id)
if name is not None:
stmt = stmt.where(schemas.ResourceClassORM.name == name)
# NOTE: The line below ensures that the right users can access the right resources, do not remove.
stmt = _classes_user_access_control(api_user, stmt)

# Apply user access control if api_user is provided
if api_user is not None:
# NOTE: The line below ensures that the right users can access the right resources, do not remove.
stmt = _classes_user_access_control(api_user, stmt)

res = await session.execute(stmt)
orms = res.scalars().all()
return [orm.dump() for orm in orms]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""add resource class in session launcher

Revision ID: 57dfd69ea814
Revises: c0631477aea4
Create Date: 2024-06-10 17:05:39.618418

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "57dfd69ea814"
down_revision = "c0631477aea4"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column("launchers", sa.Column("resource_class_id", sa.Integer(), nullable=True), schema="sessions")

op.create_foreign_key(
"fk_sessions_launchers_resource_class_id",
"launchers",
"resource_classes",
["resource_class_id"],
["id"],
source_schema="sessions",
referent_schema="resource_pools",
ondelete="SET NULL",
)
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint("fk_sessions_launchers_resource_class_id", "launchers", schema="sessions", type_="foreignkey")
op.drop_column("launchers", "resource_class_id", schema="sessions")
# ### end Alembic commands ###
6 changes: 6 additions & 0 deletions components/renku_data_services/session/api.spec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,8 @@ components:
$ref: "#/components/schemas/EnvironmentKind"
environment_id:
$ref: "#/components/schemas/EnvironmentId"
resource_class_id:
$ref: "#/components/schemas/ResourceClassId"
container_image:
$ref: "#/components/schemas/ContainerImage"
default_url:
Expand Down Expand Up @@ -387,6 +389,8 @@ components:
$ref: "#/components/schemas/EnvironmentKind"
environment_id:
$ref: "#/components/schemas/EnvironmentId"
resource_class_id:
$ref: "#/components/schemas/ResourceClassId"
container_image:
$ref: "#/components/schemas/ContainerImage"
default_url:
Expand All @@ -408,6 +412,8 @@ components:
$ref: "#/components/schemas/EnvironmentKind"
environment_id:
$ref: "#/components/schemas/EnvironmentId"
resource_class_id:
$ref: "#/components/schemas/ResourceClassId"
container_image:
$ref: "#/components/schemas/ContainerImage"
default_url:
Expand Down
59 changes: 43 additions & 16 deletions components/renku_data_services/session/apispec.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# generated by datamodel-codegen:
# filename: api.spec.yaml
# timestamp: 2024-03-29T11:09:50+00:00
# timestamp: 2024-06-10T13:14:40+00:00

from __future__ import annotations

from datetime import datetime
from enum import Enum
from typing import Optional
from typing import List, Optional

from pydantic import ConfigDict, Field, RootModel
from renku_data_services.session.apispec_base import BaseAPISpec
Expand All @@ -33,7 +33,9 @@ class Session(BaseAPISpec):

class Error(BaseAPISpec):
code: int = Field(..., example=1404, gt=0)
detail: Optional[str] = Field(None, example="A more detailed optional message showing what the problem was")
detail: Optional[str] = Field(
None, example="A more detailed optional message showing what the problem was"
)
message: str = Field(..., example="Something went wrong - please try again later")


Expand All @@ -58,10 +60,12 @@ class Environment(BaseAPISpec):
)
creation_date: datetime = Field(
...,
description="The date and time the session was created (time is always in UTC)",
description="The date and time the resource was created (in UTC and ISO-8601 format)",
example="2023-11-01T17:32:28Z",
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
container_image: str = Field(
...,
description="A container image",
Expand All @@ -84,7 +88,9 @@ class EnvironmentPost(BaseAPISpec):
max_length=99,
min_length=1,
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
container_image: str = Field(
...,
description="A container image",
Expand All @@ -110,7 +116,9 @@ class EnvironmentPatch(BaseAPISpec):
max_length=99,
min_length=1,
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
container_image: Optional[str] = Field(
None,
description="A container image",
Expand Down Expand Up @@ -149,17 +157,22 @@ class SessionLauncher(BaseAPISpec):
)
creation_date: datetime = Field(
...,
description="The date and time the session was created (time is always in UTC)",
description="The date and time the resource was created (in UTC and ISO-8601 format)",
example="2023-11-01T17:32:28Z",
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
environment_kind: EnvironmentKind
environment_id: Optional[str] = Field(
None,
description="Id of the environment to use",
example="01AN4Z79ZS6XX96588FDX0H099",
min_length=1,
)
resource_class_id: Optional[int] = Field(
None, description="The identifier of a resource class"
)
container_image: Optional[str] = Field(
None,
description="A container image",
Expand Down Expand Up @@ -192,14 +205,19 @@ class SessionLauncherPost(BaseAPISpec):
min_length=26,
pattern="^[A-Z0-9]{26}$",
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
environment_kind: EnvironmentKind
environment_id: Optional[str] = Field(
None,
description="Id of the environment to use",
example="01AN4Z79ZS6XX96588FDX0H099",
min_length=1,
)
resource_class_id: Optional[int] = Field(
None, description="The identifier of a resource class"
)
container_image: Optional[str] = Field(
None,
description="A container image",
Expand All @@ -225,14 +243,19 @@ class SessionLauncherPatch(BaseAPISpec):
max_length=99,
min_length=1,
)
description: Optional[str] = Field(None, description="A description for session", max_length=500)
description: Optional[str] = Field(
None, description="A description for the resource", max_length=500
)
environment_kind: Optional[EnvironmentKind] = None
environment_id: Optional[str] = Field(
None,
description="Id of the environment to use",
example="01AN4Z79ZS6XX96588FDX0H099",
min_length=1,
)
resource_class_id: Optional[int] = Field(
None, description="The identifier of a resource class"
)
container_image: Optional[str] = Field(
None,
description="A container image",
Expand All @@ -251,12 +274,16 @@ class SessionStart(BaseAPISpec):
model_config = ConfigDict(
extra="allow",
)
resource_class_id: Optional[int] = Field(None, description="The identifier of a resource class")
resource_class_id: Optional[int] = Field(
None, description="The identifier of a resource class"
)


class EnvironmentList(RootModel[list[Environment]]):
root: list[Environment] = Field(..., description="A list of session environments")
class EnvironmentList(RootModel[List[Environment]]):
root: List[Environment] = Field(..., description="A list of session environments")


class SessionLaunchersList(RootModel[list[SessionLauncher]]):
root: list[SessionLauncher] = Field(..., description="A list of Renku session launchers", min_length=0)
class SessionLaunchersList(RootModel[List[SessionLauncher]]):
root: List[SessionLauncher] = Field(
..., description="A list of Renku session launchers", min_length=0
)
50 changes: 47 additions & 3 deletions components/renku_data_services/session/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any

from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
Expand All @@ -12,6 +13,7 @@
from renku_data_services import errors
from renku_data_services.authz.authz import Authz, ResourceType
from renku_data_services.authz.models import Scope
from renku_data_services.crc.db import ResourcePoolRepository
from renku_data_services.session import apispec, models
from renku_data_services.session import orm as schemas
from renku_data_services.session.apispec import EnvironmentKind
Expand All @@ -20,9 +22,12 @@
class SessionRepository:
"""Repository for sessions."""

def __init__(self, session_maker: Callable[..., AsyncSession], project_authz: Authz) -> None:
def __init__(
self, session_maker: Callable[..., AsyncSession], project_authz: Authz, resource_pools: ResourcePoolRepository
) -> None:
self.session_maker = session_maker
self.project_authz: Authz = project_authz
self.resource_pools: ResourcePoolRepository = resource_pools

async def get_environments(self) -> list[models.Environment]:
"""Get all session environments from the database."""
Expand Down Expand Up @@ -181,6 +186,7 @@ async def insert_launcher(
description=new_launcher.description,
environment_kind=new_launcher.environment_kind,
environment_id=new_launcher.environment_id,
resource_class_id=new_launcher.resource_class_id,
container_image=new_launcher.container_image,
default_url=new_launcher.default_url,
created_by=models.Member(id=user.id),
Expand Down Expand Up @@ -208,12 +214,30 @@ async def insert_launcher(
message=f"Session environment with id '{environment_id}' does not exist or you do not have access to it." # noqa: E501
)

resource_class_id = new_launcher.resource_class_id
if resource_class_id is not None:
res = await session.scalars(
select(schemas.ResourceClassORM).where(schemas.ResourceClassORM.id == resource_class_id)
)
resource_class = res.one_or_none()
if resource_class is None:
raise errors.MissingResourceError(
message=f"Resource class with id '{resource_class_id}' does not exist."
)

res_classes = await self.resource_pools.get_classes(api_user=user, id=resource_class_id)
resource_class_by_user = next((rc for rc in res_classes if rc.id == resource_class_id), None)
if resource_class_by_user is None:
raise errors.Unauthorized(
message=f"Resource class with id '{resource_class_id}' you do not have access to it."
)

launcher = schemas.SessionLauncherORM.load(launcher_model)
session.add(launcher)
return launcher.dump()

async def update_launcher(
self, user: base_models.APIUser, launcher_id: str, **kwargs: dict
self, user: base_models.APIUser, launcher_id: str, **kwargs: Any
) -> models.SessionLauncher:
"""Update a session launcher entry."""
if not user.is_authenticated or user.id is None:
Expand Down Expand Up @@ -249,14 +273,34 @@ async def update_launcher(
message=f"Session environment with id '{environment_id}' does not exist or you do not have access to it." # noqa: E501
)

resource_class_id = kwargs.get("resource_class_id")
if resource_class_id is not None:
res = await session.scalars(
select(schemas.ResourceClassORM).where(schemas.ResourceClassORM.id == resource_class_id)
)
resource_class = res.one_or_none()
if resource_class is None:
raise errors.MissingResourceError(
message=f"Resource class with id '{resource_class_id}' does not exist."
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same as above


res_classes = await self.resource_pools.get_classes(api_user=user, id=resource_class_id)
resource_class_by_user = next((rc for rc in res_classes if rc.id == resource_class_id), None)
if resource_class_by_user is None:
raise errors.Unauthorized(
message=f"Resource class with id '{resource_class_id}' you do not have access to it."
)

for key, value in kwargs.items():
# NOTE: Only ``name``, ``description``, ``environment_kind``,
# ``environment_id``, ``container_image`` and ``default_url`` can be edited.
# ``environment_id``, ``resource_class_id``, ``container_image`` and
# ``default_url`` can be edited.
if key in [
"name",
"description",
"environment_kind",
"environment_id",
"resource_class_id",
"container_image",
"default_url",
]:
Expand Down
1 change: 1 addition & 0 deletions components/renku_data_services/session/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class SessionLauncher(BaseModel):
description: str | None
environment_kind: EnvironmentKind
environment_id: str | None
resource_class_id: int | None
container_image: str | None
default_url: str | None
created_by: Member
Expand Down
Loading
Loading