Loading edge_cloud_management_api/controllers/app_controllers.py +33 −14 Original line number Diff line number Diff line Loading @@ -52,16 +52,24 @@ _resolve_federated_app_identity = resolve_federated_app_identity _resolve_federated_app_provider_id = resolve_federated_app_provider_id def _get_app_provider_for_delete(api_client, app_id): def _get_app_cleanup_metadata(api_client, app_id): app_response = api_client.get_app(app_id) if not isinstance(app_response, dict): return None return None, None app_provider_id = app_response.get("appProvider") or app_response.get("appProviderId") artefact_id = None manifest = app_response.get("appManifest") if isinstance(manifest, dict) and not app_provider_id: app_provider_id = manifest.get("appProvider") or manifest.get("appProviderId") return app_provider_id app_component_specs = app_response.get("appComponentSpecs") if isinstance(app_component_specs, list) and app_component_specs: artefact_id = app_component_specs[0].get("artefactId") if artefact_id is None and isinstance(manifest, dict): manifest_component_specs = manifest.get("appComponentSpecs") if isinstance(manifest_component_specs, list) and manifest_component_specs: artefact_id = manifest_component_specs[0].get("artefactId") return app_provider_id, artefact_id def _cleanup_federated_app_before_local_delete(api_client, app_id): Loading @@ -69,13 +77,15 @@ def _cleanup_federated_app_before_local_delete(api_client, app_id): if not feds: return None app_provider_id = _get_app_provider_for_delete(api_client, app_id) app_provider_id, artefact_id = _get_app_cleanup_metadata(api_client, app_id) cleanup_response = cleanup_federated_app( federation_client=federation_client, feds=feds, app_id=app_id, app_provider_id=app_provider_id, normalize_federated_app_id=_normalize_federated_app_id, artefact_id=artefact_id, normalize_federated_artefact_id=_normalize_federated_artefact_id, resolve_federated_app_identity=_resolve_federated_app_identity, ) if cleanup_response is None: Loading Loading @@ -118,7 +128,7 @@ def _split_image_reference(image_path): clean_path = str(image_path).lstrip("/") parts = clean_path.split("/", 1) if len(parts) == 1: return "docker.io", clean_path return "docker.io/library", clean_path registry_candidate = parts[0] if "." in registry_candidate or ":" in registry_candidate: return registry_candidate, parts[1] Loading Loading @@ -305,7 +315,16 @@ def create_app_instance(): try: logger.debug("Sending deployment request to SRM") response = srm_client.deploy_service_function(data=body) local_deploy_body = dict(body) local_zone = { "edgeCloudZoneId": zone.get("edgeCloudZoneId"), "edgeCloudZoneName": zone.get("edgeCloudZoneName"), "edgeCloudProvider": zone.get("edgeCloudProvider"), "edgeCloudZoneStatus": zone.get("edgeCloudZoneStatus"), "edgeCloudRegion": zone.get("edgeCloudRegion"), } local_deploy_body["appZones"] = [{"EdgeCloudZone": local_zone}] response = srm_client.deploy_service_function(data=local_deploy_body) if isinstance(response, dict) and "error" in response: logger.warning( Loading edge_cloud_management_api/controllers/app_partner_orchestration.py +27 −5 Original line number Diff line number Diff line Loading @@ -40,11 +40,21 @@ def resolve_target_zone(srm_client, edge_cloud_zone_id, edge_cloud_provider, zon return zone def cleanup_federated_app(federation_client, feds, app_id, app_provider_id, normalize_federated_app_id, resolve_federated_app_identity): def cleanup_federated_app( federation_client, feds, app_id, app_provider_id, normalize_federated_app_id, artefact_id, normalize_federated_artefact_id, resolve_federated_app_identity, ): if not feds: return None cleanup_performed = False federated_app_id = normalize_federated_app_id(app_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading Loading @@ -85,14 +95,26 @@ def cleanup_federated_app(federation_client, feds, app_id, app_provider_id, norm remove_response = federation_client.delete_onboarded_app( federation_context_id, normalize_federated_app_id(app_id), federated_app_id, fed_token, ) remove_status = int(remove_response.get("status_code", 500)) if isinstance(remove_response, dict) else 500 if remove_status in (200, 202, 204): if remove_status in (200, 202, 204, 404): cleanup_performed = True continue if remove_status == 404: if app_provider_id: federated_artefact_id = normalize_federated_artefact_id( federation_context_id, artefact_id, app_id, ) artefact_response = federation_client.delete_artefact( federation_context_id, federated_artefact_id, fed_token, ) artefact_status = int(artefact_response.get("status_code", 500)) if isinstance(artefact_response, dict) else 500 if artefact_status not in (200, 202, 204, 404): return jsonify(artefact_response), artefact_status continue return jsonify(remove_response), remove_status Loading edge_cloud_management_api/services/federation_services.py +23 −0 Original line number Diff line number Diff line Loading @@ -437,6 +437,29 @@ class FederationManagerClient: logger.error(f"Create artefact unexpected error: {e}") return {"error": str(e)}, 500 def delete_artefact(self, federation_context_id: str, artefact_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/artefact/{artefact_id}" try: response = requests.delete(url, headers=self._get_headers(token), timeout=120) try: body = response.json() except ValueError: body = response.text response.raise_for_status() return {"message": body, "status_code": response.status_code} except Timeout: logger.error("Delete artefact timed out") return {"error": "Request timed out", "status_code": 408} except ConnectionError: logger.error("Delete artefact connection error") return {"error": "Connection error", "status_code": 503} except requests.exceptions.HTTPError as http_err: logger.error(f"Delete artefact HTTP error: {http_err}") return {"error": str(http_err), "status_code": response.status_code} except Exception as e: logger.error(f"Delete artefact unexpected error: {e}") return {"error": str(e), "status_code": 500} class FederationManagerClientFactory: def __init__(self): self.default_base_url = config.FEDERATION_MANAGER_HOST Loading tests/unit/controllers/test_app_controllers.py +75 −0 Original line number Diff line number Diff line Loading @@ -41,6 +41,39 @@ def test_resolve_federated_app_identity_normalizes_app_and_provider_ids(): assert provider_id == "providerplaygroundoegnginx" @patch("edge_cloud_management_api.controllers.app_partner_orchestration.get_local_zones") @patch("edge_cloud_management_api.controllers.app_partner_orchestration.get_zone") def test_resolve_target_zone_maps_default_local_alias_to_live_zone(mock_get_zone, mock_get_local_zones): mock_get_zone.return_value = None mock_get_local_zones.return_value = [{ "edgeCloudZoneId": "7f1c87c2-1de3-44bb-9888-a1067b425775", "edgeCloudZoneName": "221aba31da3c", "edgeCloudProvider": "Local Operator", "edgeCloudZoneStatus": "active", "edgeCloudRegion": "unknown", "isLocal": "true", }] zone = app_controllers.resolve_target_zone( srm_client=MagicMock(), edge_cloud_zone_id="default", edge_cloud_provider="Local Operator", zone_payload={ "edgeCloudZoneId": "default", "edgeCloudProvider": "Local Operator", }, ) assert zone == { "edgeCloudZoneId": "7f1c87c2-1de3-44bb-9888-a1067b425775", "edgeCloudZoneName": "221aba31da3c", "edgeCloudProvider": "Local Operator", "edgeCloudZoneStatus": "active", "edgeCloudRegion": "unknown", "isLocal": "true", } def test_submit_app_rejects_non_uuid_app_id(): app = Flask(__name__) app.config["TESTING"] = True Loading Loading @@ -557,6 +590,9 @@ def test_delete_app_runs_federated_cleanup_before_local_delete( mock_client = MagicMock() mock_client.get_app.return_value = { "appComponentSpecs": [{ "artefactId": "12345678-1234-1234-9234-123456789abc", }], "appManifest": { "appProvider": "Local Operator", } Loading Loading @@ -587,6 +623,43 @@ def test_cleanup_federated_app_handles_dict_delete_response(mock_jsonify): "message": "Deleted successfully", "status_code": 204, } federation_client.delete_artefact.return_value = { "message": "Deleted successfully", "status_code": 204, } result = app_controllers.cleanup_federated_app( federation_client=federation_client, feds=[{"_id": "fed-1", "token": "token-1"}], app_id="app-123", app_provider_id="Local Operator", normalize_federated_app_id=app_controllers._normalize_federated_app_id, artefact_id="12345678-1234-1234-9234-123456789abc", normalize_federated_artefact_id=app_controllers._normalize_federated_artefact_id, resolve_federated_app_identity=app_controllers._resolve_federated_app_identity, ) assert result == ("", 204) mock_jsonify.assert_not_called() federation_client.delete_artefact.assert_called_once_with( "fed-1", "f2175bf7-71e5-54aa-99f1-a72de468ac60", "token-1", ) @patch("edge_cloud_management_api.controllers.app_partner_orchestration.jsonify") def test_cleanup_federated_app_ignores_missing_artefact(mock_jsonify): federation_client = MagicMock() federation_client.get_all_app_instances.return_value = ([], 200) federation_client.delete_onboarded_app.return_value = { "message": "Deleted successfully", "status_code": 204, } federation_client.delete_artefact.return_value = { "error": "not found", "status_code": 404, } result = app_controllers.cleanup_federated_app( federation_client=federation_client, Loading @@ -594,6 +667,8 @@ def test_cleanup_federated_app_handles_dict_delete_response(mock_jsonify): app_id="app-123", app_provider_id="Local Operator", normalize_federated_app_id=app_controllers._normalize_federated_app_id, artefact_id="12345678-1234-1234-9234-123456789abc", normalize_federated_artefact_id=app_controllers._normalize_federated_artefact_id, resolve_federated_app_identity=app_controllers._resolve_federated_app_identity, ) Loading Loading
edge_cloud_management_api/controllers/app_controllers.py +33 −14 Original line number Diff line number Diff line Loading @@ -52,16 +52,24 @@ _resolve_federated_app_identity = resolve_federated_app_identity _resolve_federated_app_provider_id = resolve_federated_app_provider_id def _get_app_provider_for_delete(api_client, app_id): def _get_app_cleanup_metadata(api_client, app_id): app_response = api_client.get_app(app_id) if not isinstance(app_response, dict): return None return None, None app_provider_id = app_response.get("appProvider") or app_response.get("appProviderId") artefact_id = None manifest = app_response.get("appManifest") if isinstance(manifest, dict) and not app_provider_id: app_provider_id = manifest.get("appProvider") or manifest.get("appProviderId") return app_provider_id app_component_specs = app_response.get("appComponentSpecs") if isinstance(app_component_specs, list) and app_component_specs: artefact_id = app_component_specs[0].get("artefactId") if artefact_id is None and isinstance(manifest, dict): manifest_component_specs = manifest.get("appComponentSpecs") if isinstance(manifest_component_specs, list) and manifest_component_specs: artefact_id = manifest_component_specs[0].get("artefactId") return app_provider_id, artefact_id def _cleanup_federated_app_before_local_delete(api_client, app_id): Loading @@ -69,13 +77,15 @@ def _cleanup_federated_app_before_local_delete(api_client, app_id): if not feds: return None app_provider_id = _get_app_provider_for_delete(api_client, app_id) app_provider_id, artefact_id = _get_app_cleanup_metadata(api_client, app_id) cleanup_response = cleanup_federated_app( federation_client=federation_client, feds=feds, app_id=app_id, app_provider_id=app_provider_id, normalize_federated_app_id=_normalize_federated_app_id, artefact_id=artefact_id, normalize_federated_artefact_id=_normalize_federated_artefact_id, resolve_federated_app_identity=_resolve_federated_app_identity, ) if cleanup_response is None: Loading Loading @@ -118,7 +128,7 @@ def _split_image_reference(image_path): clean_path = str(image_path).lstrip("/") parts = clean_path.split("/", 1) if len(parts) == 1: return "docker.io", clean_path return "docker.io/library", clean_path registry_candidate = parts[0] if "." in registry_candidate or ":" in registry_candidate: return registry_candidate, parts[1] Loading Loading @@ -305,7 +315,16 @@ def create_app_instance(): try: logger.debug("Sending deployment request to SRM") response = srm_client.deploy_service_function(data=body) local_deploy_body = dict(body) local_zone = { "edgeCloudZoneId": zone.get("edgeCloudZoneId"), "edgeCloudZoneName": zone.get("edgeCloudZoneName"), "edgeCloudProvider": zone.get("edgeCloudProvider"), "edgeCloudZoneStatus": zone.get("edgeCloudZoneStatus"), "edgeCloudRegion": zone.get("edgeCloudRegion"), } local_deploy_body["appZones"] = [{"EdgeCloudZone": local_zone}] response = srm_client.deploy_service_function(data=local_deploy_body) if isinstance(response, dict) and "error" in response: logger.warning( Loading
edge_cloud_management_api/controllers/app_partner_orchestration.py +27 −5 Original line number Diff line number Diff line Loading @@ -40,11 +40,21 @@ def resolve_target_zone(srm_client, edge_cloud_zone_id, edge_cloud_provider, zon return zone def cleanup_federated_app(federation_client, feds, app_id, app_provider_id, normalize_federated_app_id, resolve_federated_app_identity): def cleanup_federated_app( federation_client, feds, app_id, app_provider_id, normalize_federated_app_id, artefact_id, normalize_federated_artefact_id, resolve_federated_app_identity, ): if not feds: return None cleanup_performed = False federated_app_id = normalize_federated_app_id(app_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading Loading @@ -85,14 +95,26 @@ def cleanup_federated_app(federation_client, feds, app_id, app_provider_id, norm remove_response = federation_client.delete_onboarded_app( federation_context_id, normalize_federated_app_id(app_id), federated_app_id, fed_token, ) remove_status = int(remove_response.get("status_code", 500)) if isinstance(remove_response, dict) else 500 if remove_status in (200, 202, 204): if remove_status in (200, 202, 204, 404): cleanup_performed = True continue if remove_status == 404: if app_provider_id: federated_artefact_id = normalize_federated_artefact_id( federation_context_id, artefact_id, app_id, ) artefact_response = federation_client.delete_artefact( federation_context_id, federated_artefact_id, fed_token, ) artefact_status = int(artefact_response.get("status_code", 500)) if isinstance(artefact_response, dict) else 500 if artefact_status not in (200, 202, 204, 404): return jsonify(artefact_response), artefact_status continue return jsonify(remove_response), remove_status Loading
edge_cloud_management_api/services/federation_services.py +23 −0 Original line number Diff line number Diff line Loading @@ -437,6 +437,29 @@ class FederationManagerClient: logger.error(f"Create artefact unexpected error: {e}") return {"error": str(e)}, 500 def delete_artefact(self, federation_context_id: str, artefact_id: str, token: str): url = f"{self.base_url}/{federation_context_id}/artefact/{artefact_id}" try: response = requests.delete(url, headers=self._get_headers(token), timeout=120) try: body = response.json() except ValueError: body = response.text response.raise_for_status() return {"message": body, "status_code": response.status_code} except Timeout: logger.error("Delete artefact timed out") return {"error": "Request timed out", "status_code": 408} except ConnectionError: logger.error("Delete artefact connection error") return {"error": "Connection error", "status_code": 503} except requests.exceptions.HTTPError as http_err: logger.error(f"Delete artefact HTTP error: {http_err}") return {"error": str(http_err), "status_code": response.status_code} except Exception as e: logger.error(f"Delete artefact unexpected error: {e}") return {"error": str(e), "status_code": 500} class FederationManagerClientFactory: def __init__(self): self.default_base_url = config.FEDERATION_MANAGER_HOST Loading
tests/unit/controllers/test_app_controllers.py +75 −0 Original line number Diff line number Diff line Loading @@ -41,6 +41,39 @@ def test_resolve_federated_app_identity_normalizes_app_and_provider_ids(): assert provider_id == "providerplaygroundoegnginx" @patch("edge_cloud_management_api.controllers.app_partner_orchestration.get_local_zones") @patch("edge_cloud_management_api.controllers.app_partner_orchestration.get_zone") def test_resolve_target_zone_maps_default_local_alias_to_live_zone(mock_get_zone, mock_get_local_zones): mock_get_zone.return_value = None mock_get_local_zones.return_value = [{ "edgeCloudZoneId": "7f1c87c2-1de3-44bb-9888-a1067b425775", "edgeCloudZoneName": "221aba31da3c", "edgeCloudProvider": "Local Operator", "edgeCloudZoneStatus": "active", "edgeCloudRegion": "unknown", "isLocal": "true", }] zone = app_controllers.resolve_target_zone( srm_client=MagicMock(), edge_cloud_zone_id="default", edge_cloud_provider="Local Operator", zone_payload={ "edgeCloudZoneId": "default", "edgeCloudProvider": "Local Operator", }, ) assert zone == { "edgeCloudZoneId": "7f1c87c2-1de3-44bb-9888-a1067b425775", "edgeCloudZoneName": "221aba31da3c", "edgeCloudProvider": "Local Operator", "edgeCloudZoneStatus": "active", "edgeCloudRegion": "unknown", "isLocal": "true", } def test_submit_app_rejects_non_uuid_app_id(): app = Flask(__name__) app.config["TESTING"] = True Loading Loading @@ -557,6 +590,9 @@ def test_delete_app_runs_federated_cleanup_before_local_delete( mock_client = MagicMock() mock_client.get_app.return_value = { "appComponentSpecs": [{ "artefactId": "12345678-1234-1234-9234-123456789abc", }], "appManifest": { "appProvider": "Local Operator", } Loading Loading @@ -587,6 +623,43 @@ def test_cleanup_federated_app_handles_dict_delete_response(mock_jsonify): "message": "Deleted successfully", "status_code": 204, } federation_client.delete_artefact.return_value = { "message": "Deleted successfully", "status_code": 204, } result = app_controllers.cleanup_federated_app( federation_client=federation_client, feds=[{"_id": "fed-1", "token": "token-1"}], app_id="app-123", app_provider_id="Local Operator", normalize_federated_app_id=app_controllers._normalize_federated_app_id, artefact_id="12345678-1234-1234-9234-123456789abc", normalize_federated_artefact_id=app_controllers._normalize_federated_artefact_id, resolve_federated_app_identity=app_controllers._resolve_federated_app_identity, ) assert result == ("", 204) mock_jsonify.assert_not_called() federation_client.delete_artefact.assert_called_once_with( "fed-1", "f2175bf7-71e5-54aa-99f1-a72de468ac60", "token-1", ) @patch("edge_cloud_management_api.controllers.app_partner_orchestration.jsonify") def test_cleanup_federated_app_ignores_missing_artefact(mock_jsonify): federation_client = MagicMock() federation_client.get_all_app_instances.return_value = ([], 200) federation_client.delete_onboarded_app.return_value = { "message": "Deleted successfully", "status_code": 204, } federation_client.delete_artefact.return_value = { "error": "not found", "status_code": 404, } result = app_controllers.cleanup_federated_app( federation_client=federation_client, Loading @@ -594,6 +667,8 @@ def test_cleanup_federated_app_handles_dict_delete_response(mock_jsonify): app_id="app-123", app_provider_id="Local Operator", normalize_federated_app_id=app_controllers._normalize_federated_app_id, artefact_id="12345678-1234-1234-9234-123456789abc", normalize_federated_artefact_id=app_controllers._normalize_federated_artefact_id, resolve_federated_app_identity=app_controllers._resolve_federated_app_identity, ) Loading