Commit 34c5fc85 authored by Sergio Gimenez's avatar Sergio Gimenez
Browse files

fix(federation): delete federated artefacts on app cleanup

parent 1e2f9d7e
Loading
Loading
Loading
Loading
+33 −14
Original line number Diff line number Diff line
@@ -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):
@@ -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:
@@ -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]
@@ -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(
+27 −5
Original line number Diff line number Diff line
@@ -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")
@@ -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

+23 −0
Original line number Diff line number Diff line
@@ -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
+75 −0
Original line number Diff line number Diff line
@@ -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
@@ -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",
        }
@@ -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,
@@ -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,
    )