Commit 0c2e6a53 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

fix: normalize federated app instance lookups and CAMARA responses

parent abb34b7b
Loading
Loading
Loading
Loading
+217 −108
Original line number Diff line number Diff line
@@ -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}-"
@@ -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):
@@ -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):
@@ -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")
@@ -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:
@@ -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")
@@ -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")
@@ -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):
+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():
@@ -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",
    }]