Skip to content

Commit

Permalink
update migration to restrict a one migration per project v1 id
Browse files Browse the repository at this point in the history
  • Loading branch information
andre-code committed Feb 11, 2025
1 parent e6a1089 commit 81386b2
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 29 deletions.
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"""add project migrations
Revision ID: 32fab117ebec
Revision ID: 6e4dfb1ca90f
Revises: d71f0f795d30
Create Date: 2025-02-06 15:20:45.667192
Create Date: 2025-02-11 10:05:25.003625
"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "32fab117ebec"
revision = "6e4dfb1ca90f"
down_revision = "d71f0f795d30"
branch_labels = None
depends_on = None
Expand All @@ -25,7 +25,8 @@ def upgrade() -> None:
sa.Column("project_id", sa.String(length=26), nullable=False),
sa.ForeignKeyConstraint(["project_id"], ["projects.projects.id"], ondelete="CASCADE"),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("project_id", "project_v1_id", name="uq_project_migrations"),
sa.UniqueConstraint("project_v1_id"),
sa.UniqueConstraint("project_v1_id", name="uq_project_v1_id"),
schema="projects",
)
op.create_index(
Expand Down
30 changes: 15 additions & 15 deletions components/renku_data_services/project/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -917,30 +917,30 @@ async def migrate_v1_project(
"""Migrate a v1 project by creating a new project and tracking the migration."""
if not session:
raise errors.ProgrammingError(message="A database session is required")
created_project = await self.project_repo.insert_project(user, project)
if not created_project:
raise errors.ValidationError(
message=f"Failed to create a project for migration from v1 (project_v1_id={project_v1_id})."
)

result = await session.scalars(
select(schemas.ProjectMigrationsORM)
.where(schemas.ProjectMigrationsORM.project_id == created_project.id)
.where(schemas.ProjectMigrationsORM.project_v1_id == project_v1_id)
select(schemas.ProjectMigrationsORM).where(schemas.ProjectMigrationsORM.project_v1_id == project_v1_id)
)
project_migration = result.one_or_none()
if project_migration is not None:
raise errors.ValidationError(message=f"Project V1 with id '{project_v1_id}' already exists.")
created_project = await self.project_repo.insert_project(user, project)
if not created_project:
raise errors.ValidationError(
message=(
f"Project with project_v1_id '{project_v1_id}' and "
f"Project id '{created_project.id}' already exists."
)
message=f"Failed to create a project for migration from v1 (project_v1_id={project_v1_id})."
)

migration_entry = models.UnsavedProjectMigration(project=created_project, project_v1_id=project_v1_id)
session.add(migration_entry)
if not created_project.id:
raise errors.ValidationError(message="Failed to create project with a valid ID.")

migration_orm = schemas.ProjectMigrationsORM(project_id=created_project.id, project_v1_id=project_v1_id)

if migration_orm.project_id is None:
raise errors.ValidationError(message="Project ID cannot be None for the migration entry.")

session.add(migration_orm)
await session.flush()
await session.refresh(migration_entry)
await session.refresh(migration_orm)

return created_project

Expand Down
6 changes: 3 additions & 3 deletions components/renku_data_services/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class SessionSecretPatchSecretValue:
class UnsavedProjectMigration:
"""Model representing a migration from an old project version that has not been persisted."""

project: Project
project_id: ULID
project_v1_id: int


Expand All @@ -170,11 +170,11 @@ class ProjectMigration:
"""Model representing a migration from an old project version."""

id: ULID
project: Project
project_id: ULID
project_v1_id: int
migrated_at: datetime = field(default_factory=lambda: datetime.now(UTC).replace(microsecond=0))

@property
def etag(self) -> str:
"""Entity tag value for this project migration object."""
return compute_etag_from_fields(self.migrated_at, self.project.id)
return compute_etag_from_fields(self.migrated_at, self.project_v1_id)
12 changes: 5 additions & 7 deletions components/renku_data_services/project/orm.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,17 +199,15 @@ class ProjectMigrationsORM(BaseORM):
"""Tracks project migrations from an old project (project_v1_id) to a new project (project_id)."""

__tablename__ = "project_migrations"
__table_args__ = (UniqueConstraint("project_id", "project_v1_id", name="uq_project_migrations"),)
__table_args__ = (UniqueConstraint("project_v1_id", name="uq_project_v1_id"),)

id: Mapped[ULID] = mapped_column("id", ULIDType, primary_key=True, default_factory=lambda: str(ULID()), init=False)

project_v1_id: Mapped[int] = mapped_column("project_v1_id", Integer, nullable=False)
"""The old project being migrated."""
project_v1_id: Mapped[int] = mapped_column("project_v1_id", Integer, nullable=False, unique=True)
"""The old project being migrated. Must be unique."""

project_id: Mapped[Optional[ULID]] = mapped_column(
ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True
)
"""The new project that replaces the old one."""
project_id: Mapped[ULID] = mapped_column(ForeignKey("projects.id", ondelete="CASCADE"), nullable=False, index=True)
"""The new project of the migration of the v1."""

project: Mapped[Optional[ProjectORM]] = relationship("ProjectORM", foreign_keys=[project_id], default=None)
"""Relationship to the new project."""
55 changes: 55 additions & 0 deletions test/bases/renku_data_services/data_api/test_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -1560,3 +1560,58 @@ async def test_get_project_after_group_moved(
assert response.json is not None
assert response.json.get("id") == project_id
assert response.json.get("documentation") == "Hello, World!"


@pytest.mark.asyncio
async def test_migrate_v1_project(sanic_client, app_config, user_headers, regular_user, create_project) -> None:
v1_id = 1122
v1_project = {
"name": "New Migrated Project",
"slug": "new-project-slug",
"namespace": regular_user.namespace.slug,
"description": "Old project for migration",
"repositories": ["http://old-repository.com"],
"visibility": "private",
"keywords": ["old", "project"],
}

await app_config.event_repo.delete_all_events()

_, response = await sanic_client.post(
f"/api/data/renku_v1_projects/{v1_id}/migrations", headers=user_headers, json=v1_project
)

assert response.status_code == 201, response.text
migrated_project = response.json
assert migrated_project["name"] == "New Migrated Project"
assert migrated_project["slug"] == "new-project-slug"
assert migrated_project["created_by"] == "user"
assert migrated_project["namespace"] == regular_user.namespace.slug
assert migrated_project["description"] == "Old project for migration"
assert migrated_project["visibility"] == "private"
assert migrated_project["keywords"] == ["old", "project"]
assert migrated_project["repositories"] == ["http://old-repository.com"]

# events = await app_config.event_repo.get_pending_events()
# assert len(events) == 2
# project_created_event = next(e for e in events if e.get_message_type() == "project.created")
# project_created = deserialize_event(project_created_event)
# assert project_created.name == v1_project["name"]
# assert project_created.slug == v1_project["slug"]
# project_auth_added_event = next(e for e in events if e.get_message_type() == "projectAuth.added")
# project_auth_added = deserialize_event(project_auth_added_event)
# assert project_auth_added.userId == "user"
# assert project_auth_added.role == MemberRole.OWNER

# migrated_project_id = migrated_project["id"]
# _, response = await sanic_client.get(f"/api/data/projects/{migrated_project_id}", headers=user_headers)
# assert response.status_code == 200, response.text
# migrated_project = response.json
# assert migrated_project["name"] == "New Migrated Project"
# assert migrated_project["slug"] == "new-project-slug"
# assert migrated_project["created_by"] == "user"
# assert migrated_project["namespace"] == regular_user.namespace.slug
# assert migrated_project["description"] == "Old project for migration"
# assert migrated_project["visibility"] == "private"
# assert migrated_project["keywords"] == ["old", "project"]
# assert migrated_project["repositories"] == ["http://old-repository.com"]

0 comments on commit 81386b2

Please sign in to comment.