Skip to content

Commit

Permalink
fix: update project ETag when the namespace slug is changed
Browse files Browse the repository at this point in the history
  • Loading branch information
leafty committed Jan 13, 2025
1 parent fce24d6 commit 8056245
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 14 deletions.
2 changes: 1 addition & 1 deletion components/renku_data_services/data_connectors/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ class DataConnector(BaseDataConnector):
@property
def etag(self) -> str:
"""Entity tag value for this data connector object."""
return compute_etag_from_timestamp(self.updated_at, include_quotes=True)
return compute_etag_from_timestamp(self.updated_at)


@dataclass(frozen=True, eq=True, kw_only=True)
Expand Down
2 changes: 1 addition & 1 deletion components/renku_data_services/platform/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PlatformConfig:
@property
def etag(self) -> str:
"""Entity tag value for this project object."""
return compute_etag_from_timestamp(self.updated_at, include_quotes=True)
return compute_etag_from_timestamp(self.updated_at)


@dataclass(frozen=True, eq=True, kw_only=True)
Expand Down
4 changes: 4 additions & 0 deletions components/renku_data_services/project/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,8 @@ async def update_project(
message=f"The project cannot be moved because you do not have sufficient permissions with the namespace {patch.namespace}" # noqa: E501
)
project.slug.namespace_id = ns.id
# Trigger update for ``updated_at`` column
await session.execute(update(schemas.ProjectORM).where(schemas.ProjectORM.id == project_id).values())
if patch.slug is not None and patch.slug != old_project.slug:
namespace_id = project.slug.namespace_id
existing_entity = await session.scalar(
Expand All @@ -358,6 +360,8 @@ async def update_project(
)
session.add(ns_schemas.EntitySlugOldORM(slug=old_project.slug, latest_slug_id=project.slug.id))
project.slug.slug = patch.slug
# Trigger update for ``updated_at`` column
await session.execute(update(schemas.ProjectORM).where(schemas.ProjectORM.id == project_id).values())
if patch.visibility is not None:
visibility_orm = (
project_apispec.Visibility(patch.visibility)
Expand Down
19 changes: 10 additions & 9 deletions components/renku_data_services/project/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from renku_data_services.authz.models import Visibility
from renku_data_services.base_models import ResetType
from renku_data_services.namespace.models import Namespace
from renku_data_services.utils.etag import compute_etag_from_timestamp
from renku_data_services.utils.etag import compute_etag_from_fields, compute_etag_from_timestamp

Repository = str

Expand All @@ -33,13 +33,6 @@ class BaseProject:
is_template: bool = False
secrets_mount_directory: PurePosixPath | None = None

@property
def etag(self) -> str | None:
"""Entity tag value for this project object."""
if self.updated_at is None:
return None
return compute_etag_from_timestamp(self.updated_at)


@dataclass(frozen=True, eq=True, kw_only=True)
class Project(BaseProject):
Expand All @@ -49,6 +42,14 @@ class Project(BaseProject):
namespace: Namespace
secrets_mount_directory: PurePosixPath

@property
def etag(self) -> str | None:
"""Entity tag value for this project object."""
if self.updated_at is None:
return None
# NOTE: `slug` is the only field from `self.namespace` which is serialized in API responses.
return compute_etag_from_fields(self.updated_at, self.namespace.slug)


@dataclass(frozen=True, eq=True, kw_only=True)
class UnsavedProject(BaseProject):
Expand Down Expand Up @@ -120,7 +121,7 @@ class SessionSecretSlot(UnsavedSessionSecretSlot):
@property
def etag(self) -> str:
"""Entity tag value for this session secret slot object."""
return compute_etag_from_timestamp(self.updated_at, include_quotes=True)
return compute_etag_from_timestamp(self.updated_at)


@dataclass(frozen=True, eq=True, kw_only=True)
Expand Down
23 changes: 20 additions & 3 deletions components/renku_data_services/utils/etag.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,28 @@

from datetime import datetime
from hashlib import md5
from typing import Any


def compute_etag_from_timestamp(updated_at: datetime, include_quotes: bool = False) -> str:
def compute_etag_from_timestamp(updated_at: datetime) -> str:
"""Computes an entity tag value by hashing the updated_at value."""
etag = md5(updated_at.isoformat().encode(), usedforsecurity=False).hexdigest().upper()
if not include_quotes:
return etag
return f'"{etag}"'


def compute_etag_from_fields(updated_at: datetime, *args: Any) -> str:
"""Computes an entity tag value by hashing the field values.
By convention, the first field should be `updated_at`.
"""
values: list[Any] = [updated_at]
values.extend(arg for arg in args)
to_hash = "-".join(_get_hashable_string(value) for value in values)
etag = md5(to_hash.encode(), usedforsecurity=False).hexdigest().upper()
return f'"{etag}"'


def _get_hashable_string(value: Any) -> str:
if isinstance(value, datetime):
return value.isoformat()
return f"{value}"

0 comments on commit 8056245

Please sign in to comment.