mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
feat: enterprise plugin pre uninstall (#33158)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@ -18,3 +18,7 @@ class EnterpriseFeatureConfig(BaseSettings):
|
|||||||
description="Allow customization of the enterprise logo.",
|
description="Allow customization of the enterprise logo.",
|
||||||
default=False,
|
default=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
ENTERPRISE_REQUEST_TIMEOUT: int = Field(
|
||||||
|
ge=1, description="Maximum timeout in seconds for enterprise requests", default=5
|
||||||
|
)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import logging
|
|||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
from configs import dify_config
|
||||||
from services.enterprise.base import EnterprisePluginManagerRequest
|
from services.enterprise.base import EnterprisePluginManagerRequest
|
||||||
from services.errors.base import BaseServiceError
|
from services.errors.base import BaseServiceError
|
||||||
|
|
||||||
@ -28,6 +29,11 @@ class CheckCredentialPolicyComplianceRequest(BaseModel):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
class PreUninstallPluginRequest(BaseModel):
|
||||||
|
tenant_id: str
|
||||||
|
plugin_unique_identifier: str
|
||||||
|
|
||||||
|
|
||||||
class CredentialPolicyViolationError(BaseServiceError):
|
class CredentialPolicyViolationError(BaseServiceError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -55,3 +61,21 @@ class PluginManagerService:
|
|||||||
body.dify_credential_id,
|
body.dify_credential_id,
|
||||||
ret.get("result", False),
|
ret.get("result", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def try_pre_uninstall_plugin(cls, body: PreUninstallPluginRequest):
|
||||||
|
try:
|
||||||
|
# the invocation must be synchronous.
|
||||||
|
EnterprisePluginManagerRequest.send_request(
|
||||||
|
"POST",
|
||||||
|
"/pre-uninstall-plugin",
|
||||||
|
json=body.model_dump(),
|
||||||
|
raise_for_status=True,
|
||||||
|
timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.exception(
|
||||||
|
"failed to perform pre uninstall plugin hook. tenant_id: %s, plugin_unique_identifier: %s",
|
||||||
|
body.tenant_id,
|
||||||
|
body.plugin_unique_identifier,
|
||||||
|
)
|
||||||
|
|||||||
@ -32,6 +32,10 @@ from extensions.ext_database import db
|
|||||||
from extensions.ext_redis import redis_client
|
from extensions.ext_redis import redis_client
|
||||||
from models.provider import Provider, ProviderCredential
|
from models.provider import Provider, ProviderCredential
|
||||||
from models.provider_ids import GenericProviderID
|
from models.provider_ids import GenericProviderID
|
||||||
|
from services.enterprise.plugin_manager_service import (
|
||||||
|
PluginManagerService,
|
||||||
|
PreUninstallPluginRequest,
|
||||||
|
)
|
||||||
from services.errors.plugin import PluginInstallationForbiddenError
|
from services.errors.plugin import PluginInstallationForbiddenError
|
||||||
from services.feature_service import FeatureService, PluginInstallationScope
|
from services.feature_service import FeatureService, PluginInstallationScope
|
||||||
|
|
||||||
@ -519,6 +523,13 @@ class PluginService:
|
|||||||
if not plugin:
|
if not plugin:
|
||||||
return manager.uninstall(tenant_id, plugin_installation_id)
|
return manager.uninstall(tenant_id, plugin_installation_id)
|
||||||
|
|
||||||
|
if dify_config.ENTERPRISE_ENABLED:
|
||||||
|
PluginManagerService.try_pre_uninstall_plugin(
|
||||||
|
PreUninstallPluginRequest(
|
||||||
|
tenant_id=tenant_id,
|
||||||
|
plugin_unique_identifier=plugin.plugin_unique_identifier,
|
||||||
|
)
|
||||||
|
)
|
||||||
with Session(db.engine) as session, session.begin():
|
with Session(db.engine) as session, session.begin():
|
||||||
plugin_id = plugin.plugin_id
|
plugin_id = plugin.plugin_id
|
||||||
logger.info("Deleting credentials for plugin: %s", plugin_id)
|
logger.info("Deleting credentials for plugin: %s", plugin_id)
|
||||||
|
|||||||
@ -0,0 +1,93 @@
|
|||||||
|
"""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 configs import dify_config
|
||||||
|
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,
|
||||||
|
timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
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_with(
|
||||||
|
"POST",
|
||||||
|
"/pre-uninstall-plugin",
|
||||||
|
json={"tenant_id": "tenant-789", "plugin_unique_identifier": "com.example.failing_plugin"},
|
||||||
|
raise_for_status=True,
|
||||||
|
timeout=dify_config.ENTERPRISE_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
mock_logger.exception.assert_called_once()
|
||||||
Reference in New Issue
Block a user