Loading src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py +3 −1 Changes for src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -145,7 +145,9 @@ async def get_app( service: EdgeAppService, caller: Caller, ) -> Any: return await service.get_app(app_id=appId, x_correlator=caller.x_correlator) return await service.get_app( app_id=appId, app_provider_id=caller.app_provider_id, x_correlator=caller.x_correlator ) @router.post( Loading src/open_exposure_gateway/application/services/edge_application_management_service.py +40 −20 Changes for src/open_exposure_gateway/application/services/edge_application_management_service.py: 40 added lines, 20 removed lines. Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ from open_exposure_gateway.core.exceptions import ( NotImplementedException, ) from open_exposure_gateway.domain.edge_application_management import ( SRMCatalogPayload, SRMCompletedInstance, SRMOperationCompleted, Subject, Loading Loading @@ -350,17 +351,12 @@ class EdgeApplicationManagementService: ) from exc async def get_app( self, app_id: UUID, x_correlator: Optional[str] = None self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] = None ) -> AppManifestEnvelope: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: owned = await self._get_owned_app(app_id, app_provider_id, x_correlator) if owned is None: raise NotFoundException(message=f"App {app_id} not found") catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) _, catalog = owned try: manifest = build_app_manifest(catalog) except (ValueError, TypeError, IndexError) as exc: Loading Loading @@ -452,9 +448,10 @@ class EdgeApplicationManagementService: if self._app_instance_repo is None: raise RuntimeError("AppInstanceRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: owned = await self._get_owned_app(app_id, app_provider_id, x_correlator) if owned is None: raise NotFoundException(message=f"App {app_id} not found") app_registration, _ = owned has_instances = await self._app_instance_repo.exists_for_app_registration( app_registration.app_registration_id Loading Loading @@ -506,9 +503,10 @@ class EdgeApplicationManagementService: edgeCloudZoneId=request.edgeCloudZoneId, ) app_registration = await self._app_registration_repo.get_by_app_id(request.appId) if app_registration is None: owned = await self._get_owned_app(request.appId, app_provider_id, x_correlator) if owned is None: raise BadRequestException(message=f"App {request.appId} is not registered") app_registration, _ = owned if await self._app_instance_repo.exists_in_zone( app_registration.app_registration_id, request.edgeCloudZoneId Loading Loading @@ -610,9 +608,10 @@ class EdgeApplicationManagementService: ) return AppDeploymentId(appDeploymentId=existing_deployment.app_deployment_id) app_registration = await self._app_registration_repo.get_by_app_id(request.appId) if app_registration is None: owned = await self._get_owned_app(request.appId, app_provider_id, x_correlator) if owned is None: raise BadRequestException(message=f"App {request.appId} is not registered") app_registration, _ = owned if len(set(request.edgeCloudZones)) != len(request.edgeCloudZones): raise BadRequestException(message="edgeCloudZones must not contain duplicates") Loading Loading @@ -788,12 +787,12 @@ class EdgeApplicationManagementService: ): raise NotFoundException(message=f"App deployment {app_deployment_id} not found") # Close an in-flight deploy so its late completion is ignored. deploy_operation = await self._operation_repo.get_by_id(app_deployment.operation_id) if ( deploy_operation is not None and deploy_operation.status not in _TERMINAL_OPERATION_STATUSES ): if deploy_operation is None or deploy_operation.app_provider_id != app_provider_id: raise NotFoundException(message=f"App deployment {app_deployment_id} not found") # Close an in-flight deploy so its late completion is ignored. if deploy_operation.status not in _TERMINAL_OPERATION_STATUSES: await self._operation_repo.save( deploy_operation.model_copy( update={ Loading Loading @@ -968,6 +967,9 @@ class EdgeApplicationManagementService: app_instance = await self._app_instance_repo.get_by_id(app_instance_id) if app_instance is None or app_instance.state == AppInstanceState.TERMINATED: raise NotFoundException(message=f"App instance {app_instance_id} not found") deploy_operation = await self._operation_repo.get_by_id(app_instance.operation_id) if deploy_operation is None or deploy_operation.app_provider_id != app_provider_id: raise NotFoundException(message=f"App instance {app_instance_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_terminate_instance_command( Loading Loading @@ -1456,6 +1458,24 @@ class EdgeApplicationManagementService: ) ) async def _get_owned_app( self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] ) -> Optional[tuple[AppRegistration, SRMCatalogPayload]]: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: return None try: catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) except NotFoundException: return None if catalog.service_specification.app_provider_id != app_provider_id: return None return app_registration, catalog async def _resolve_app_id(self, app_registration_id: UUID) -> Optional[UUID]: if self._app_registration_repo is None: return None Loading src/open_exposure_gateway/application/services/traffic_influence_service.py +17 −4 Changes for src/open_exposure_gateway/application/services/traffic_influence_service.py: 17 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -177,12 +177,23 @@ class TrafficInfluenceService: return instances[0].zone_id return None async def _resolve_app_registration_id(self, app_id: UUID, tenant_id: str) -> UUID: async def _resolve_app_registration_id( self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] ) -> UUID: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") not_registered = BadRequestException(message=f"App {app_id} is not registered") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None or app_registration.tenant_id != tenant_id: raise BadRequestException(message=f"App {app_id} is not registered") if app_registration is None: raise not_registered try: catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) except NotFoundException: raise not_registered from None if catalog.service_specification.app_provider_id != app_provider_id: raise not_registered return app_registration.app_registration_id async def _replay_create(self, operation: Operation) -> TrafficInfluenceResponse: Loading Loading @@ -242,7 +253,9 @@ class TrafficInfluenceService: if existing is not None: return await self._replay_create(existing) app_registration_id = await self._resolve_app_registration_id(request.appId, tenant_id) app_registration_id = await self._resolve_app_registration_id( request.appId, app_provider_id, x_correlator ) operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) traffic_influence_id = uuid4() Loading tests/unit/test_eam_service.py +164 −40 File changed.Preview size limit exceeded, changes collapsed. Show changes tests/unit/test_traffic_influence_flows.py +20 −7 Changes for tests/unit/test_traffic_influence_flows.py: 20 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -80,7 +80,9 @@ TI_APP_REGISTRATION_ID = UUID("11111111-2222-3333-4444-555555555555") @pytest.fixture(autouse=True) def registered_app(app_registration_repo: FakeAppRegistrationRepository) -> None: def registered_app( app_registration_repo: FakeAppRegistrationRepository, fake_srm: FakeSRMClient ) -> None: """A Traffic Influence request always targets an already-registered app.""" app_registration_repo.rows[TI_APP_REGISTRATION_ID] = AppRegistration( app_registration_id=TI_APP_REGISTRATION_ID, Loading @@ -91,6 +93,18 @@ def registered_app(app_registration_repo: FakeAppRegistrationRepository) -> None package_type=PackageType.HELM, status=AppRegistrationStatus.REGISTERED, ) fake_srm.catalog[str(TI_APP_REGISTRATION_ID)] = { "service_specification": { "id": str(TI_APP_REGISTRATION_ID), "ref": TI_BODY["appId"], "name": "traffic-influence-app", "version": "1.0.0", "app_provider_id": "test_app_provider", "descriptor": {}, }, "service_deployment_units": [], "service_capability_requirements": [], } def _ready_app_instance(app_instance_repo: FakeAppInstanceRepository, zone_id: UUID) -> AppInstance: Loading Loading @@ -277,17 +291,16 @@ class TestTrafficInfluenceCreateFlow: assert not [p for s, p in fake_bus.published if s == Subject.TASK_ACTIVATE] assert traffic_influence_repo.rows == {} def test_other_tenants_app_id_is_rejected( def test_other_providers_app_id_is_rejected( self, api_client: TestClient, app_registration_repo: FakeAppRegistrationRepository, fake_srm: FakeSRMClient, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: registration = app_registration_repo.rows[TI_APP_REGISTRATION_ID] app_registration_repo.rows[TI_APP_REGISTRATION_ID] = registration.model_copy( update={"tenant_id": "other-tenant"} ) fake_srm.catalog[str(TI_APP_REGISTRATION_ID)]["service_specification"][ "app_provider_id" ] = "other-provider" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) Loading Loading
src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py +3 −1 Changes for src/open_exposure_gateway/api/camara/edge_application_management/v0_1_0_alpha_1/router.py: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -145,7 +145,9 @@ async def get_app( service: EdgeAppService, caller: Caller, ) -> Any: return await service.get_app(app_id=appId, x_correlator=caller.x_correlator) return await service.get_app( app_id=appId, app_provider_id=caller.app_provider_id, x_correlator=caller.x_correlator ) @router.post( Loading
src/open_exposure_gateway/application/services/edge_application_management_service.py +40 −20 Changes for src/open_exposure_gateway/application/services/edge_application_management_service.py: 40 added lines, 20 removed lines. Original line number Diff line number Diff line Loading @@ -48,6 +48,7 @@ from open_exposure_gateway.core.exceptions import ( NotImplementedException, ) from open_exposure_gateway.domain.edge_application_management import ( SRMCatalogPayload, SRMCompletedInstance, SRMOperationCompleted, Subject, Loading Loading @@ -350,17 +351,12 @@ class EdgeApplicationManagementService: ) from exc async def get_app( self, app_id: UUID, x_correlator: Optional[str] = None self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] = None ) -> AppManifestEnvelope: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: owned = await self._get_owned_app(app_id, app_provider_id, x_correlator) if owned is None: raise NotFoundException(message=f"App {app_id} not found") catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) _, catalog = owned try: manifest = build_app_manifest(catalog) except (ValueError, TypeError, IndexError) as exc: Loading Loading @@ -452,9 +448,10 @@ class EdgeApplicationManagementService: if self._app_instance_repo is None: raise RuntimeError("AppInstanceRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: owned = await self._get_owned_app(app_id, app_provider_id, x_correlator) if owned is None: raise NotFoundException(message=f"App {app_id} not found") app_registration, _ = owned has_instances = await self._app_instance_repo.exists_for_app_registration( app_registration.app_registration_id Loading Loading @@ -506,9 +503,10 @@ class EdgeApplicationManagementService: edgeCloudZoneId=request.edgeCloudZoneId, ) app_registration = await self._app_registration_repo.get_by_app_id(request.appId) if app_registration is None: owned = await self._get_owned_app(request.appId, app_provider_id, x_correlator) if owned is None: raise BadRequestException(message=f"App {request.appId} is not registered") app_registration, _ = owned if await self._app_instance_repo.exists_in_zone( app_registration.app_registration_id, request.edgeCloudZoneId Loading Loading @@ -610,9 +608,10 @@ class EdgeApplicationManagementService: ) return AppDeploymentId(appDeploymentId=existing_deployment.app_deployment_id) app_registration = await self._app_registration_repo.get_by_app_id(request.appId) if app_registration is None: owned = await self._get_owned_app(request.appId, app_provider_id, x_correlator) if owned is None: raise BadRequestException(message=f"App {request.appId} is not registered") app_registration, _ = owned if len(set(request.edgeCloudZones)) != len(request.edgeCloudZones): raise BadRequestException(message="edgeCloudZones must not contain duplicates") Loading Loading @@ -788,12 +787,12 @@ class EdgeApplicationManagementService: ): raise NotFoundException(message=f"App deployment {app_deployment_id} not found") # Close an in-flight deploy so its late completion is ignored. deploy_operation = await self._operation_repo.get_by_id(app_deployment.operation_id) if ( deploy_operation is not None and deploy_operation.status not in _TERMINAL_OPERATION_STATUSES ): if deploy_operation is None or deploy_operation.app_provider_id != app_provider_id: raise NotFoundException(message=f"App deployment {app_deployment_id} not found") # Close an in-flight deploy so its late completion is ignored. if deploy_operation.status not in _TERMINAL_OPERATION_STATUSES: await self._operation_repo.save( deploy_operation.model_copy( update={ Loading Loading @@ -968,6 +967,9 @@ class EdgeApplicationManagementService: app_instance = await self._app_instance_repo.get_by_id(app_instance_id) if app_instance is None or app_instance.state == AppInstanceState.TERMINATED: raise NotFoundException(message=f"App instance {app_instance_id} not found") deploy_operation = await self._operation_repo.get_by_id(app_instance.operation_id) if deploy_operation is None or deploy_operation.app_provider_id != app_provider_id: raise NotFoundException(message=f"App instance {app_instance_id} not found") operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) command = build_terminate_instance_command( Loading Loading @@ -1456,6 +1458,24 @@ class EdgeApplicationManagementService: ) ) async def _get_owned_app( self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] ) -> Optional[tuple[AppRegistration, SRMCatalogPayload]]: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None: return None try: catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) except NotFoundException: return None if catalog.service_specification.app_provider_id != app_provider_id: return None return app_registration, catalog async def _resolve_app_id(self, app_registration_id: UUID) -> Optional[UUID]: if self._app_registration_repo is None: return None Loading
src/open_exposure_gateway/application/services/traffic_influence_service.py +17 −4 Changes for src/open_exposure_gateway/application/services/traffic_influence_service.py: 17 added lines, 4 removed lines. Original line number Diff line number Diff line Loading @@ -177,12 +177,23 @@ class TrafficInfluenceService: return instances[0].zone_id return None async def _resolve_app_registration_id(self, app_id: UUID, tenant_id: str) -> UUID: async def _resolve_app_registration_id( self, app_id: UUID, app_provider_id: str, x_correlator: Optional[str] ) -> UUID: if self._app_registration_repo is None: raise RuntimeError("AppRegistrationRepository is not available") not_registered = BadRequestException(message=f"App {app_id} is not registered") app_registration = await self._app_registration_repo.get_by_app_id(app_id) if app_registration is None or app_registration.tenant_id != tenant_id: raise BadRequestException(message=f"App {app_id} is not registered") if app_registration is None: raise not_registered try: catalog = await self.srm_client.get_app( app_id=app_registration.app_registration_id, x_correlator=x_correlator ) except NotFoundException: raise not_registered from None if catalog.service_specification.app_provider_id != app_provider_id: raise not_registered return app_registration.app_registration_id async def _replay_create(self, operation: Operation) -> TrafficInfluenceResponse: Loading Loading @@ -242,7 +253,9 @@ class TrafficInfluenceService: if existing is not None: return await self._replay_create(existing) app_registration_id = await self._resolve_app_registration_id(request.appId, tenant_id) app_registration_id = await self._resolve_app_registration_id( request.appId, app_provider_id, x_correlator ) operation_id, correlation_id, requested_at = self._new_operation_metadata(x_correlator) traffic_influence_id = uuid4() Loading
tests/unit/test_eam_service.py +164 −40 File changed.Preview size limit exceeded, changes collapsed. Show changes
tests/unit/test_traffic_influence_flows.py +20 −7 Changes for tests/unit/test_traffic_influence_flows.py: 20 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -80,7 +80,9 @@ TI_APP_REGISTRATION_ID = UUID("11111111-2222-3333-4444-555555555555") @pytest.fixture(autouse=True) def registered_app(app_registration_repo: FakeAppRegistrationRepository) -> None: def registered_app( app_registration_repo: FakeAppRegistrationRepository, fake_srm: FakeSRMClient ) -> None: """A Traffic Influence request always targets an already-registered app.""" app_registration_repo.rows[TI_APP_REGISTRATION_ID] = AppRegistration( app_registration_id=TI_APP_REGISTRATION_ID, Loading @@ -91,6 +93,18 @@ def registered_app(app_registration_repo: FakeAppRegistrationRepository) -> None package_type=PackageType.HELM, status=AppRegistrationStatus.REGISTERED, ) fake_srm.catalog[str(TI_APP_REGISTRATION_ID)] = { "service_specification": { "id": str(TI_APP_REGISTRATION_ID), "ref": TI_BODY["appId"], "name": "traffic-influence-app", "version": "1.0.0", "app_provider_id": "test_app_provider", "descriptor": {}, }, "service_deployment_units": [], "service_capability_requirements": [], } def _ready_app_instance(app_instance_repo: FakeAppInstanceRepository, zone_id: UUID) -> AppInstance: Loading Loading @@ -277,17 +291,16 @@ class TestTrafficInfluenceCreateFlow: assert not [p for s, p in fake_bus.published if s == Subject.TASK_ACTIVATE] assert traffic_influence_repo.rows == {} def test_other_tenants_app_id_is_rejected( def test_other_providers_app_id_is_rejected( self, api_client: TestClient, app_registration_repo: FakeAppRegistrationRepository, fake_srm: FakeSRMClient, fake_bus: FakeDataBus, traffic_influence_repo: FakeTrafficInfluenceRepository, ) -> None: registration = app_registration_repo.rows[TI_APP_REGISTRATION_ID] app_registration_repo.rows[TI_APP_REGISTRATION_ID] = registration.model_copy( update={"tenant_id": "other-tenant"} ) fake_srm.catalog[str(TI_APP_REGISTRATION_ID)]["service_specification"][ "app_provider_id" ] = "other-provider" response = api_client.post(f"{TI_BASE}/traffic-influences", json=TI_BODY) Loading