Skip to content

Commit 8970f0b

Browse files
committed
keep K8s types out of core.py and repository
1 parent 9cd8afa commit 8970f0b

5 files changed

Lines changed: 109 additions & 65 deletions

File tree

bases/renku_data_services/data_api/dependencies.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,13 @@ def from_env(cls) -> DependencyManager:
263263
default_kubeconfig=default_kubeconfig,
264264
cluster_repo=cluster_repo,
265265
cache=k8s_db_cache,
266-
kinds_to_cache=[AMALTHEA_SESSION_GVK, JUPYTER_SESSION_GVK, BUILD_RUN_GVK, TASK_RUN_GVK, KNATIVE_SERVICE_GVK],
266+
kinds_to_cache=[
267+
AMALTHEA_SESSION_GVK,
268+
JUPYTER_SESSION_GVK,
269+
BUILD_RUN_GVK,
270+
TASK_RUN_GVK,
271+
KNATIVE_SERVICE_GVK,
272+
],
267273
),
268274
)
269275
quota_repo = QuotaRepository(K8sResourceQuotaClient(client), K8sPriorityClassClient(client))
@@ -310,7 +316,13 @@ def from_env(cls) -> DependencyManager:
310316
default_kubeconfig=default_kubeconfig,
311317
cluster_repo=cluster_repo,
312318
cache=k8s_db_cache,
313-
kinds_to_cache=[AMALTHEA_SESSION_GVK, JUPYTER_SESSION_GVK, BUILD_RUN_GVK, TASK_RUN_GVK, KNATIVE_SERVICE_GVK],
319+
kinds_to_cache=[
320+
AMALTHEA_SESSION_GVK,
321+
JUPYTER_SESSION_GVK,
322+
BUILD_RUN_GVK,
323+
TASK_RUN_GVK,
324+
KNATIVE_SERVICE_GVK,
325+
],
314326
),
315327
),
316328
namespace=config.k8s_namespace,
Lines changed: 22 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,31 @@
11
"""Business logic for Renku apps."""
22

3-
from datetime import datetime
4-
5-
from renku_data_services.renku_apps import models
6-
from renku_data_services.renku_apps.cr_knative_service import Condition
7-
from renku_data_services.renku_apps.crs import KnativeService
3+
from renku_data_services.renku_apps.models import App, AppRuntimeState, AppStatus
84
from renku_data_services.session.models import SessionLauncher
95

106

11-
def knative_service_to_app(session_launcher: SessionLauncher, knative_service: KnativeService) -> models.App:
12-
"""Convert a Knative service to an app."""
13-
return models.App(
14-
name=knative_service.metadata.name,
15-
launcher_id=session_launcher.id,
16-
project_id=session_launcher.project_id,
17-
status=_project_app_status(knative_service),
18-
url=_url(knative_service),
19-
started=_started_at(knative_service),
20-
image=session_launcher.environment.container_image,
7+
def build_app(launcher: SessionLauncher, runtime: AppRuntimeState) -> App:
8+
"""Compose an App from its launcher and the runtime state observed in the cluster."""
9+
return App(
10+
name=runtime.name,
11+
launcher_id=launcher.id,
12+
project_id=launcher.project_id,
13+
status=app_status_from_ready(runtime.ready_status),
14+
url=runtime.url,
15+
started=runtime.started_at,
16+
image=launcher.environment.container_image,
2117
)
2218

2319

24-
def _url(knative_service: KnativeService) -> str | None:
25-
"""Get the public URL Knative assigned to the service, or None if it is not yet routed."""
26-
if knative_service.status is None:
27-
return None
28-
return knative_service.status.url
29-
30-
31-
def _ready_condition(knative_service: KnativeService) -> Condition | None:
32-
"""Get the Ready condition from a Knative service, or None if it doesn't exist."""
33-
if knative_service.status is None or not knative_service.status.conditions:
34-
return None
35-
return next((c for c in knative_service.status.conditions if c.type == "Ready"), None)
36-
37-
38-
def _started_at(knative_service: KnativeService) -> datetime | None:
39-
"""Get the time the Knative service became Ready, or None if not yet ready."""
40-
ready = _ready_condition(knative_service)
41-
if ready is None or ready.status != "True" or ready.lastTransitionTime is None:
42-
return None
43-
return datetime.fromisoformat(ready.lastTransitionTime)
44-
20+
def app_status_from_ready(ready_status: str | None) -> AppStatus:
21+
"""Map a Kubernetes Ready-condition status value to an app status.
4522
46-
def _project_app_status(knative_service: KnativeService) -> models.AppStatus:
47-
"""Convert a Knative service's Ready condition into an app status."""
48-
ready = _ready_condition(knative_service)
49-
if ready is None:
50-
return models.AppStatus("pending")
51-
if ready.status == "True":
52-
return models.AppStatus("ready")
53-
if ready.status == "False":
54-
return models.AppStatus("failed")
55-
return models.AppStatus("pending")
23+
Inputs follow the Kubernetes condition convention: "True", "False",
24+
"Unknown", or None when the condition is absent. Unknown and absent
25+
both collapse to PENDING.
26+
"""
27+
if ready_status == "True":
28+
return AppStatus.READY
29+
if ready_status == "False":
30+
return AppStatus.FAILED
31+
return AppStatus.PENDING

components/renku_data_services/renku_apps/k8s_client.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""K8s client wrapper for Renku apps."""
22

33
from collections.abc import AsyncGenerator
4+
from datetime import datetime
45
from typing import Any
56

67
from renku_data_services.crc.db import ClusterRepository
@@ -9,7 +10,9 @@
910
from renku_data_services.k8s.constants import DEFAULT_K8S_CLUSTER, DUMMY_RENKU_APP_USER_ID, ClusterId
1011
from renku_data_services.k8s.models import GVK, K8sObjectMeta
1112
from renku_data_services.project.models import Project
13+
from renku_data_services.renku_apps.cr_knative_service import Condition
1214
from renku_data_services.renku_apps.crs import KnativeService
15+
from renku_data_services.renku_apps.models import AppRuntimeState
1316
from renku_data_services.session.models import SessionLauncher
1417

1518
KNATIVE_SERVICE_GVK = GVK(kind="Service", group="serving.knative.dev", version="v1")
@@ -36,8 +39,8 @@ def __init__(self, client: K8sClusterClientsPool, cluster_repo: ClusterRepositor
3639

3740
async def create_app_deployment(
3841
self, session_launcher: SessionLauncher, resource_class: ResourceClass | None, project: Project
39-
) -> KnativeService:
40-
"""Create a deployment for the given app and return the created Knative Service."""
42+
) -> AppRuntimeState:
43+
"""Create a deployment for the given app and return its observed runtime state."""
4144
cluster_id: ClusterId = DEFAULT_K8S_CLUSTER
4245
cluster = await self.__client.cluster_by_id(cluster_id)
4346
app_name = _generate_app_name(project)
@@ -52,10 +55,10 @@ async def create_app_deployment(
5255
created = await self.__client.create(
5356
meta.with_manifest(manifest.model_dump(exclude_none=True, mode="json")), refresh=True
5457
)
55-
return KnativeService.model_validate(created.manifest)
58+
return _extract_runtime_state(KnativeService.model_validate(created.manifest))
5659

57-
async def get_app_deployment(self, app_name: str) -> KnativeService | None:
58-
"""Get the deployment for the given app name, or None if it does not exist."""
60+
async def get_app_deployment(self, app_name: str) -> AppRuntimeState | None:
61+
"""Get the runtime state for the given app name, or None if it does not exist."""
5962
cluster_id: ClusterId = DEFAULT_K8S_CLUSTER
6063
cluster = await self.__client.cluster_by_id(cluster_id)
6164
meta = K8sObjectMeta(
@@ -68,21 +71,21 @@ async def get_app_deployment(self, app_name: str) -> KnativeService | None:
6871
obj = await self.__client.get(meta)
6972
if obj is None:
7073
return None
71-
return KnativeService.model_validate(obj.manifest)
74+
return _extract_runtime_state(KnativeService.model_validate(obj.manifest))
7275

73-
async def get_app_deployment_for_project(self, project: Project) -> KnativeService | None:
74-
"""Get the app deployment for the given project, or None if it does not exist."""
76+
async def get_app_deployment_for_project(self, project: Project) -> AppRuntimeState | None:
77+
"""Get the runtime state for the given project's app, or None if it does not exist."""
7578
return await self.get_app_deployment(_generate_app_name(project))
7679

7780
async def delete_app_deployment(self, app_name: str) -> None:
7881
"""Delete the deployment for the given app name. NOT IMPLEMENTED."""
7982
raise NotImplementedError("Deleting app deployment is not implemented yet")
8083

81-
async def list_app_deployments(self) -> AsyncGenerator[KnativeService, None]:
84+
async def list_app_deployments(self) -> AsyncGenerator[AppRuntimeState, None]:
8285
"""List all app deployments. NOT IMPLEMENTED."""
8386
raise NotImplementedError("Listing app deployments is not implemented yet")
8487

85-
async def update_app_deployment(self, app_name: str, session_launcher: SessionLauncher) -> KnativeService:
88+
async def update_app_deployment(self, app_name: str, session_launcher: SessionLauncher) -> AppRuntimeState:
8689
"""Update the deployment for the given app name. NOT IMPLEMENTED."""
8790
raise NotImplementedError("Updating app deployment is not implemented yet")
8891

@@ -142,3 +145,38 @@ def _build_app_deployment_manifest(
142145
},
143146
}
144147
)
148+
149+
150+
def _url(knative_service: KnativeService) -> str | None:
151+
"""Get the public URL Knative assigned to the service, or None if it is not yet routed."""
152+
if knative_service.status is None:
153+
return None
154+
return knative_service.status.url
155+
156+
157+
def _ready_condition(knative_service: KnativeService) -> Condition | None:
158+
"""Get the Ready condition from a Knative service, or None if it doesn't exist."""
159+
if knative_service.status is None or not knative_service.status.conditions:
160+
return None
161+
return next((c for c in knative_service.status.conditions if c.type == "Ready"), None)
162+
163+
164+
def _started_at(knative_service: KnativeService) -> datetime | None:
165+
"""Get the time the Knative service became Ready, or None if not yet ready."""
166+
ready = _ready_condition(knative_service)
167+
if ready is None or ready.status != "True" or ready.lastTransitionTime is None:
168+
return None
169+
return datetime.fromisoformat(ready.lastTransitionTime)
170+
171+
172+
def _extract_runtime_state(knative_service: KnativeService) -> AppRuntimeState:
173+
"""Read app runtime state primitives off a Knative Service."""
174+
ready = _ready_condition(knative_service)
175+
return AppRuntimeState(
176+
name=knative_service.metadata.name,
177+
launcher_id=knative_service.launcher_id,
178+
project_id=knative_service.project_id,
179+
ready_status=ready.status if ready is not None else None,
180+
url=_url(knative_service),
181+
started_at=_started_at(knative_service),
182+
)

components/renku_data_services/renku_apps/models.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,21 @@ def as_apispec(self) -> apispec.AppResponse:
4040
started=self.started,
4141
image=self.image,
4242
)
43+
44+
45+
@dataclass(frozen=True, kw_only=True)
46+
class AppRuntimeState:
47+
"""Runtime state of an app deployment, as observed in the cluster.
48+
49+
Carries the primitives that the K8s adapter extracts from a Knative Service
50+
so that domain logic can compose an App without depending on K8s types.
51+
The ready_status field holds the raw Kubernetes Ready-condition status value
52+
("True", "False", "Unknown", or None if the condition is absent).
53+
"""
54+
55+
name: str
56+
launcher_id: ULID
57+
project_id: ULID
58+
ready_status: str | None
59+
url: str | None
60+
started_at: datetime | None

components/renku_data_services/renku_apps/repository.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from renku_data_services.crc.db import ResourcePoolRepository
1010
from renku_data_services.crc.models import ResourceClass
1111
from renku_data_services.project.db import ProjectRepository
12-
from renku_data_services.renku_apps.core import knative_service_to_app
12+
from renku_data_services.renku_apps.core import build_app
1313
from renku_data_services.renku_apps.k8s_client import RenkuAppsK8sClient
1414
from renku_data_services.renku_apps.models import App
1515
from renku_data_services.session.db import SessionRepository
@@ -52,16 +52,16 @@ async def create_app(self, user: base_models.APIUser, launcher_id: ULID) -> App:
5252
project = await self.project_repo.get_project(user, launcher.project_id)
5353
if await self.k8s_client.get_app_deployment_for_project(project) is not None:
5454
raise errors.ConflictError(message=f"An app already exists for project '{launcher.project_id}'.")
55-
service = await self.k8s_client.create_app_deployment(launcher, resource_class, project)
56-
return knative_service_to_app(launcher, service)
55+
runtime_state = await self.k8s_client.create_app_deployment(launcher, resource_class, project)
56+
return build_app(launcher, runtime_state)
5757

5858
async def get_app(self, user: base_models.APIUser, app_name: str) -> App:
5959
"""Retrieve an app by its name."""
60-
service = await self.k8s_client.get_app_deployment(app_name)
61-
if service is None:
60+
runtime_state = await self.k8s_client.get_app_deployment(app_name)
61+
if runtime_state is None:
6262
raise errors.MissingResourceError(
6363
message=f"App with name '{app_name}' does not exist or you do not have access to it."
6464
)
6565

66-
launcher = await self.session_repo.get_launcher(user, service.launcher_id)
67-
return knative_service_to_app(launcher, service)
66+
launcher = await self.session_repo.get_launcher(user, runtime_state.launcher_id)
67+
return build_app(launcher, runtime_state)

0 commit comments

Comments
 (0)