feat: add enterprise pre uninstall hook for plugin

This commit is contained in:
yunlu.wen 2026-03-09 15:31:56 +08:00
parent 825765231b
commit e1adf73ad5
5 changed files with 127 additions and 0 deletions

View File

@ -64,3 +64,7 @@ class EnterpriseTelemetryConfig(BaseSettings):
description="Sampling rate for enterprise traces (0.0 to 1.0, default 1.0 = 100%).",
default=1.0,
)
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
)

View File

@ -7,3 +7,4 @@ class BasePluginEntity(BaseModel):
id: str
created_at: datetime
updated_at: datetime
plugin_unique_identifier: str

View File

@ -3,6 +3,7 @@ import logging
from pydantic import BaseModel
from configs import dify_config
from services.enterprise.base import EnterprisePluginManagerRequest
from services.errors.base import BaseServiceError
@ -28,6 +29,11 @@ class CheckCredentialPolicyComplianceRequest(BaseModel):
return data
class PreUninstallPluginRequest(BaseModel):
tenant_id: str
plugin_unique_identifier: str
class CredentialPolicyViolationError(BaseServiceError):
pass
@ -55,3 +61,24 @@ class PluginManagerService:
body.dify_credential_id,
ret.get("result", False),
)
@classmethod
def try_pre_uninstall_plugin(cls, body: PreUninstallPluginRequest):
try:
# the invocation must be synchronous.
EnterprisePluginManagerRequest.send_request( # pyright: ignore[reportUnknownMemberType]
"POST",
"/pre-uninstall-plugin",
json=body.model_dump(), # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]
raise_for_status=True,
timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT,
)
except Exception as e:
logger.exception(
"""
failed to perform pre uninstall plugin hook. tenant_id: %s, plugin_unique_identifier: %s,
this may cause plugin %s to be automatically garbage collected
""",
body.tenant_id,
body.plugin_unique_identifier,
)

View File

@ -30,6 +30,10 @@ from extensions.ext_database import db
from extensions.ext_redis import redis_client
from models.provider import ProviderCredential
from models.provider_ids import GenericProviderID
from services.enterprise.plugin_manager_service import (
PluginManagerService,
PreUninstallPluginRequest,
)
from services.errors.plugin import PluginInstallationForbiddenError
from services.feature_service import FeatureService, PluginInstallationScope
@ -518,6 +522,13 @@ class PluginService:
if plugin:
plugin_id = plugin.plugin_id
logger.info("Deleting credentials for plugin: %s", plugin_id)
if dify_config.ENTERPRISE_ENABLED:
PluginManagerService.try_pre_uninstall_plugin(
PreUninstallPluginRequest(
tenant_id=tenant_id,
plugin_unique_identifier=plugin.plugin_unique_identifier,
)
)
# Delete provider credentials that match this plugin
credentials = db.session.scalars(

View File

@ -0,0 +1,84 @@
"""Unit tests for PluginManagerService.
This module covers the pre-uninstall plugin hook behavior:
- Successful API call: no exception raised, correct request sent
- API failure: soft-fail (logs and does not re-raise)
"""
from unittest.mock import patch
from httpx import HTTPStatusError
from services.enterprise.plugin_manager_service import (
PluginManagerService,
PreUninstallPluginRequest,
)
class TestTryPreUninstallPlugin:
def test_try_pre_uninstall_plugin_success(self):
body = PreUninstallPluginRequest(
tenant_id="tenant-123",
plugin_unique_identifier="com.example.my_plugin",
)
with patch(
"services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request"
) as mock_send_request:
mock_send_request.return_value = {}
PluginManagerService.try_pre_uninstall_plugin(body)
mock_send_request.assert_called_once_with(
"POST",
"/pre-uninstall-plugin",
json={"tenant_id": "tenant-123", "plugin_unique_identifier": "com.example.my_plugin"},
raise_for_status=True,
)
def test_try_pre_uninstall_plugin_http_error_soft_fails(self):
body = PreUninstallPluginRequest(
tenant_id="tenant-456",
plugin_unique_identifier="com.example.other_plugin",
)
with (
patch(
"services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request"
) as mock_send_request,
patch("services.enterprise.plugin_manager_service.logger") as mock_logger,
):
mock_send_request.side_effect = HTTPStatusError(
"502 Bad Gateway",
request=None,
response=None,
)
PluginManagerService.try_pre_uninstall_plugin(body)
mock_send_request.assert_called_once_with(
"POST",
"/pre-uninstall-plugin",
json={"tenant_id": "tenant-456", "plugin_unique_identifier": "com.example.other_plugin"},
raise_for_status=True,
)
mock_logger.exception.assert_called_once()
def test_try_pre_uninstall_plugin_generic_exception_soft_fails(self):
body = PreUninstallPluginRequest(
tenant_id="tenant-789",
plugin_unique_identifier="com.example.failing_plugin",
)
with (
patch(
"services.enterprise.plugin_manager_service.EnterprisePluginManagerRequest.send_request"
) as mock_send_request,
patch("services.enterprise.plugin_manager_service.logger") as mock_logger,
):
mock_send_request.side_effect = ConnectionError("network unreachable")
PluginManagerService.try_pre_uninstall_plugin(body)
mock_send_request.assert_called_once()
mock_logger.exception.assert_called_once()