mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-16 05:45:32 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
539 lines
26 KiB
Python
539 lines
26 KiB
Python
import sys
|
|
from collections.abc import Callable
|
|
from datetime import timedelta
|
|
from typing import Any
|
|
|
|
from config.database import DataSourceRegistry
|
|
from plugins.core.artifacts import StoredArtifact
|
|
from plugins.core.deployment.catalog import PluginArtifactCatalog, artifact_payload
|
|
from plugins.core.deployment.config import PluginDeploymentConfig
|
|
from plugins.core.deployment.convergence import wait_for_release
|
|
from plugins.core.deployment.state import RUNTIME_PLUGIN_ID, aggregate_release
|
|
from plugins.core.discovery.scanner import DiscoveredPlugin, PluginScanner
|
|
from plugins.core.environment import PluginRuntimeEnvironmentService
|
|
from plugins.core.management.dao.dao import PluginDao
|
|
from plugins.core.management.dao.release_dao import PluginReleaseConflictError, PluginReleaseDao
|
|
from plugins.core.management.service.service import PluginService
|
|
from plugins.core.runtime.service.lifecycle_lock import NoopPluginLifecycleLock, RedisPluginLifecycleLock
|
|
from plugins.core.utils import validate_plugin_id_value
|
|
from plugins.core.validation.versioning import PluginVersionComparator
|
|
from utils.time_util import TimezoneUtil
|
|
|
|
|
|
def build_maintenance_runtime(config: PluginDeploymentConfig, plugins: list[DiscoveredPlugin]) -> Any:
|
|
"""
|
|
复用现有生命周期用例,为独立维护实例注入制品发现快照。
|
|
|
|
:param config: 制品发布配置快照
|
|
:param plugins: 候选制品及其依赖使用的插件发现快照
|
|
:return: 已配置维护模式和管理网关的插件运行时
|
|
"""
|
|
from plugins.core.management.service.gateway import PluginManagementRuntimeGateway # noqa: PLC0415
|
|
from plugins.core.runtime.service import PluginRuntimeService # noqa: PLC0415
|
|
from plugins.core.runtime.service.dependency_container import PluginRuntimeGatewayOverrides # noqa: PLC0415
|
|
|
|
environment = PluginRuntimeEnvironmentService(backend_root=config.backend_root, python_executable=sys.executable)
|
|
environment.backend_runtime_mode = 'maintenance'
|
|
gateway = PluginManagementRuntimeGateway()
|
|
runtime = PluginRuntimeService(
|
|
runtime_environment=environment,
|
|
gateways=PluginRuntimeGatewayOverrides(
|
|
config_gateway=gateway,
|
|
audit_gateway=gateway,
|
|
state_query_gateway=gateway,
|
|
migration_history_gateway=gateway,
|
|
purge_plan_gateway=gateway,
|
|
lifecycle_state_gateway=gateway,
|
|
lifecycle_uow_gateway=gateway,
|
|
migration_execution_gateway=gateway,
|
|
),
|
|
model_gateway=gateway,
|
|
command_gateway=gateway,
|
|
# 外层发布服务已经持有同一个全局生命周期锁,不能在此二次竞争。
|
|
lifecycle_lock=NoopPluginLifecycleLock(),
|
|
)
|
|
runtime.context.set_discovery_snapshot(plugins)
|
|
return runtime
|
|
|
|
|
|
class PluginDeploymentService:
|
|
"""
|
|
签名制品维护准备、发布目标选择及集群状态查询服务。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
config: PluginDeploymentConfig,
|
|
*,
|
|
session_factory: Any = None,
|
|
dao: Any = PluginReleaseDao,
|
|
lifecycle_lock: Any = None,
|
|
runtime_factory: Callable[..., Any] = build_maintenance_runtime,
|
|
) -> None:
|
|
"""
|
|
初始化签名制品维护发布服务。
|
|
|
|
:param config: 制品发布配置快照
|
|
:param session_factory: 异步数据库会话工厂,未提供时使用宿主数据源
|
|
:param dao: 制品、发布目标及进程报告的数据访问接口
|
|
:param lifecycle_lock: 与插件维护发布共用的生命周期锁
|
|
:param runtime_factory: 用于创建独立维护运行时的工厂
|
|
:return: None
|
|
"""
|
|
self.config = config
|
|
self.session_factory = session_factory or DataSourceRegistry.session
|
|
self.dao = dao
|
|
self.catalog = PluginArtifactCatalog(config, session_factory=self.session_factory, dao=dao)
|
|
self.lifecycle_lock = lifecycle_lock or RedisPluginLifecycleLock()
|
|
self.runtime_factory = runtime_factory
|
|
|
|
async def status(self, plugin_id: str | None = None) -> dict[str, Any]:
|
|
"""
|
|
汇总发布目标、维护准备证据及宿主进程实际加载状态。
|
|
|
|
:param plugin_id: 可选的插件ID过滤条件,为 None 时汇总全部发布记录
|
|
:return: 发布摘要列表和进程报告列表
|
|
"""
|
|
self.config.require_enabled()
|
|
if plugin_id is not None:
|
|
validate_plugin_id_value(plugin_id)
|
|
async with self.session_factory() as db:
|
|
releases = await self.dao.list_releases(db)
|
|
reports = await self.dao.list_worker_reports(db, plugin_id)
|
|
rows = []
|
|
for release in releases:
|
|
if plugin_id is not None and release.plugin_id != plugin_id:
|
|
continue
|
|
plugin = await PluginDao.get_plugin_by_id(db, release.plugin_id)
|
|
summary = aggregate_release(
|
|
release,
|
|
reports,
|
|
now=TimezoneUtil.utc_now(),
|
|
ttl_seconds=self.config.worker_ttl_seconds,
|
|
enabled=getattr(plugin, 'enabled', '1') == '0',
|
|
).to_payload()
|
|
summary.update(
|
|
installedVersion=getattr(plugin, 'installed_version', None),
|
|
enabled=getattr(plugin, 'enabled', '1') == '0',
|
|
preparedDigest=release.prepared_digest,
|
|
preparedVersion=release.prepared_version,
|
|
previousDigest=release.previous_digest,
|
|
)
|
|
rows.append(summary)
|
|
workers = [
|
|
{
|
|
'workerId': item.worker_id,
|
|
'pluginId': item.plugin_id,
|
|
'digest': item.artifact_digest,
|
|
'version': item.version,
|
|
'generation': item.generation,
|
|
'state': item.state,
|
|
'heartbeatTime': item.heartbeat_time.isoformat(),
|
|
'error': item.error,
|
|
}
|
|
for item in reports
|
|
]
|
|
return {'ok': True, 'releases': rows, 'workers': workers}
|
|
|
|
async def wait(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
*,
|
|
expected_generation: str,
|
|
expected_workers: int,
|
|
timeout_seconds: float = 300,
|
|
poll_interval_seconds: float = 2,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
只读等待调用方固定的发布目标在全部有效存活 worker 上收敛。
|
|
|
|
:param plugin_id: 固定验收的插件 ID
|
|
:param digest: 固定验收的制品 SHA256 摘要
|
|
:param expected_generation: 固定验收的发布代际
|
|
:param expected_workers: 必须与发布记录相同的预期 worker 数量下限
|
|
:param timeout_seconds: 总体等待秒数,零表示单次断言
|
|
:param poll_interval_seconds: 未收敛时的查询间隔秒数
|
|
:return: 固定目标的收敛结果及最后一次有效观测
|
|
:raises ValueError: 固定身份或等待参数不合法
|
|
:raises asyncio.CancelledError: 调用方取消等待
|
|
"""
|
|
return await wait_for_release(
|
|
self.status,
|
|
plugin_id,
|
|
digest,
|
|
expected_generation=expected_generation,
|
|
expected_workers=expected_workers,
|
|
worker_ttl_seconds=self.config.worker_ttl_seconds,
|
|
timeout_seconds=timeout_seconds,
|
|
poll_interval_seconds=poll_interval_seconds,
|
|
)
|
|
|
|
async def _require_quiescent(self) -> None:
|
|
"""
|
|
在全局锁内检查存活宿主报告,确认当前没有进程阻挡维护。
|
|
|
|
该检查依赖已上报的进程状态,调用方仍须先实际停止全部宿主进程。
|
|
|
|
:return: None
|
|
:raises ValueError: 仍存在有效期内且尚未停止的宿主进程报告
|
|
"""
|
|
cutoff = TimezoneUtil.utc_now() - timedelta(seconds=self.config.worker_ttl_seconds)
|
|
async with self.session_factory() as db:
|
|
reports = await self.dao.list_worker_reports(db)
|
|
if any(
|
|
item.plugin_id == RUNTIME_PLUGIN_ID and item.state != 'stopped' and item.heartbeat_time >= cutoff
|
|
for item in reports
|
|
):
|
|
raise ValueError('仍有存活的宿主 worker,请停止全部 worker 后执行维护发布')
|
|
|
|
@staticmethod
|
|
def _require_maintenance(maintenance: bool) -> None:
|
|
"""
|
|
要求调用方显式确认已进入停机维护窗口。
|
|
|
|
:param maintenance: 是否已显式确认在停止全部宿主进程后执行维护
|
|
:return: None
|
|
:raises ValueError: 调用方未显式确认停机维护
|
|
"""
|
|
if not maintenance:
|
|
raise ValueError('该操作必须在独立维护进程执行,并显式传入 --maintenance')
|
|
|
|
@staticmethod
|
|
def _require_clean_process(plugin: DiscoveredPlugin) -> None:
|
|
"""
|
|
拒绝已加载目标插件模块的进程继续执行制品维护准备。
|
|
|
|
:param plugin: 本次拟执行维护准备的插件发现对象
|
|
:return: None
|
|
:raises ValueError: 当前进程已经导入该插件的模块命名空间
|
|
"""
|
|
namespace = plugin.manifest.backend.module
|
|
if any(name == namespace or name.startswith(f'{namespace}.') for name in sys.modules):
|
|
raise ValueError('该维护进程已加载插件模块,请启动新进程执行,不能复用旧版本模块')
|
|
|
|
async def _snapshot(self, candidate: StoredArtifact) -> list[DiscoveredPlugin]:
|
|
"""
|
|
收集源码插件、候选制品和其他发布目标供静态依赖预检使用。
|
|
|
|
:param candidate: 本次拟预检的制品对象
|
|
:return: 未导入插件代码的发现结果列表
|
|
"""
|
|
sources = PluginScanner(self.config.backend_root / 'plugins').discover_with_errors().plugins
|
|
async with self.session_factory() as db:
|
|
selected = [(row.plugin_id, row.target_digest) for row in await self.dao.list_releases(db)]
|
|
result = {plugin.manifest.id: plugin for plugin in sources}
|
|
for plugin_id, digest in selected:
|
|
if digest and plugin_id != candidate.plugin_id:
|
|
artifact = await self.catalog.get(digest)
|
|
result[plugin_id] = self.catalog.discovered(artifact)
|
|
result[candidate.plugin_id] = self.catalog.discovered(candidate)
|
|
return list(result.values())
|
|
|
|
async def _read_state(self, plugin_id: str) -> tuple[Any, Any]:
|
|
"""
|
|
读取指定插件的发布记录和数据结构安装状态。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 发布记录与插件记录,缺失记录分别为 None
|
|
"""
|
|
async with self.session_factory() as db:
|
|
release = await self.dao.get_release(db, plugin_id)
|
|
plugin = await PluginDao.get_plugin_by_id(db, plugin_id)
|
|
return release, plugin
|
|
|
|
@staticmethod
|
|
def _has_prepared_evidence(release: Any, digest: str, installed: str | None) -> bool:
|
|
"""
|
|
检查制品摘要与已安装数据结构版本是否具有一致的维护准备证据。
|
|
|
|
:param release: 插件发布目标及维护准备记录
|
|
:param digest: 制品规范元数据的小写 SHA256 摘要
|
|
:param installed: 当前已安装的数据结构版本,未安装时为 None
|
|
:return: 是否存在完全匹配的维护准备证据
|
|
"""
|
|
return bool(
|
|
release is not None
|
|
and release.prepare_status == 'prepared'
|
|
and release.prepared_digest == digest
|
|
and release.prepared_version == installed
|
|
)
|
|
|
|
async def plan(self, plugin_id: str, digest: str) -> dict[str, Any]:
|
|
"""
|
|
静态预检制品及维护操作,区分数据结构准备与代码目标切换。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param digest: 制品规范元数据的小写 SHA256 摘要
|
|
:return: 可执行性、所需维护操作及准备证据说明
|
|
:raises ValueError: 请求的制品不属于指定插件或当前制品校验失败
|
|
"""
|
|
validate_plugin_id_value(plugin_id)
|
|
artifact = await self.catalog.get(digest)
|
|
if artifact.plugin_id != plugin_id:
|
|
raise ValueError('制品不属于请求的插件')
|
|
release, plugin = await self._read_state(plugin_id)
|
|
installed = getattr(plugin, 'installed_version', None)
|
|
operation = 'install' if not installed else 'upgrade'
|
|
comparison = PluginVersionComparator.compare(artifact.version, installed) if installed else 1
|
|
if installed and comparison != 1:
|
|
operation = 'reuse_prepared' if comparison == 0 else 'rollback_required'
|
|
runtime = self.runtime_factory(self.config, await self._snapshot(artifact))
|
|
if operation in {'install', 'upgrade'}:
|
|
precheck = await getattr(runtime, f'{operation}_plugin')(plugin_id, dry_run=True)
|
|
else:
|
|
precheck = await runtime.check_plugin_async(plugin_id)
|
|
preparation_matches = self._has_prepared_evidence(release, digest, installed)
|
|
same_version_ready = operation != 'reuse_prepared' or (installed == artifact.version and preparation_matches)
|
|
return {
|
|
'ok': bool(precheck.get('ok')) and operation != 'rollback_required' and same_version_ready,
|
|
'operation': operation,
|
|
**artifact_payload(artifact),
|
|
'installedVersion': installed,
|
|
'targetDigest': getattr(release, 'target_digest', None),
|
|
'generation': getattr(release, 'generation', None),
|
|
'restartRequired': True,
|
|
'maintenanceRequired': True,
|
|
'preparationMatches': preparation_matches,
|
|
'message': (
|
|
'同版本制品缺少匹配的维护准备证据;请提升版本后发布,不能猜测已安装内容'
|
|
if not same_version_ready
|
|
else str(precheck.get('message') or '')
|
|
),
|
|
'precheck': precheck,
|
|
}
|
|
|
|
async def prepare(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
*,
|
|
maintenance: bool = False,
|
|
actor: str | None = None,
|
|
dry_run: bool = False,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
在停机维护窗口执行安装或升级,并保存与制品绑定的准备证据。
|
|
|
|
准备成功不会自动选择发布目标;目标选择须通过单独的显式操作完成。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param digest: 制品规范元数据的小写 SHA256 摘要
|
|
:param maintenance: 是否已显式确认在停止全部宿主进程后执行维护
|
|
:param actor: 本次操作的执行者,未提供时不记录执行者名称
|
|
:param dry_run: 是否只执行预检而不写入文件或数据库
|
|
:return: 维护准备结果或只读预检计划
|
|
:raises ValueError: 维护条件、版本顺序或生命周期准备结果不符合发布要求
|
|
"""
|
|
if dry_run:
|
|
return await self.plan(plugin_id, digest)
|
|
self._require_maintenance(maintenance)
|
|
async with self.lifecycle_lock.lock(plugin_id, 'release_prepare') as lock:
|
|
if not lock.acquired:
|
|
raise ValueError(lock.message)
|
|
await self._require_quiescent()
|
|
artifact = await self.catalog.get(digest)
|
|
if artifact.plugin_id != validate_plugin_id_value(plugin_id):
|
|
raise ValueError('制品不属于请求的插件')
|
|
candidate = self.catalog.discovered(artifact)
|
|
self._require_clean_process(candidate)
|
|
release, plugin = await self._read_state(plugin_id)
|
|
installed = getattr(plugin, 'installed_version', None)
|
|
if installed == artifact.version:
|
|
if self._has_prepared_evidence(release, digest, installed):
|
|
return {'ok': True, 'message': '该制品已完成维护准备', **artifact_payload(artifact)}
|
|
raise ValueError('同版本制品缺少匹配的维护准备证据;请提升版本后发布,不能猜测已安装内容')
|
|
if installed and PluginVersionComparator.compare(artifact.version, installed) != 1:
|
|
raise ValueError('维护准备只允许升级;代码回滚请使用显式 rollback 命令')
|
|
async with self.session_factory() as db:
|
|
release = await self.dao.set_preparation_status(db, plugin_id, status='preparing', actor=actor)
|
|
generation = release.generation
|
|
await db.commit()
|
|
operation = 'install' if not installed else 'upgrade'
|
|
try:
|
|
# 插件可能在 Hook 内延迟 import;整个维护进程禁写字节码以保持制品完整性。
|
|
sys.dont_write_bytecode = True
|
|
runtime = self.runtime_factory(self.config, await self._snapshot(artifact))
|
|
result = await getattr(runtime, f'{operation}_plugin')(plugin_id, operated_by=actor)
|
|
if not result.get('ok'):
|
|
raise ValueError(str(result.get('message') or '插件生命周期准备失败'))
|
|
# Hook 不得修改自身发布目录;准备完成前重新检查全部字节。
|
|
await self.catalog.get(digest)
|
|
async with self.session_factory() as db:
|
|
current = await PluginDao.get_plugin_by_id(db, plugin_id)
|
|
if current is None or current.installed_version != artifact.version:
|
|
raise ValueError('维护准备后安装版本没有到达目标版本')
|
|
await self.dao.set_preparation_status(
|
|
db,
|
|
plugin_id,
|
|
status='prepared',
|
|
prepared_digest=digest,
|
|
prepared_version=current.installed_version,
|
|
actor=actor,
|
|
expected_generation=generation,
|
|
)
|
|
await db.commit()
|
|
return {
|
|
'ok': True,
|
|
'message': '维护准备完成;选择目标制品后再启动 worker',
|
|
'generation': generation,
|
|
'restartRequired': True,
|
|
**artifact_payload(artifact),
|
|
}
|
|
except BaseException as exc:
|
|
async with self.session_factory() as db:
|
|
await self.dao.set_preparation_status(
|
|
db,
|
|
plugin_id,
|
|
status='failed',
|
|
last_error=str(exc)[:2000] or type(exc).__name__,
|
|
actor=actor,
|
|
expected_generation=generation,
|
|
)
|
|
await db.commit()
|
|
raise
|
|
|
|
async def select(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
*,
|
|
expected_generation: str,
|
|
expected_workers: int = 1,
|
|
maintenance: bool = False,
|
|
actor: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
在匹配准备证据和发布代际的前提下选择目标制品。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param digest: 制品规范元数据的小写 SHA256 摘要
|
|
:param expected_generation: 调用方读取并确认的发布代际,用于并发状态校验
|
|
:param expected_workers: 预期启动并报告当前目标的宿主进程数量
|
|
:param maintenance: 是否已显式确认在停止全部宿主进程后执行维护
|
|
:param actor: 本次操作的执行者,未提供时不记录执行者名称
|
|
:return: 新发布代际、目标制品身份和重启要求
|
|
:raises ValueError: 维护条件、制品归属或准备证据不符合要求
|
|
:raises PluginReleaseConflictError: 发布代际在操作期间发生变化
|
|
"""
|
|
self._require_maintenance(maintenance)
|
|
async with self.lifecycle_lock.lock(plugin_id, 'release_select') as lock:
|
|
if not lock.acquired:
|
|
raise ValueError(lock.message)
|
|
await self._require_quiescent()
|
|
artifact = await self.catalog.get(digest)
|
|
if artifact.plugin_id != validate_plugin_id_value(plugin_id):
|
|
raise ValueError('目标制品不属于请求的插件')
|
|
async with self.session_factory() as db:
|
|
release = await self.dao.select_target(
|
|
db,
|
|
plugin_id,
|
|
target_digest=digest,
|
|
expected_generation=expected_generation,
|
|
expected_workers=expected_workers,
|
|
actor=actor,
|
|
)
|
|
payload = {
|
|
'ok': True,
|
|
'operation': 'release_select',
|
|
'message': '发布目标已更新,等待全部 worker 重启并报告实际版本',
|
|
'generation': release.generation,
|
|
'restartRequired': True,
|
|
'expectedWorkers': expected_workers,
|
|
**artifact_payload(artifact),
|
|
}
|
|
await self._audit(db, payload, actor)
|
|
await db.commit()
|
|
return payload
|
|
|
|
async def rollback(
|
|
self,
|
|
plugin_id: str,
|
|
*,
|
|
expected_generation: str,
|
|
schema_compatible: bool = False,
|
|
maintenance: bool = False,
|
|
actor: str | None = None,
|
|
) -> dict[str, Any]:
|
|
"""
|
|
切回上一代码制品并重建声明资源,保留当前数据结构安装版本。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param expected_generation: 调用方读取并确认的发布代际,用于并发状态校验
|
|
:param schema_compatible: 是否明确确认旧代码兼容当前数据结构,回滚不会撤销数据库迁移
|
|
:param maintenance: 是否已显式确认在停止全部宿主进程后执行维护
|
|
:param actor: 本次操作的执行者,未提供时不记录执行者名称
|
|
:return: 回滚后的发布代际、代码制品身份及保留的安装版本
|
|
:raises ValueError: 未确认结构兼容性、没有上一制品或回滚预检失败
|
|
:raises PluginReleaseConflictError: 发布代际在操作期间发生变化
|
|
"""
|
|
self._require_maintenance(maintenance)
|
|
if not schema_compatible:
|
|
raise ValueError('代码回滚不会撤销数据库迁移,须确认旧代码兼容当前数据结构并传入 --schema-compatible')
|
|
async with self.lifecycle_lock.lock(plugin_id, 'release_rollback') as lock:
|
|
if not lock.acquired:
|
|
raise ValueError(lock.message)
|
|
await self._require_quiescent()
|
|
release, plugin = await self._read_state(validate_plugin_id_value(plugin_id))
|
|
if release is None or release.generation != expected_generation:
|
|
raise PluginReleaseConflictError('发布代际已变化,请重新读取目标')
|
|
if not release.previous_digest or plugin is None or not plugin.installed_version:
|
|
raise ValueError('没有可回滚的上一制品或数据结构安装状态')
|
|
artifact = await self.catalog.get(release.previous_digest)
|
|
candidate = self.catalog.discovered(artifact)
|
|
runtime = self.runtime_factory(self.config, await self._snapshot(artifact))
|
|
check = await runtime.check_plugin_async(plugin_id)
|
|
if not check.get('ok'):
|
|
raise ValueError(str(check.get('message') or '回滚制品预检失败'))
|
|
async with self.session_factory() as db:
|
|
# 重建菜单/配置/任务声明,绝不执行旧版迁移、seed或Hook。
|
|
await PluginService.upsert_discovered_plugin_services(
|
|
db, candidate, self.config.backend_root / 'plugins'
|
|
)
|
|
await PluginService.install_plugin_menu_services(db, candidate, enabled=plugin.enabled == '0')
|
|
await PluginService.install_plugin_default_config_services(db, candidate)
|
|
await PluginService.install_plugin_job_services(db, candidate, enabled=plugin.enabled == '0')
|
|
await self.dao.set_preparation_status(
|
|
db,
|
|
plugin_id,
|
|
status='prepared',
|
|
prepared_digest=artifact.digest,
|
|
prepared_version=plugin.installed_version,
|
|
actor=actor,
|
|
expected_generation=expected_generation,
|
|
)
|
|
selected = await self.dao.select_target(
|
|
db,
|
|
plugin_id,
|
|
target_digest=artifact.digest,
|
|
expected_generation=expected_generation,
|
|
expected_workers=release.expected_workers,
|
|
actor=actor,
|
|
)
|
|
payload = {
|
|
'ok': True,
|
|
'operation': 'release_rollback',
|
|
'message': '已选择上一代码制品,数据库版本保持不变;请重启全部 worker',
|
|
'installedVersion': plugin.installed_version,
|
|
'generation': selected.generation,
|
|
'restartRequired': True,
|
|
**artifact_payload(artifact),
|
|
}
|
|
await self._audit(db, payload, actor)
|
|
await db.commit()
|
|
return payload
|
|
|
|
@staticmethod
|
|
async def _audit(db: Any, payload: dict[str, Any], actor: str | None) -> None:
|
|
"""
|
|
在调用方事务中记录发布操作审计,审计失败时向上传播异常。
|
|
|
|
:param db: 当前操作使用的数据库会话
|
|
:param payload: 需要写入审计的操作结果数据
|
|
:param actor: 本次操作的执行者,未提供时不记录执行者名称
|
|
:return: None
|
|
"""
|
|
await PluginService.add_plugin_operation_log_services(
|
|
db, {**payload, 'operatedBy': actor}, dry_run=False, continue_on_error=False
|
|
)
|