Skip to content

Commit decde18

Browse files
authored
feat: add endpoints for migrated Renku v1 projects (#628)
- Add GET /renku_v1_projects/{v1_id}/migrations to check migration status - Add POST /renku_v1_projects/{v1_id}/migrations to migrate projects to Renku v2 - Add GET /projects/{project_id}/migration_info to retrieve migration details - Introduce projectMigration table to track migrations
1 parent 7c1690d commit decde18

File tree

13 files changed

+675
-13
lines changed

13 files changed

+675
-13
lines changed

bases/renku_data_services/data_api/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ def register_all_handlers(app: Sanic, config: Config) -> Sanic:
105105
session_repo=config.session_repo,
106106
data_connector_to_project_link_repo=config.data_connector_to_project_link_repo,
107107
data_connector_repo=config.data_connector_repo,
108+
project_migration_repo=config.project_migration_repo,
108109
)
109110
project_session_secrets = ProjectSessionSecretBP(
110111
name="project_session_secrets",

components/renku_data_services/app_config/config.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@
6565
from renku_data_services.namespace.db import GroupRepository
6666
from renku_data_services.notebooks.config import NotebooksConfig
6767
from renku_data_services.platform.db import PlatformRepository
68-
from renku_data_services.project.db import ProjectMemberRepository, ProjectRepository, ProjectSessionSecretRepository
68+
from renku_data_services.project.db import (
69+
ProjectMemberRepository,
70+
ProjectMigrationRepository,
71+
ProjectRepository,
72+
ProjectSessionSecretRepository,
73+
)
6974
from renku_data_services.repositories.db import GitRepositoriesRepository
7075
from renku_data_services.search.db import SearchUpdatesRepo
7176
from renku_data_services.secrets.db import LowLevelUserSecretsRepo, UserSecretsRepo
@@ -277,6 +282,7 @@ class Config:
277282
_rp_repo: ResourcePoolRepository | None = field(default=None, repr=False, init=False)
278283
_storage_repo: StorageRepository | None = field(default=None, repr=False, init=False)
279284
_project_repo: ProjectRepository | None = field(default=None, repr=False, init=False)
285+
_project_migration_repo: ProjectMigrationRepository | None = field(default=None, repr=False, init=False)
280286
_group_repo: GroupRepository | None = field(default=None, repr=False, init=False)
281287
_event_repo: EventRepository | None = field(default=None, repr=False, init=False)
282288
_reprovisioning_repo: ReprovisioningRepository | None = field(default=None, repr=False, init=False)
@@ -413,6 +419,20 @@ def project_repo(self) -> ProjectRepository:
413419
)
414420
return self._project_repo
415421

422+
@property
423+
def project_migration_repo(self) -> ProjectMigrationRepository:
424+
"""The DB adapter for Renku native project migrations."""
425+
if not self._project_migration_repo:
426+
self._project_migration_repo = ProjectMigrationRepository(
427+
session_maker=self.db.async_session_maker,
428+
authz=self.authz,
429+
message_queue=self.message_queue,
430+
project_repo=self.project_repo,
431+
event_repo=self.event_repo,
432+
session_repo=self.session_repo,
433+
)
434+
return self._project_migration_repo
435+
416436
@property
417437
def project_member_repo(self) -> ProjectMemberRepository:
418438
"""The DB adapter for Renku native projects members."""
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""add project migrations
2+
3+
Revision ID: 559b1fc46cfe
4+
Revises: 71ef5efe740f
5+
Create Date: 2025-03-03 13:58:07.450665
6+
7+
"""
8+
9+
import sqlalchemy as sa
10+
from alembic import op
11+
12+
# revision identifiers, used by Alembic.
13+
revision = "559b1fc46cfe"
14+
down_revision = "71ef5efe740f"
15+
branch_labels = None
16+
depends_on = None
17+
18+
19+
def upgrade() -> None:
20+
# ### commands auto generated by Alembic - please adjust! ###
21+
op.create_table(
22+
"project_migrations",
23+
sa.Column("id", sa.String(length=26), nullable=False),
24+
sa.Column("project_v1_id", sa.Integer(), nullable=False),
25+
sa.Column("project_id", sa.String(length=26), nullable=False),
26+
sa.Column("launcher_id", sa.String(length=26), nullable=True),
27+
sa.ForeignKeyConstraint(["project_id"], ["projects.projects.id"], ondelete="CASCADE"),
28+
sa.PrimaryKeyConstraint("id"),
29+
sa.UniqueConstraint("project_v1_id"),
30+
sa.UniqueConstraint("project_v1_id", name="uq_project_v1_id"),
31+
schema="projects",
32+
)
33+
op.create_index(
34+
op.f("ix_projects_project_migrations_project_id"),
35+
"project_migrations",
36+
["project_id"],
37+
unique=False,
38+
schema="projects",
39+
)
40+
# ### end Alembic commands ###
41+
42+
43+
def downgrade() -> None:
44+
# ### commands auto generated by Alembic - please adjust! ###
45+
op.drop_index(op.f("ix_projects_project_migrations_project_id"), table_name="project_migrations", schema="projects")
46+
op.drop_table("project_migrations", schema="projects")
47+
# ### end Alembic commands ###

components/renku_data_services/project/api.spec.yaml

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,59 @@ paths:
148148
$ref: "#/components/responses/Error"
149149
tags:
150150
- projects
151+
/renku_v1_projects/{v1_id}/migrations:
152+
get:
153+
summary: Check if a v1 project has been migrated to v2
154+
parameters:
155+
- in: path
156+
name: v1_id
157+
required: true
158+
description: The ID of the project in Renku v1
159+
schema:
160+
type: integer
161+
responses:
162+
"200":
163+
description: Project exists in v2 and has been migrated
164+
content:
165+
application/json:
166+
schema:
167+
$ref: "#/components/schemas/Project"
168+
"404":
169+
description: No corresponding project found in v2
170+
content:
171+
application/json:
172+
schema:
173+
$ref: "#/components/schemas/ErrorResponse"
174+
default:
175+
$ref: "#/components/responses/Error"
176+
tags:
177+
- projects
178+
post:
179+
summary: Create a new project migrated from Renku v1
180+
parameters:
181+
- in: path
182+
name: v1_id
183+
required: true
184+
description: The ID of the Gitlab repository that represents the project in Renku v1
185+
schema:
186+
type: integer
187+
requestBody:
188+
required: true
189+
content:
190+
application/json:
191+
schema:
192+
$ref: "#/components/schemas/ProjectMigrationPost"
193+
responses:
194+
"201":
195+
description: The project was created
196+
content:
197+
application/json:
198+
schema:
199+
$ref: "#/components/schemas/Project"
200+
default:
201+
$ref: "#/components/responses/Error"
202+
tags:
203+
- projects
151204
/namespaces/{namespace}/projects/{slug}:
152205
get:
153206
summary: Get a project by namespace and project slug
@@ -254,6 +307,32 @@ paths:
254307
$ref: "#/components/responses/Error"
255308
tags:
256309
- projects
310+
/projects/{project_id}/migration_info:
311+
get:
312+
summary: Check if a v2 project is a project migrated from v1
313+
parameters:
314+
- in: path
315+
name: project_id
316+
required: true
317+
schema:
318+
$ref: "#/components/schemas/Ulid"
319+
responses:
320+
"200":
321+
description: Project exists in v2 and is a migrated project from v1
322+
content:
323+
application/json:
324+
schema:
325+
$ref: "#/components/schemas/ProjectMigrationInfo"
326+
"404":
327+
description: No corresponding project migrated from v1
328+
content:
329+
application/json:
330+
schema:
331+
$ref: "#/components/schemas/ErrorResponse"
332+
default:
333+
$ref: "#/components/responses/Error"
334+
tags:
335+
- projects
257336
/projects/{project_id}/members:
258337
get:
259338
summary: Get all members of a project
@@ -681,6 +760,67 @@ components:
681760
$ref: "#/components/schemas/IsTemplate"
682761
secrets_mount_directory:
683762
$ref: "#/components/schemas/SecretsMountDirectoryPatch"
763+
ProjectMigrationPost:
764+
description: Project v1 data to be migrated in Renku
765+
type: object
766+
additionalProperties: false
767+
properties:
768+
project:
769+
$ref: "#/components/schemas/ProjectPost"
770+
session_launcher:
771+
$ref: "#/components/schemas/MigrationSessionLauncherPost"
772+
required:
773+
- "project"
774+
MigrationSessionLauncherPost:
775+
description: Data required to create a session launcher for a project migrated
776+
type: object
777+
additionalProperties: false
778+
properties:
779+
name:
780+
$ref: "#/components/schemas/SessionName"
781+
container_image:
782+
$ref: "#/components/schemas/ContainerImage"
783+
default_url:
784+
allOf:
785+
- $ref: "#/components/schemas/DefaultUrl"
786+
- default: /lab
787+
default: /lab
788+
resource_class_id:
789+
$ref: "#/components/schemas/ResourceClassId"
790+
disk_storage:
791+
$ref: "#/components/schemas/DiskStorage"
792+
required:
793+
- name
794+
- container_image
795+
SessionName:
796+
description: Renku session name
797+
type: string
798+
minLength: 1
799+
maxLength: 99
800+
example: My Renku Session :)
801+
ContainerImage:
802+
description: A container image
803+
type: string
804+
maxLength: 500
805+
# NOTE: regex for an image name, optionally with a tag or sha256 specified
806+
# based on https://github.com/opencontainers/distribution-spec/blob/main/spec.md
807+
pattern: "^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$"
808+
example: renku/renkulab-py:3.10-0.18.1
809+
DefaultUrl:
810+
description: The default path to open in a session
811+
type: string
812+
maxLength: 200
813+
example: "/lab"
814+
ResourceClassId:
815+
description: The identifier of a resource class
816+
type: integer
817+
default: null
818+
nullable: true
819+
DiskStorage:
820+
description: The size of disk storage for the session, in gigabytes
821+
type: integer
822+
minimum: 1
823+
example: 8
684824
Ulid:
685825
description: ULID identifier
686826
type: string
@@ -902,6 +1042,20 @@ components:
9021042
description: A flag to filter projects where the user is a direct member.
9031043
type: boolean
9041044
default: false
1045+
ProjectMigrationInfo:
1046+
description: Information if a project is a migrated project
1047+
type: object
1048+
properties:
1049+
project_id:
1050+
$ref: "#/components/schemas/Ulid"
1051+
v1_id:
1052+
description: The id of the project in v1
1053+
type: integer
1054+
launcher_id:
1055+
$ref: "#/components/schemas/Ulid"
1056+
required:
1057+
- v1_id
1058+
- project_id
9051059
ProjectPermissions:
9061060
description: The set of permissions on a project
9071061
type: object

components/renku_data_services/project/apispec.py

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# generated by datamodel-codegen:
22
# filename: api.spec.yaml
3-
# timestamp: 2025-02-12T08:08:38+00:00
3+
# timestamp: 2025-03-12T11:42:14+00:00
44

55
from __future__ import annotations
66

@@ -71,6 +71,24 @@ class DataConnectorToProjectLink(BaseAPISpec):
7171
)
7272

7373

74+
class ProjectMigrationInfo(BaseAPISpec):
75+
project_id: str = Field(
76+
...,
77+
description="ULID identifier",
78+
max_length=26,
79+
min_length=26,
80+
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
81+
)
82+
v1_id: int = Field(..., description="The id of the project in v1")
83+
launcher_id: Optional[str] = Field(
84+
None,
85+
description="ULID identifier",
86+
max_length=26,
87+
min_length=26,
88+
pattern="^[0-7][0-9A-HJKMNP-TV-Z]{25}$",
89+
)
90+
91+
7492
class ProjectPermissions(BaseAPISpec):
7593
write: Optional[bool] = Field(None, description="The user can edit the project")
7694
delete: Optional[bool] = Field(None, description="The user can delete the project")
@@ -140,6 +158,41 @@ class ProjectsProjectIdCopiesGetParametersQuery(BaseAPISpec):
140158
writable: bool = False
141159

142160

161+
class MigrationSessionLauncherPost(BaseAPISpec):
162+
model_config = ConfigDict(
163+
extra="forbid",
164+
)
165+
name: str = Field(
166+
...,
167+
description="Renku session name",
168+
example="My Renku Session :)",
169+
max_length=99,
170+
min_length=1,
171+
)
172+
container_image: str = Field(
173+
...,
174+
description="A container image",
175+
example="renku/renkulab-py:3.10-0.18.1",
176+
max_length=500,
177+
pattern="^[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*(\\/[a-z0-9]+((\\.|_|__|-+)[a-z0-9]+)*)*(:[a-zA-Z0-9_][a-zA-Z0-9._-]{0,127}|@sha256:[a-fA-F0-9]{64}){0,1}$",
178+
)
179+
default_url: str = Field(
180+
"/lab",
181+
description="The default path to open in a session",
182+
example="/lab",
183+
max_length=200,
184+
)
185+
resource_class_id: Optional[int] = Field(
186+
None, description="The identifier of a resource class"
187+
)
188+
disk_storage: Optional[int] = Field(
189+
None,
190+
description="The size of disk storage for the session, in gigabytes",
191+
example=8,
192+
ge=1,
193+
)
194+
195+
143196
class ProjectMemberPatchRequest(BaseAPISpec):
144197
model_config = ConfigDict(
145198
extra="forbid",
@@ -553,6 +606,14 @@ class ProjectPatch(BaseAPISpec):
553606
secrets_mount_directory: Optional[str] = Field(None, example="/secrets")
554607

555608

609+
class ProjectMigrationPost(BaseAPISpec):
610+
model_config = ConfigDict(
611+
extra="forbid",
612+
)
613+
project: ProjectPost
614+
session_launcher: Optional[MigrationSessionLauncherPost] = None
615+
616+
556617
class ProjectMemberListPatchRequest(RootModel[List[ProjectMemberPatchRequest]]):
557618
root: List[ProjectMemberPatchRequest] = Field(
558619
...,

0 commit comments

Comments
 (0)