Loading edge_cloud_management_api/controllers/app_controllers.py +217 −108 Original line number Diff line number Diff line Loading @@ -123,6 +123,85 @@ def _normalize_federated_app_provider_id(app_provider_id, fallback_source): ) def _resolve_federated_app_provider_id(app_id_value, app_provider_id): if not app_provider_id: return None return _normalize_federated_app_provider_id(app_provider_id, app_id_value) def _normalize_local_app_instance(instance): if not isinstance(instance, dict): return None app_instance_id = instance.get("appInstanceId") or instance.get("appInstIdentifier") if not app_instance_id: return None status = instance.get("status") or instance.get("appInstanceState") or "unknown" edge_cloud_zone = instance.get("edgeCloudZone") if not isinstance(edge_cloud_zone, dict): zone_id = instance.get("edgeCloudZoneId") or instance.get("zoneId") zone_name = instance.get("edgeCloudZoneName") or instance.get("zoneName") or "unknown" edge_cloud_provider = instance.get("edgeCloudProvider") or "unknown" if zone_id: edge_cloud_zone = { "edgeCloudZoneId": zone_id, "edgeCloudZoneName": zone_name, "edgeCloudProvider": edge_cloud_provider, "edgeCloudZoneStatus": instance.get("edgeCloudZoneStatus") or "unknown", "edgeCloudRegion": instance.get("edgeCloudRegion") or "unknown", } normalized = { "appInstanceId": app_instance_id, "status": status, } if instance.get("appId"): normalized["appId"] = instance.get("appId") if edge_cloud_zone: normalized["edgeCloudZone"] = edge_cloud_zone if instance.get("componentEndpointInfo"): normalized["componentEndpointInfo"] = instance.get("componentEndpointInfo") elif instance.get("accesspointInfo"): normalized["componentEndpointInfo"] = instance.get("accesspointInfo") if instance.get("kubernetesClusterRef"): normalized["kubernetesClusterRef"] = instance.get("kubernetesClusterRef") return normalized def _normalize_federated_app_instances(fed_instances, zone_provider=None, region=None): normalized_instances = [] if not isinstance(fed_instances, list): return normalized_instances for zone_info in fed_instances: if not isinstance(zone_info, dict): continue zone_id = zone_info.get("zoneId") for instance in zone_info.get("appInstanceInfo", []) or []: if not isinstance(instance, dict): continue app_instance_id = instance.get("appInstIdentifier") or instance.get("appInstanceId") if not app_instance_id: continue normalized = { "appInstanceId": app_instance_id, "status": instance.get("appInstanceState") or "unknown", } if instance.get("appId"): normalized["appId"] = instance.get("appId") if zone_id: normalized["edgeCloudZone"] = { "edgeCloudZoneId": zone_id, "edgeCloudZoneName": "unknown", "edgeCloudProvider": zone_provider or "unknown", "edgeCloudZoneStatus": "unknown", "edgeCloudRegion": region or "unknown", } normalized_instances.append(normalized) return normalized_instances def _normalize_federated_artefact_id(federation_context_id, artefact_id, fallback_source): if artefact_id and re.match( r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-" Loading Loading @@ -215,10 +294,11 @@ def delete_app(appId, x_correlator=None): if not federation_context_id or not fed_token: continue if app_provider_id: federated_app_provider_id = _resolve_federated_app_provider_id(appId, app_provider_id) fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=appId, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): Loading Loading @@ -680,20 +760,29 @@ def create_app_instance(): "details": str(e) }), 500 def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, region=None): def get_app_instance(app_id=None, appId=None, x_correlator=None, xCorrelator=None, app_instance_id=None, appInstanceId=None, region=None): """ Retrieve application instances from the database. Supports filtering by app_id, app_instance_id, and region. """ try: app_id = app_id or appId app_instance_id = app_instance_id or appInstanceId instances = [] pi_edge_client_factory = PiEdgeAPIClientFactory() pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() if app_instance_id is None: local_instances = pi_edge_client.get_app_instances()["appInstances"] if isinstance(local_instances, list): instances.extend(local_instances) local_response = pi_edge_client.get_app_instances() local_instances = [] if isinstance(local_response, dict): local_instances = local_response.get("appInstances", []) elif isinstance(local_response, list): local_instances = local_response for instance in local_instances: normalized_instance = _normalize_local_app_instance(instance) if normalized_instance: instances.append(normalized_instance) def resolve_app_provider(app_id_value, app_payload=None): if isinstance(app_payload, dict): Loading Loading @@ -721,6 +810,7 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio if app_id: app_provider_id = resolve_app_provider(app_id) if app_provider_id: federated_app_provider_id = _resolve_federated_app_provider_id(app_id, app_provider_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading @@ -729,11 +819,17 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): instances.extend(fed_instances) if fed_code == 200: instances.extend( _normalize_federated_app_instances( fed_instances, zone_provider=(fed.get("partnerOPFederationId") or "unknown"), region=region, ) ) else: logger.info("Skipping federated lookup; no appProviderId for appId=%s", app_id) else: Loading @@ -751,6 +847,7 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio app_provider_map[app_id_value] = provider for app_id_value, app_provider_id in app_provider_map.items(): federated_app_provider_id = _resolve_federated_app_provider_id(app_id_value, app_provider_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading @@ -759,20 +856,31 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id_value, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): instances.extend(fed_instances) if fed_code == 200: instances.extend( _normalize_federated_app_instances( fed_instances, zone_provider=(fed.get("partnerOPFederationId") or "unknown"), region=region, ) ) if not instances: return jsonify({ "status": 404, "code": "NOT_FOUND", "message": "No application instances found for the given parameters." }), 404 if app_instance_id: instances = [ instance for instance in instances if instance.get("appInstanceId") == app_instance_id ] if region: instances = [ instance for instance in instances if isinstance(instance.get("edgeCloudZone"), dict) and instance["edgeCloudZone"].get("edgeCloudRegion") == region ] return jsonify({"appInstanceInfo": instances}), 200 return jsonify(instances), 200 except Exception as e: logger.exception("Failed to retrieve app instances") Loading Loading @@ -850,10 +958,11 @@ def delete_app_instance(appInstanceId: str, x_correlator=None): if not federation_context_id or not fed_token: continue for app_id_value, app_provider_id in app_provider_map.items(): federated_app_provider_id = _resolve_federated_app_provider_id(app_id_value, app_provider_id) fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id_value, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code != 200 or not isinstance(fed_instances, list): Loading tests/unit/controllers/test_app_controllers.py +103 −0 Original line number Diff line number Diff line from edge_cloud_management_api.controllers import app_controllers from unittest.mock import MagicMock, patch from flask import Flask def test_split_image_reference_preserves_helm_repo_url(): Loading @@ -17,3 +20,103 @@ def test_split_image_reference_preserves_container_registry_behavior(): assert repo_url == "ghcr.io" assert image_ref == "example/image:1.2.3" def test_resolve_federated_app_provider_id_normalizes_local_provider_name(): provider_id = app_controllers._resolve_federated_app_provider_id( "app-123", "Local Operator", ) assert provider_id == "providerapp123" @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds") @patch("edge_cloud_management_api.controllers.app_controllers.federation_client") @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_uses_normalized_provider_for_federated_lookup( mock_factory_class, mock_federation_client, mock_get_all_feds, ): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = {"appInstances": []} mock_client.get_app.return_value = { "appManifest": { "appProvider": "Local Operator", } } mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client mock_get_all_feds.return_value = [{"_id": "fed-1", "token": "token-1", "partnerOPFederationId": "Remote Operator"}] mock_federation_client.get_all_app_instances.return_value = ([{ "zoneId": "default", "appInstanceInfo": [{"appInstIdentifier": "inst-1", "appInstanceState": "ready"}], }], 200) with app.test_request_context(): response, status_code = app_controllers.get_app_instance(app_id="app-123") assert status_code == 200 assert response.get_json() == [{ "appInstanceId": "inst-1", "status": "ready", "edgeCloudZone": { "edgeCloudZoneId": "default", "edgeCloudZoneName": "unknown", "edgeCloudProvider": "Remote Operator", "edgeCloudZoneStatus": "unknown", "edgeCloudRegion": "unknown", }, }] mock_federation_client.get_all_app_instances.assert_called_once_with( federation_context_id="fed-1", app_id="app-123", app_provider_id="providerapp123", token="token-1", ) @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds", return_value=[]) @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_returns_empty_list_when_no_instances(mock_factory_class, _mock_get_all_feds): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = {"appInstances": []} mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client with app.test_request_context(): response, status_code = app_controllers.get_app_instance() assert status_code == 200 assert response.get_json() == [] @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds", return_value=[]) @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_supports_openapi_query_param_names(mock_factory_class, _mock_get_all_feds): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = { "appInstances": [ {"appInstanceId": "inst-1", "appId": "app-1", "status": "ready"}, {"appInstanceId": "inst-2", "appId": "app-2", "status": "failed"}, ] } mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client with app.test_request_context(): response, status_code = app_controllers.get_app_instance(appId="app-1", appInstanceId="inst-1") assert status_code == 200 assert response.get_json() == [{ "appId": "app-1", "appInstanceId": "inst-1", "status": "ready", }] Loading
edge_cloud_management_api/controllers/app_controllers.py +217 −108 Original line number Diff line number Diff line Loading @@ -123,6 +123,85 @@ def _normalize_federated_app_provider_id(app_provider_id, fallback_source): ) def _resolve_federated_app_provider_id(app_id_value, app_provider_id): if not app_provider_id: return None return _normalize_federated_app_provider_id(app_provider_id, app_id_value) def _normalize_local_app_instance(instance): if not isinstance(instance, dict): return None app_instance_id = instance.get("appInstanceId") or instance.get("appInstIdentifier") if not app_instance_id: return None status = instance.get("status") or instance.get("appInstanceState") or "unknown" edge_cloud_zone = instance.get("edgeCloudZone") if not isinstance(edge_cloud_zone, dict): zone_id = instance.get("edgeCloudZoneId") or instance.get("zoneId") zone_name = instance.get("edgeCloudZoneName") or instance.get("zoneName") or "unknown" edge_cloud_provider = instance.get("edgeCloudProvider") or "unknown" if zone_id: edge_cloud_zone = { "edgeCloudZoneId": zone_id, "edgeCloudZoneName": zone_name, "edgeCloudProvider": edge_cloud_provider, "edgeCloudZoneStatus": instance.get("edgeCloudZoneStatus") or "unknown", "edgeCloudRegion": instance.get("edgeCloudRegion") or "unknown", } normalized = { "appInstanceId": app_instance_id, "status": status, } if instance.get("appId"): normalized["appId"] = instance.get("appId") if edge_cloud_zone: normalized["edgeCloudZone"] = edge_cloud_zone if instance.get("componentEndpointInfo"): normalized["componentEndpointInfo"] = instance.get("componentEndpointInfo") elif instance.get("accesspointInfo"): normalized["componentEndpointInfo"] = instance.get("accesspointInfo") if instance.get("kubernetesClusterRef"): normalized["kubernetesClusterRef"] = instance.get("kubernetesClusterRef") return normalized def _normalize_federated_app_instances(fed_instances, zone_provider=None, region=None): normalized_instances = [] if not isinstance(fed_instances, list): return normalized_instances for zone_info in fed_instances: if not isinstance(zone_info, dict): continue zone_id = zone_info.get("zoneId") for instance in zone_info.get("appInstanceInfo", []) or []: if not isinstance(instance, dict): continue app_instance_id = instance.get("appInstIdentifier") or instance.get("appInstanceId") if not app_instance_id: continue normalized = { "appInstanceId": app_instance_id, "status": instance.get("appInstanceState") or "unknown", } if instance.get("appId"): normalized["appId"] = instance.get("appId") if zone_id: normalized["edgeCloudZone"] = { "edgeCloudZoneId": zone_id, "edgeCloudZoneName": "unknown", "edgeCloudProvider": zone_provider or "unknown", "edgeCloudZoneStatus": "unknown", "edgeCloudRegion": region or "unknown", } normalized_instances.append(normalized) return normalized_instances def _normalize_federated_artefact_id(federation_context_id, artefact_id, fallback_source): if artefact_id and re.match( r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-" Loading Loading @@ -215,10 +294,11 @@ def delete_app(appId, x_correlator=None): if not federation_context_id or not fed_token: continue if app_provider_id: federated_app_provider_id = _resolve_federated_app_provider_id(appId, app_provider_id) fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=appId, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): Loading Loading @@ -680,20 +760,29 @@ def create_app_instance(): "details": str(e) }), 500 def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, region=None): def get_app_instance(app_id=None, appId=None, x_correlator=None, xCorrelator=None, app_instance_id=None, appInstanceId=None, region=None): """ Retrieve application instances from the database. Supports filtering by app_id, app_instance_id, and region. """ try: app_id = app_id or appId app_instance_id = app_instance_id or appInstanceId instances = [] pi_edge_client_factory = PiEdgeAPIClientFactory() pi_edge_client = pi_edge_client_factory.create_pi_edge_api_client() if app_instance_id is None: local_instances = pi_edge_client.get_app_instances()["appInstances"] if isinstance(local_instances, list): instances.extend(local_instances) local_response = pi_edge_client.get_app_instances() local_instances = [] if isinstance(local_response, dict): local_instances = local_response.get("appInstances", []) elif isinstance(local_response, list): local_instances = local_response for instance in local_instances: normalized_instance = _normalize_local_app_instance(instance) if normalized_instance: instances.append(normalized_instance) def resolve_app_provider(app_id_value, app_payload=None): if isinstance(app_payload, dict): Loading Loading @@ -721,6 +810,7 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio if app_id: app_provider_id = resolve_app_provider(app_id) if app_provider_id: federated_app_provider_id = _resolve_federated_app_provider_id(app_id, app_provider_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading @@ -729,11 +819,17 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): instances.extend(fed_instances) if fed_code == 200: instances.extend( _normalize_federated_app_instances( fed_instances, zone_provider=(fed.get("partnerOPFederationId") or "unknown"), region=region, ) ) else: logger.info("Skipping federated lookup; no appProviderId for appId=%s", app_id) else: Loading @@ -751,6 +847,7 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio app_provider_map[app_id_value] = provider for app_id_value, app_provider_id in app_provider_map.items(): federated_app_provider_id = _resolve_federated_app_provider_id(app_id_value, app_provider_id) for fed in feds: fed_token = fed.get("token") federation_context_id = fed.get("_id") Loading @@ -759,20 +856,31 @@ def get_app_instance(app_id=None, x_correlator=None, app_instance_id=None, regio fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id_value, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code == 200 and isinstance(fed_instances, list): instances.extend(fed_instances) if fed_code == 200: instances.extend( _normalize_federated_app_instances( fed_instances, zone_provider=(fed.get("partnerOPFederationId") or "unknown"), region=region, ) ) if not instances: return jsonify({ "status": 404, "code": "NOT_FOUND", "message": "No application instances found for the given parameters." }), 404 if app_instance_id: instances = [ instance for instance in instances if instance.get("appInstanceId") == app_instance_id ] if region: instances = [ instance for instance in instances if isinstance(instance.get("edgeCloudZone"), dict) and instance["edgeCloudZone"].get("edgeCloudRegion") == region ] return jsonify({"appInstanceInfo": instances}), 200 return jsonify(instances), 200 except Exception as e: logger.exception("Failed to retrieve app instances") Loading Loading @@ -850,10 +958,11 @@ def delete_app_instance(appInstanceId: str, x_correlator=None): if not federation_context_id or not fed_token: continue for app_id_value, app_provider_id in app_provider_map.items(): federated_app_provider_id = _resolve_federated_app_provider_id(app_id_value, app_provider_id) fed_instances, fed_code = federation_client.get_all_app_instances( federation_context_id=federation_context_id, app_id=app_id_value, app_provider_id=app_provider_id, app_provider_id=federated_app_provider_id, token=fed_token, ) if fed_code != 200 or not isinstance(fed_instances, list): Loading
tests/unit/controllers/test_app_controllers.py +103 −0 Original line number Diff line number Diff line from edge_cloud_management_api.controllers import app_controllers from unittest.mock import MagicMock, patch from flask import Flask def test_split_image_reference_preserves_helm_repo_url(): Loading @@ -17,3 +20,103 @@ def test_split_image_reference_preserves_container_registry_behavior(): assert repo_url == "ghcr.io" assert image_ref == "example/image:1.2.3" def test_resolve_federated_app_provider_id_normalizes_local_provider_name(): provider_id = app_controllers._resolve_federated_app_provider_id( "app-123", "Local Operator", ) assert provider_id == "providerapp123" @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds") @patch("edge_cloud_management_api.controllers.app_controllers.federation_client") @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_uses_normalized_provider_for_federated_lookup( mock_factory_class, mock_federation_client, mock_get_all_feds, ): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = {"appInstances": []} mock_client.get_app.return_value = { "appManifest": { "appProvider": "Local Operator", } } mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client mock_get_all_feds.return_value = [{"_id": "fed-1", "token": "token-1", "partnerOPFederationId": "Remote Operator"}] mock_federation_client.get_all_app_instances.return_value = ([{ "zoneId": "default", "appInstanceInfo": [{"appInstIdentifier": "inst-1", "appInstanceState": "ready"}], }], 200) with app.test_request_context(): response, status_code = app_controllers.get_app_instance(app_id="app-123") assert status_code == 200 assert response.get_json() == [{ "appInstanceId": "inst-1", "status": "ready", "edgeCloudZone": { "edgeCloudZoneId": "default", "edgeCloudZoneName": "unknown", "edgeCloudProvider": "Remote Operator", "edgeCloudZoneStatus": "unknown", "edgeCloudRegion": "unknown", }, }] mock_federation_client.get_all_app_instances.assert_called_once_with( federation_context_id="fed-1", app_id="app-123", app_provider_id="providerapp123", token="token-1", ) @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds", return_value=[]) @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_returns_empty_list_when_no_instances(mock_factory_class, _mock_get_all_feds): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = {"appInstances": []} mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client with app.test_request_context(): response, status_code = app_controllers.get_app_instance() assert status_code == 200 assert response.get_json() == [] @patch("edge_cloud_management_api.controllers.app_controllers.get_all_feds", return_value=[]) @patch("edge_cloud_management_api.controllers.app_controllers.PiEdgeAPIClientFactory") def test_get_app_instance_supports_openapi_query_param_names(mock_factory_class, _mock_get_all_feds): app = Flask(__name__) app.config["TESTING"] = True mock_client = MagicMock() mock_client.get_app_instances.return_value = { "appInstances": [ {"appInstanceId": "inst-1", "appId": "app-1", "status": "ready"}, {"appInstanceId": "inst-2", "appId": "app-2", "status": "failed"}, ] } mock_factory_class.return_value.create_pi_edge_api_client.return_value = mock_client with app.test_request_context(): response, status_code = app_controllers.get_app_instance(appId="app-1", appInstanceId="inst-1") assert status_code == 200 assert response.get_json() == [{ "appId": "app-1", "appInstanceId": "inst-1", "status": "ready", }]