|
4 | 4 | import operator |
5 | 5 | from collections.abc import Callable |
6 | 6 | from datetime import UTC, datetime |
7 | | -from typing import cast |
| 7 | +from typing import Any, TypedDict, cast |
8 | 8 |
|
9 | 9 | import arrow |
10 | 10 | from fastapi import FastAPI |
|
16 | 16 | from sqlalchemy.ext.asyncio import AsyncEngine |
17 | 17 |
|
18 | 18 | from ..api._dependencies.director import get_director_client |
19 | | -from ..models.services_db import ServiceAccessRightsDB |
| 19 | +from ..models.services_db import ServiceAccessRightsDB, ServiceMetaDataDBGet |
20 | 20 | from ..repository.groups import GroupsRepository |
21 | 21 | from ..repository.services import ServicesRepository |
22 | 22 | from ..utils.versioning import as_version, is_patch_release |
|
26 | 26 | _LEGACY_SERVICES_DATE: datetime = datetime(year=2020, month=8, day=19, tzinfo=UTC) |
27 | 27 |
|
28 | 28 |
|
| 29 | +class InheritedData(TypedDict): |
| 30 | + access_rights: list[ServiceAccessRightsDB] |
| 31 | + metadata_updates: dict[str, Any] |
| 32 | + |
| 33 | + |
29 | 34 | def _is_frontend_service(service: ServiceMetaDataPublished) -> bool: |
30 | 35 | return "/frontend/" in service.key |
31 | 36 |
|
@@ -117,71 +122,104 @@ async def evaluate_service_ownership_and_rights( |
117 | 122 | return (owner_gid, default_access_rights) |
118 | 123 |
|
119 | 124 |
|
120 | | -async def evaluate_auto_upgrade_policy( |
| 125 | +async def _find_previous_compatible_release( |
121 | 126 | services_repo: ServicesRepository, *, service_metadata: ServiceMetaDataPublished |
122 | | -) -> list[ServiceAccessRightsDB]: |
| 127 | +) -> ServiceMetaDataDBGet | None: |
123 | 128 | """ |
124 | | - Evaluates the access rights for a service based on the auto-upgrade patch policy. |
125 | | -
|
126 | | - The AUTO-UPGRADE PATCH policy ensures that: |
127 | | - - Any new patch release of a service automatically inherits the access rights from the previous compatible version. |
128 | | - - This policy does NOT apply to frontend services. |
| 129 | + Finds the previous compatible release for a service. |
129 | 130 |
|
130 | 131 | Args: |
131 | 132 | services_repo: Instance of ServicesRepository for database access. |
132 | 133 | service_metadata: Metadata of the service being evaluated. |
133 | 134 |
|
134 | 135 | Returns: |
135 | | - A list of ServiceAccessRightsDB objects representing the inherited access rights for the new patch version. |
136 | | - Returns an empty list if the service is a frontend service or if no previous compatible version is found. |
137 | | -
|
138 | | - Notes: |
139 | | - - The policy is described in https://github.com/ITISFoundation/osparc-simcore/issues/2244 |
140 | | - - Inheritance is only for patch releases (i.e., same major and minor version). |
141 | | - - Future improvement: Consider making this behavior configurable in the service publication contract. |
142 | | -
|
| 136 | + The previous compatible release if found, None otherwise. |
143 | 137 | """ |
144 | 138 | if _is_frontend_service(service_metadata): |
145 | | - return [] |
| 139 | + return None |
146 | 140 |
|
147 | | - service_access_rights = [] |
148 | 141 | new_version: Version = as_version(service_metadata.version) |
149 | 142 | latest_releases = await services_repo.list_service_releases( |
150 | 143 | service_metadata.key, |
151 | 144 | major=new_version.major, |
152 | 145 | minor=new_version.minor, |
153 | 146 | ) |
154 | 147 |
|
155 | | - previous_release = None |
| 148 | + # latest_releases is sorted from newer to older |
156 | 149 | for release in latest_releases: |
157 | | - # latest_releases is sorted from newer to older |
158 | | - # Find the previous version that is patched by new_version |
| 150 | + # COMPATIBILITY RULE: |
| 151 | + # - a patch release is compatible with the previous patch release |
159 | 152 | if is_patch_release(new_version, release.version): |
160 | | - previous_release = release |
161 | | - break |
| 153 | + return release |
| 154 | + |
| 155 | + return None |
| 156 | + |
| 157 | + |
| 158 | +async def inherit_from_previous_release( |
| 159 | + services_repo: ServicesRepository, *, service_metadata: ServiceMetaDataPublished |
| 160 | +) -> InheritedData: |
| 161 | + """ |
| 162 | + Inherits metadata and access rights from a previous compatible release. |
| 163 | +
|
| 164 | + This function applies inheritance policies: |
| 165 | + - AUTO-UPGRADE PATCH policy: new patch releases inherit access rights from previous compatible versions |
| 166 | + - Metadata inheritance: icon and other metadata fields are inherited if not specified in the new version |
| 167 | +
|
| 168 | + Args: |
| 169 | + services_repo: Instance of ServicesRepository for database access. |
| 170 | + service_metadata: Metadata of the service being evaluated. |
| 171 | +
|
| 172 | + Returns: |
| 173 | + An InheritedData object containing: |
| 174 | + - access_rights: List of ServiceAccessRightsDB objects inherited from the previous release |
| 175 | + - metadata_updates: Dict of metadata fields that should be updated in the new service |
| 176 | +
|
| 177 | + Notes: |
| 178 | + - The policy is described in https://github.com/ITISFoundation/osparc-simcore/issues/2244 |
| 179 | + - Inheritance is only for patch releases (i.e., same major and minor version). |
| 180 | + """ |
| 181 | + inherited_data: InheritedData = { |
| 182 | + "access_rights": [], |
| 183 | + "metadata_updates": {}, |
| 184 | + } |
| 185 | + |
| 186 | + previous_release = await _find_previous_compatible_release( |
| 187 | + services_repo, service_metadata=service_metadata |
| 188 | + ) |
| 189 | + |
| 190 | + if not previous_release: |
| 191 | + return inherited_data |
162 | 192 |
|
163 | | - if previous_release: |
164 | | - previous_access_rights = await services_repo.get_service_access_rights( |
165 | | - previous_release.key, previous_release.version |
| 193 | + # 1. ACCESS-RIGHTS: |
| 194 | + # Inherit access rights |
| 195 | + previous_access_rights = await services_repo.get_service_access_rights( |
| 196 | + previous_release.key, previous_release.version |
| 197 | + ) |
| 198 | + |
| 199 | + inherited_data["access_rights"] = [ |
| 200 | + access.model_copy( |
| 201 | + update={"version": service_metadata.version}, |
| 202 | + deep=True, |
166 | 203 | ) |
| 204 | + for access in previous_access_rights |
| 205 | + ] |
167 | 206 |
|
168 | | - service_access_rights = [ |
169 | | - access.model_copy( |
170 | | - update={"version": service_metadata.version}, |
171 | | - deep=True, |
172 | | - ) |
173 | | - for access in previous_access_rights |
174 | | - ] |
175 | | - return service_access_rights |
| 207 | + # 2. METADATA: |
| 208 | + # Inherit icon if not specified in the new service |
| 209 | + if not service_metadata.icon and previous_release.icon: |
| 210 | + inherited_data["metadata_updates"]["icon"] = previous_release.icon |
| 211 | + |
| 212 | + return inherited_data |
176 | 213 |
|
177 | 214 |
|
178 | 215 | def reduce_access_rights( |
179 | 216 | access_rights: list[ServiceAccessRightsDB], |
180 | 217 | reduce_operation: Callable = operator.ior, |
181 | 218 | ) -> list[ServiceAccessRightsDB]: |
182 | | - """ |
183 | | - Reduces a list of access-rights per target |
| 219 | + """Reduces a list of access-rights per target |
| 220 | +
|
184 | 221 | By default, the reduction is OR (i.e. preserves True flags) |
| 222 | +
|
185 | 223 | """ |
186 | 224 | # TODO: probably a lot of room to optimize |
187 | 225 | # helper functions to simplify operation of access rights |
|
0 commit comments