mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-14 05:00:07 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
656 lines
25 KiB
Python
656 lines
25 KiB
Python
import asyncio
|
|
import importlib
|
|
import math
|
|
import os
|
|
import re
|
|
import time
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from cli.context import CliContext
|
|
from cli.core import DEFAULT_CORE_SERVICES, CliContextFactory, CliExecutionService
|
|
from cli.exit_codes import ARGUMENT_ERROR
|
|
from cli.runtime.base import RUNTIME_OPERATOR
|
|
|
|
MAX_SIGNING_KEY_BYTES = 64 * 1024
|
|
|
|
|
|
class PluginArtifactCommandController:
|
|
"""
|
|
制品发布命令控制器。
|
|
|
|
普通插件命令及帮助查询不加载制品部署依赖。
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
context_factory: CliContextFactory | None = None,
|
|
execution_service: CliExecutionService | None = None,
|
|
config_factory: Callable[[], Any] | None = None,
|
|
catalog_factory: Callable[[Any], Any] | None = None,
|
|
service_factory: Callable[[Any], Any] | None = None,
|
|
enablement_factory: Callable[[Any], Any] | None = None,
|
|
maintenance_factory: Callable[[Any], Any] | None = None,
|
|
) -> None:
|
|
"""
|
|
初始化制品命令控制器,并保留部署依赖的延迟加载。
|
|
|
|
:param context_factory: CLI 上下文工厂
|
|
:param execution_service: CLI 执行服务
|
|
:param config_factory: 制品部署配置工厂
|
|
:param catalog_factory: 制品目录服务工厂
|
|
:param service_factory: 维护发布服务工厂
|
|
:param enablement_factory: 制品启停服务工厂
|
|
:param maintenance_factory: 制品清理与签名轮换服务工厂
|
|
:return: None
|
|
"""
|
|
self.context_factory = context_factory or DEFAULT_CORE_SERVICES.context_factory
|
|
self.execution_service = execution_service or DEFAULT_CORE_SERVICES.execution_service
|
|
self.config_factory = config_factory or self._default_config
|
|
self.catalog_factory = catalog_factory or self._default_catalog
|
|
self.service_factory = service_factory or self._default_service
|
|
self.enablement_factory = enablement_factory or self._default_enablement
|
|
self.maintenance_factory = maintenance_factory or self._default_maintenance
|
|
|
|
@staticmethod
|
|
def _default_maintenance(config: Any) -> Any:
|
|
"""
|
|
延迟创建制品维护服务,帮助查询不加载数据库。
|
|
|
|
:param config: 制品发布配置
|
|
:return: 制品维护服务
|
|
"""
|
|
return importlib.import_module('plugins.core.deployment.maintenance').PluginArtifactMaintenanceService(config)
|
|
|
|
def maintain_artifacts(
|
|
self,
|
|
operation: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
options: dict[str, Any],
|
|
allow_prod: bool = False,
|
|
yes: bool = False,
|
|
dry_run: bool = False,
|
|
) -> None:
|
|
"""
|
|
执行白名单中的维护操作,并复用 CLI 生产环境保护。
|
|
|
|
:param operation: inspect、prune、rotate-signature 或 reconcile
|
|
:param env: 当前命令环境
|
|
:param output: 输出格式
|
|
:param options: 当前命令显式支持的维护参数
|
|
:param allow_prod: 是否允许生产环境变更
|
|
:param yes: 是否跳过交互确认
|
|
:param dry_run: 是否只生成清理计划
|
|
:return: None
|
|
"""
|
|
methods = {
|
|
'inspect': 'inspect',
|
|
'prune': 'prune',
|
|
'rotate-signature': 'rotate_signature',
|
|
'reconcile': 'reconcile',
|
|
}
|
|
if operation not in methods:
|
|
raise ValueError('未知制品维护命令')
|
|
ctx = self._context(
|
|
env,
|
|
output,
|
|
command_name=None if operation == 'inspect' else f'plugin artifact {operation}',
|
|
allow_prod=allow_prod,
|
|
yes=yes,
|
|
dry_run=dry_run,
|
|
)
|
|
arguments = dict(options)
|
|
if operation != 'inspect':
|
|
arguments['actor'] = ctx.operator
|
|
if operation == 'prune':
|
|
arguments['dry_run'] = dry_run
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
getattr(self.maintenance_factory(self.config_factory()), methods[operation])(**arguments)
|
|
),
|
|
)
|
|
|
|
@staticmethod
|
|
def _default_config() -> Any:
|
|
"""
|
|
延迟读取当前环境的制品部署配置。
|
|
|
|
:return: 制品部署配置
|
|
"""
|
|
module = importlib.import_module('plugins.core.deployment.config')
|
|
return module.PluginDeploymentConfig.from_settings(Path(__file__).resolve().parents[3])
|
|
|
|
@staticmethod
|
|
def _default_catalog(config: Any) -> Any:
|
|
"""
|
|
延迟创建制品目录服务。
|
|
|
|
:param config: 制品部署配置
|
|
:return: 制品目录服务
|
|
"""
|
|
return importlib.import_module('plugins.core.deployment.catalog').PluginArtifactCatalog(config)
|
|
|
|
@staticmethod
|
|
def _default_service(config: Any) -> Any:
|
|
"""
|
|
延迟创建维护发布服务。
|
|
|
|
:param config: 制品部署配置
|
|
:return: 维护发布服务
|
|
"""
|
|
return importlib.import_module('plugins.core.deployment.service').PluginDeploymentService(config)
|
|
|
|
@staticmethod
|
|
def _default_enablement(config: Any) -> Any:
|
|
"""
|
|
延迟创建维护启停服务。
|
|
|
|
:param config: 制品部署配置
|
|
:return: 维护启停服务
|
|
"""
|
|
return importlib.import_module('plugins.core.deployment.enablement').PluginArtifactEnablementService(config)
|
|
|
|
def _context(
|
|
self,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
command_name: str | None = None,
|
|
allow_prod: bool = False,
|
|
yes: bool = False,
|
|
dry_run: bool = False,
|
|
) -> CliContext:
|
|
# config.env 可能在应用日志策略时首次导入,因此必须先设置命令选择的环境。
|
|
"""
|
|
先设置命令环境,再应用危险操作保护并解析操作者。
|
|
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param command_name: 用于匹配危险操作规则的命令名称
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param dry_run: 是否仅预演而不写入发布状态
|
|
:return: 完成环境与操作者设置的 CLI 上下文
|
|
"""
|
|
os.environ['APP_ENV'] = env
|
|
if command_name and not dry_run:
|
|
ctx = self.context_factory.build_dangerous(env, output, allow_prod, yes, dry_run, command_name=command_name)
|
|
else:
|
|
ctx = self.context_factory.build_regular(env, output, allow_prod, yes, dry_run)
|
|
ctx.operator = RUNTIME_OPERATOR.resolve_operator()
|
|
return ctx
|
|
|
|
def _execute(self, ctx: CliContext, action: Callable[[], dict[str, Any]]) -> None:
|
|
"""
|
|
执行命令操作并统一输出成功结果或异常信息。
|
|
|
|
:param ctx: CLI 上下文
|
|
:param action: 当前命令需要执行的操作
|
|
:return: None
|
|
"""
|
|
try:
|
|
payload = action()
|
|
except Exception as exc:
|
|
payload = {'ok': False, 'message': str(exc), 'errorType': type(exc).__name__}
|
|
self.execution_service.complete_payload(ctx, {**payload, 'env': ctx.env})
|
|
|
|
@staticmethod
|
|
def _identity(artifact: Any) -> dict[str, Any]:
|
|
"""
|
|
提取制品身份和文件数量,供命令结果复用。
|
|
|
|
:param artifact: 已构建或验证的制品
|
|
:return: 制品身份字段
|
|
"""
|
|
return {
|
|
'pluginId': artifact.plugin_id,
|
|
'version': artifact.version,
|
|
'digest': artifact.digest,
|
|
'keyId': artifact.key_id,
|
|
'fileCount': len(artifact.files),
|
|
}
|
|
|
|
@staticmethod
|
|
def _signing_key(source: Path, key_file: Path, password_env: str | None) -> Any:
|
|
"""
|
|
读取位于插件目录之外的签名私钥,密码仅从指定环境变量读取。
|
|
|
|
:param source: 已构建插件目录
|
|
:param key_file: 插件目录之外的 Ed25519 PKCS8 私钥文件
|
|
:param password_env: 保存加密私钥密码的环境变量名称
|
|
:return: Ed25519 签名私钥
|
|
"""
|
|
if key_file.resolve().is_relative_to(source.resolve()):
|
|
raise ValueError('签名私钥必须放在插件源码/预构建目录之外,防止被打入制品')
|
|
if not key_file.is_file() or key_file.stat().st_size > MAX_SIGNING_KEY_BYTES:
|
|
raise ValueError('签名私钥文件不存在或过大')
|
|
password = None
|
|
if password_env is not None:
|
|
value = os.environ.get(password_env)
|
|
if not value:
|
|
raise ValueError(f'私钥密码环境变量未设置:{password_env}')
|
|
password = value.encode('utf-8')
|
|
data = key_file.read_bytes()
|
|
if not data.startswith((b'-----BEGIN PRIVATE KEY-----', b'-----BEGIN ENCRYPTED PRIVATE KEY-----')):
|
|
raise ValueError('签名私钥必须是PKCS8 PEM格式')
|
|
serialization = importlib.import_module('cryptography.hazmat.primitives.serialization')
|
|
ed25519 = importlib.import_module('cryptography.hazmat.primitives.asymmetric.ed25519')
|
|
try:
|
|
key = serialization.load_pem_private_key(data, password=password)
|
|
except (TypeError, ValueError):
|
|
raise ValueError('无法读取PKCS8私钥,请检查密钥格式和密码环境变量') from None
|
|
if not isinstance(key, ed25519.Ed25519PrivateKey):
|
|
raise ValueError('制品签名私钥必须为Ed25519')
|
|
return key
|
|
|
|
def build_artifact(
|
|
self,
|
|
source: Path,
|
|
destination: Path,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
key_file: Path,
|
|
key_id: str,
|
|
password_env: str | None = None,
|
|
) -> None:
|
|
"""
|
|
离线签名打包预构建目录,不加载部署配置或插件代码。
|
|
|
|
:param source: 已构建插件目录
|
|
:param destination: 签名制品输出文件
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param key_file: 插件目录之外的 Ed25519 PKCS8 私钥文件
|
|
:param key_id: 发布者公钥标识
|
|
:param password_env: 保存加密私钥密码的环境变量名称
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output)
|
|
|
|
def build() -> dict[str, Any]:
|
|
"""
|
|
读取签名私钥并返回离线构建结果。
|
|
|
|
:return: 制品路径和身份信息
|
|
"""
|
|
key = self._signing_key(source, key_file, password_env)
|
|
artifact = importlib.import_module('plugins.core.artifacts').build_artifact(
|
|
source, destination, key, key_id
|
|
)
|
|
return {'ok': True, 'path': str(artifact.artifact_path), **self._identity(artifact)}
|
|
|
|
self._execute(ctx, build)
|
|
|
|
def verify_artifact(self, path: Path, env: str, output: str) -> None:
|
|
"""
|
|
校验制品签名、完整摘要与发布者授权范围。
|
|
|
|
:param path: 待处理的签名制品文件
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output)
|
|
|
|
def verify() -> dict[str, Any]:
|
|
"""
|
|
按当前宿主信任配置验证制品及发布者范围。
|
|
|
|
:return: 制品验证结果
|
|
"""
|
|
config = self.config_factory()
|
|
verified = importlib.import_module('plugins.core.artifacts').verify_artifact(path, config.trusted_keys())
|
|
config.authorize_publisher(verified.key_id, verified.plugin_id)
|
|
return {'ok': True, 'message': '签名、完整摘要和发布者授权检查通过', **self._identity(verified)}
|
|
|
|
self._execute(ctx, verify)
|
|
|
|
def import_artifact(self, path: Path, env: str, output: str, *, allow_prod: bool, yes: bool, dry_run: bool) -> None:
|
|
"""
|
|
导入已验证制品及其索引,预演时只执行检查。
|
|
|
|
:param path: 待处理的签名制品文件
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param dry_run: 是否仅预演而不写入发布状态
|
|
:return: None
|
|
"""
|
|
ctx = self._context(
|
|
env, output, command_name='plugin artifact import', allow_prod=allow_prod, yes=yes, dry_run=dry_run
|
|
)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.catalog_factory(self.config_factory()).import_artifact(path, actor=ctx.operator, dry_run=dry_run)
|
|
),
|
|
)
|
|
|
|
def list_artifacts(self, env: str, output: str, *, plugin_id: str | None = None) -> None:
|
|
"""
|
|
查询已导入制品,可按插件 ID 筛选。
|
|
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param plugin_id: 插件 ID
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.catalog_factory(self.config_factory()).list_artifacts(plugin_id)
|
|
),
|
|
)
|
|
|
|
def plan_release(self, plugin_id: str, digest: str, env: str, output: str) -> None:
|
|
"""
|
|
静态检查目标制品并输出维护计划。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param digest: 已导入制品的 SHA256 摘要
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.service_factory(self.config_factory()).plan(plugin_id, digest)
|
|
),
|
|
)
|
|
|
|
def prepare_release(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
maintenance: bool,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
dry_run: bool,
|
|
) -> None:
|
|
"""
|
|
在维护窗口执行安装或升级,并记录制品准备结果。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param digest: 已导入制品的 SHA256 摘要
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param maintenance: 是否确认已停止全部宿主 worker 并进入维护窗口
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:param dry_run: 是否仅预演而不写入发布状态
|
|
:return: None
|
|
"""
|
|
ctx = self._context(
|
|
env, output, command_name='plugin release prepare', allow_prod=allow_prod, yes=yes, dry_run=dry_run
|
|
)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.service_factory(self.config_factory()).prepare(
|
|
plugin_id, digest, maintenance=maintenance, actor=ctx.operator, dry_run=dry_run
|
|
)
|
|
),
|
|
)
|
|
|
|
def select_release(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
expected_generation: str,
|
|
expected_workers: int,
|
|
maintenance: bool,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
按预期发布代际选择目标制品,供重启后的 worker 加载。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param digest: 已导入制品的 SHA256 摘要
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param expected_generation: 最近一次查询得到的发布代际,用于防止并发覆盖
|
|
:param expected_workers: 发布完成时预期就绪的宿主 worker 数量
|
|
:param maintenance: 是否确认已停止全部宿主 worker 并进入维护窗口
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output, command_name='plugin release select', allow_prod=allow_prod, yes=yes)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.service_factory(self.config_factory()).select(
|
|
plugin_id,
|
|
digest,
|
|
expected_generation=expected_generation,
|
|
expected_workers=expected_workers,
|
|
maintenance=maintenance,
|
|
actor=ctx.operator,
|
|
)
|
|
),
|
|
)
|
|
|
|
def release_status(self, env: str, output: str, *, plugin_id: str | None = None) -> None:
|
|
"""
|
|
查询目标制品、数据准备状态和 worker 加载结果。
|
|
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param plugin_id: 插件 ID
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(self.service_factory(self.config_factory()).status(plugin_id)),
|
|
)
|
|
|
|
@staticmethod
|
|
def _release_wait_argument_error(
|
|
plugin_id: str,
|
|
digest: str,
|
|
expected_generation: str,
|
|
expected_workers: int,
|
|
timeout_seconds: float,
|
|
poll_interval_seconds: float,
|
|
) -> str | None:
|
|
"""
|
|
在读取部署配置前校验固定目标和有限时间参数。
|
|
|
|
:param plugin_id: 固定等待的插件 ID
|
|
:param digest: 固定等待的制品 SHA256 摘要
|
|
:param expected_generation: 选择、启用或回滚目标后返回的新发布代际
|
|
:param expected_workers: 本次发布要求的宿主 worker 数量
|
|
:param timeout_seconds: 非负的有限轮询超时秒数
|
|
:param poll_interval_seconds: 正的有限查询间隔秒数
|
|
:return: 固定参数错误描述,参数有效时为 None
|
|
"""
|
|
for value, pattern, message in (
|
|
(plugin_id, r'[a-z][a-z0-9_]{1,63}', '插件 ID 必须为 2 至 64 位小写字母、数字或下划线,并以字母开头'),
|
|
(digest, r'[0-9a-f]{64}', 'digest 必须为 64 位小写十六进制摘要'),
|
|
(expected_generation, r'[0-9a-f]{32}', 'generation 必须为 32 位小写十六进制代际'),
|
|
):
|
|
if not isinstance(value, str) or re.fullmatch(pattern, value) is None:
|
|
return message
|
|
if type(expected_workers) is not int or expected_workers < 1:
|
|
return 'expected-workers 必须为正整数'
|
|
for value, label, allow_zero in (
|
|
(timeout_seconds, 'timeout', True),
|
|
(poll_interval_seconds, 'interval', False),
|
|
):
|
|
try:
|
|
finite = type(value) in (int, float) and math.isfinite(value)
|
|
except OverflowError:
|
|
finite = False
|
|
if not finite or value < 0 or (not allow_zero and value == 0):
|
|
return f'{label} 必须为有限数值,且' + ('不小于 0' if allow_zero else '大于 0')
|
|
return None
|
|
|
|
def wait_release(
|
|
self,
|
|
plugin_id: str,
|
|
digest: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
expected_generation: str,
|
|
expected_workers: int,
|
|
timeout_seconds: float = 300,
|
|
poll_interval_seconds: float = 2,
|
|
) -> None:
|
|
"""
|
|
只读等待固定发布目标,将取消或初始化异常转换为脱敏的最终结果。
|
|
|
|
:param plugin_id: 固定等待的插件 ID
|
|
:param digest: 固定等待的制品 SHA256 摘要
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param expected_generation: 选择、启用或回滚目标后返回的新发布代际
|
|
:param expected_workers: 本次发布要求的宿主 worker 数量
|
|
:param timeout_seconds: 轮询超时秒数,为零时只查询一次
|
|
:param poll_interval_seconds: 两次查询的间隔秒数
|
|
:return: None
|
|
"""
|
|
error = self._release_wait_argument_error(
|
|
plugin_id, digest, expected_generation, expected_workers, timeout_seconds, poll_interval_seconds
|
|
)
|
|
ctx = CliContext(env=env, output=output)
|
|
payload: dict[str, Any] = {
|
|
'ok': False,
|
|
'operation': 'release_wait',
|
|
'reason': 'invalid_arguments' if error else 'unavailable',
|
|
'message': error or '发布状态暂不可查询',
|
|
'target': {
|
|
'pluginId': plugin_id,
|
|
'digest': digest,
|
|
'generation': expected_generation,
|
|
'expectedWorkers': expected_workers,
|
|
},
|
|
'lastObserved': None,
|
|
'attempts': 0 if error else None,
|
|
'elapsedSeconds': 0.0,
|
|
'timeoutSeconds': None if error else timeout_seconds,
|
|
'intervalSeconds': None if error else poll_interval_seconds,
|
|
}
|
|
if error:
|
|
self.execution_service.complete_payload(ctx, {**payload, 'exit_code': ARGUMENT_ERROR})
|
|
return
|
|
started = time.monotonic()
|
|
try:
|
|
ctx = self._context(env, output)
|
|
payload = self.execution_service.run_async(
|
|
self.service_factory(self.config_factory()).wait(
|
|
plugin_id,
|
|
digest,
|
|
expected_generation=expected_generation,
|
|
expected_workers=expected_workers,
|
|
timeout_seconds=timeout_seconds,
|
|
poll_interval_seconds=poll_interval_seconds,
|
|
)
|
|
)
|
|
except (asyncio.CancelledError, KeyboardInterrupt):
|
|
payload.update(ok=False, reason='cancelled', message='已取消等待发布就绪', attempts=None, lastObserved=None)
|
|
payload['elapsedSeconds'] = round(max(0.0, time.monotonic() - started), 3)
|
|
except Exception:
|
|
payload.update(
|
|
ok=False, reason='unavailable', message='发布状态暂不可查询', attempts=None, lastObserved=None
|
|
)
|
|
payload['elapsedSeconds'] = round(max(0.0, time.monotonic() - started), 3)
|
|
self.execution_service.complete_payload(ctx, payload)
|
|
|
|
def rollback_release(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
expected_generation: str,
|
|
schema_compatible: bool,
|
|
maintenance: bool,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
确认数据兼容后选择上一代码制品,不回滚数据库迁移。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param expected_generation: 最近一次查询得到的发布代际,用于防止并发覆盖
|
|
:param schema_compatible: 是否确认旧代码兼容当前数据结构
|
|
:param maintenance: 是否确认已停止全部宿主 worker 并进入维护窗口
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
ctx = self._context(env, output, command_name='plugin release rollback', allow_prod=allow_prod, yes=yes)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.service_factory(self.config_factory()).rollback(
|
|
plugin_id,
|
|
expected_generation=expected_generation,
|
|
schema_compatible=schema_compatible,
|
|
maintenance=maintenance,
|
|
actor=ctx.operator,
|
|
)
|
|
),
|
|
)
|
|
|
|
def set_release_enabled(
|
|
self,
|
|
plugin_id: str,
|
|
env: str,
|
|
output: str,
|
|
*,
|
|
enabled: bool,
|
|
expected_generation: str,
|
|
maintenance: bool,
|
|
allow_prod: bool,
|
|
yes: bool,
|
|
) -> None:
|
|
"""
|
|
在维护窗口更新目标制品的启停状态与发布代际。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param env: 当前命令运行环境
|
|
:param output: 输出格式
|
|
:param enabled: 是否启用目标制品
|
|
:param expected_generation: 最近一次查询得到的发布代际,用于防止并发覆盖
|
|
:param maintenance: 是否确认已停止全部宿主 worker 并进入维护窗口
|
|
:param allow_prod: 是否允许生产环境危险命令
|
|
:param yes: 是否跳过确认
|
|
:return: None
|
|
"""
|
|
operation = 'enable' if enabled else 'disable'
|
|
ctx = self._context(env, output, command_name=f'plugin release {operation}', allow_prod=allow_prod, yes=yes)
|
|
self._execute(
|
|
ctx,
|
|
lambda: self.execution_service.run_async(
|
|
self.enablement_factory(self.config_factory()).set_enabled(
|
|
plugin_id,
|
|
enabled=enabled,
|
|
expected_generation=expected_generation,
|
|
maintenance=maintenance,
|
|
actor=ctx.operator,
|
|
)
|
|
),
|
|
)
|