mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-02 17:30:23 +08:00
feat: clarify deployment update strategies
This commit is contained in:
@@ -19,9 +19,9 @@ use crate::handlers::admin::system::shared::settings::{
|
||||
};
|
||||
use crate::handlers::admin::system::shared::smtp::build_admin_smtp_test_payload;
|
||||
use crate::handlers::admin::system::shared::update::{
|
||||
build_admin_system_update_capability_payload, prepare_admin_system_update_task,
|
||||
read_update_history, read_update_task_status, start_admin_system_rollback_task,
|
||||
start_admin_system_update_task,
|
||||
build_admin_system_update_capability_payload, current_self_update_blocker,
|
||||
prepare_admin_system_update_task, read_update_history, read_update_task_status,
|
||||
self_update_supported, start_admin_system_rollback_task, start_admin_system_update_task,
|
||||
};
|
||||
use crate::important_notification::build_important_notification_test_payload;
|
||||
use crate::maintenance::{ManualUsageCleanupMode, ManualUsageCleanupOptions};
|
||||
@@ -99,6 +99,16 @@ pub(super) async fn maybe_build_local_admin_core_system_response(
|
||||
&& request_method == http::Method::POST
|
||||
&& request_path == "/api/admin/system/prepare-update"
|
||||
{
|
||||
if !self_update_supported() {
|
||||
return Ok(Some(
|
||||
(
|
||||
http::StatusCode::PRECONDITION_REQUIRED,
|
||||
Json(json!({ "detail": current_self_update_blocker() })),
|
||||
)
|
||||
.into_response(),
|
||||
));
|
||||
}
|
||||
|
||||
let target_version = request_body
|
||||
.filter(|b| !b.is_empty())
|
||||
.and_then(|body| serde_json::from_slice::<serde_json::Value>(body).ok())
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::handlers::admin::request::AdminAppState;
|
||||
use crate::handlers::admin::shared::build_admin_usage_counter_health_payload;
|
||||
use crate::handlers::admin::system::shared::update::{
|
||||
current_self_update_blocker, self_update_supported,
|
||||
};
|
||||
use crate::handlers::admin::system::shared::update_client::{
|
||||
build_direct_update_http_client, build_update_http_client, has_explicit_update_proxy_env,
|
||||
update_github_token_from_env,
|
||||
@@ -52,7 +55,7 @@ pub(crate) fn build_admin_system_check_update_payload_from_release(
|
||||
latest_release,
|
||||
error,
|
||||
);
|
||||
apply_source_build_check_update_override(&mut payload, current_build_is_release());
|
||||
apply_self_update_check_update_override(&mut payload, self_update_supported());
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -62,7 +65,7 @@ pub(crate) fn build_admin_system_releases_list_payload(
|
||||
) -> serde_json::Value {
|
||||
let mut payload =
|
||||
build_admin_system_releases_payload(current_aether_version(), releases, error);
|
||||
apply_source_build_releases_override(&mut payload, current_build_is_release());
|
||||
apply_self_update_releases_override(&mut payload, self_update_supported());
|
||||
payload
|
||||
}
|
||||
|
||||
@@ -70,8 +73,20 @@ fn current_build_is_release() -> bool {
|
||||
option_env!("AETHER_BUILD_TYPE").unwrap_or("source") == "release"
|
||||
}
|
||||
|
||||
fn apply_source_build_check_update_override(payload: &mut Value, release_build: bool) {
|
||||
if release_build {
|
||||
fn apply_self_update_check_update_override(payload: &mut Value, supported: bool) {
|
||||
apply_self_update_check_update_override_with_blocker(
|
||||
payload,
|
||||
supported,
|
||||
current_self_update_blocker(),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_self_update_check_update_override_with_blocker(
|
||||
payload: &mut Value,
|
||||
supported: bool,
|
||||
blocker: &str,
|
||||
) {
|
||||
if supported {
|
||||
return;
|
||||
}
|
||||
if payload.get("has_update").and_then(Value::as_bool) != Some(true) {
|
||||
@@ -79,11 +94,23 @@ fn apply_source_build_check_update_override(payload: &mut Value, release_build:
|
||||
}
|
||||
|
||||
payload["updatable"] = json!(false);
|
||||
payload["update_blocker"] = json!(SOURCE_BUILD_UPDATE_BLOCKER);
|
||||
payload["update_blocker"] = json!(blocker);
|
||||
}
|
||||
|
||||
fn apply_source_build_releases_override(payload: &mut Value, release_build: bool) {
|
||||
if release_build {
|
||||
fn apply_self_update_releases_override(payload: &mut Value, supported: bool) {
|
||||
apply_self_update_releases_override_with_blocker(
|
||||
payload,
|
||||
supported,
|
||||
current_self_update_release_blocker(),
|
||||
);
|
||||
}
|
||||
|
||||
fn apply_self_update_releases_override_with_blocker(
|
||||
payload: &mut Value,
|
||||
supported: bool,
|
||||
blocker: &str,
|
||||
) {
|
||||
if supported {
|
||||
return;
|
||||
}
|
||||
let Some(releases) = payload.get_mut("releases").and_then(Value::as_array_mut) else {
|
||||
@@ -96,11 +123,23 @@ fn apply_source_build_releases_override(payload: &mut Value, release_build: bool
|
||||
}
|
||||
release["updatable"] = json!(false);
|
||||
if release.get("update_blocker").is_none() || release["update_blocker"].is_null() {
|
||||
release["update_blocker"] = json!(SOURCE_BUILD_RELEASE_BLOCKER);
|
||||
release["update_blocker"] = json!(blocker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn current_self_update_release_blocker() -> &'static str {
|
||||
if !current_build_is_release() {
|
||||
return SOURCE_BUILD_RELEASE_BLOCKER;
|
||||
}
|
||||
|
||||
if self_update_supported() {
|
||||
""
|
||||
} else {
|
||||
current_self_update_blocker()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(test))]
|
||||
struct CachedReleases {
|
||||
all: Vec<AdminSystemUpdateRelease>,
|
||||
@@ -719,21 +758,25 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_build_check_update_override_marks_latest_release_non_updatable() {
|
||||
fn self_update_check_update_override_marks_latest_release_non_updatable() {
|
||||
let mut payload = json!({
|
||||
"has_update": true,
|
||||
"updatable": true,
|
||||
"update_blocker": serde_json::Value::Null
|
||||
});
|
||||
|
||||
apply_source_build_check_update_override(&mut payload, false);
|
||||
apply_self_update_check_update_override_with_blocker(
|
||||
&mut payload,
|
||||
false,
|
||||
SOURCE_BUILD_UPDATE_BLOCKER,
|
||||
);
|
||||
|
||||
assert_eq!(payload["updatable"], false);
|
||||
assert_eq!(payload["update_blocker"], SOURCE_BUILD_UPDATE_BLOCKER);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_build_releases_override_marks_non_current_entries_non_updatable() {
|
||||
fn self_update_releases_override_marks_non_current_entries_non_updatable() {
|
||||
let mut payload = json!({
|
||||
"releases": [
|
||||
{
|
||||
@@ -751,7 +794,11 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
apply_source_build_releases_override(&mut payload, false);
|
||||
apply_self_update_releases_override_with_blocker(
|
||||
&mut payload,
|
||||
false,
|
||||
SOURCE_BUILD_RELEASE_BLOCKER,
|
||||
);
|
||||
|
||||
assert_eq!(payload["releases"][0]["updatable"], false);
|
||||
assert_eq!(
|
||||
|
||||
@@ -113,6 +113,74 @@ const MAX_SHA256SUMS_DOWNLOAD_BYTES: u64 = 1024 * 1024;
|
||||
const MAX_EXTRACTED_RELEASE_BYTES: u64 = 1024 * 1024 * 1024;
|
||||
const DEFAULT_UPDATE_DOWNLOAD_TIMEOUT_SECS: u64 = 600;
|
||||
const DEFAULT_UPDATE_DOWNLOAD_IDLE_TIMEOUT_SECS: u64 = 30;
|
||||
const SOURCE_BUILD_UPDATE_BLOCKER: &str = "当前为源码构建,请使用 git pull 后重新编译。";
|
||||
const DOCKER_UPDATE_BLOCKER: &str =
|
||||
"Docker 部署请使用镜像更新:进入 docker-compose.yml 所在目录执行 ./update.sh。";
|
||||
const MANUAL_UPDATE_BLOCKER: &str =
|
||||
"当前部署策略不支持在线自更新,请手动下载 Release 或使用安装脚本更新。";
|
||||
const MULTI_NODE_UPDATE_BLOCKER: &str =
|
||||
"多节点部署不支持在管理后台更新单个节点,请使用镜像滚动更新或外部发布编排。";
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum UpdateStrategy {
|
||||
SelfManaged,
|
||||
Docker,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl UpdateStrategy {
|
||||
fn from_env_value(value: Option<&str>, release_build: bool) -> Self {
|
||||
let Some(value) = value.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return if release_build {
|
||||
Self::SelfManaged
|
||||
} else {
|
||||
Self::Manual
|
||||
};
|
||||
};
|
||||
|
||||
match value.to_ascii_lowercase().as_str() {
|
||||
"self" | "self-managed" | "binary" | "systemd" | "launchd" => Self::SelfManaged,
|
||||
"docker" | "compose" | "docker-compose" | "container" => Self::Docker,
|
||||
"manual" | "source" | "none" | "off" | "disabled" => Self::Manual,
|
||||
_ => Self::Manual,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::SelfManaged => "self",
|
||||
Self::Docker => "docker",
|
||||
Self::Manual => "manual",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum DeploymentTopology {
|
||||
SingleNode,
|
||||
MultiNode,
|
||||
}
|
||||
|
||||
impl DeploymentTopology {
|
||||
fn from_env_value(value: Option<&str>) -> Self {
|
||||
match value
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("multi-node" | "multi" | "cluster") => Self::MultiNode,
|
||||
_ => Self::SingleNode,
|
||||
}
|
||||
}
|
||||
|
||||
fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::SingleNode => "single-node",
|
||||
Self::MultiNode => "multi-node",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub(crate) struct UpdateHistoryEntry {
|
||||
@@ -249,12 +317,84 @@ fn is_release_build() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn current_update_strategy() -> UpdateStrategy {
|
||||
UpdateStrategy::from_env_value(
|
||||
std::env::var("AETHER_UPDATE_STRATEGY").ok().as_deref(),
|
||||
is_release_build(),
|
||||
)
|
||||
}
|
||||
|
||||
fn current_deployment_topology() -> DeploymentTopology {
|
||||
DeploymentTopology::from_env_value(
|
||||
std::env::var("AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY")
|
||||
.ok()
|
||||
.as_deref(),
|
||||
)
|
||||
}
|
||||
|
||||
fn self_update_supported_for(
|
||||
release_build: bool,
|
||||
update_strategy: UpdateStrategy,
|
||||
deployment_topology: DeploymentTopology,
|
||||
) -> bool {
|
||||
release_build
|
||||
&& update_strategy == UpdateStrategy::SelfManaged
|
||||
&& deployment_topology == DeploymentTopology::SingleNode
|
||||
}
|
||||
|
||||
pub(crate) fn self_update_supported() -> bool {
|
||||
self_update_supported_for(
|
||||
is_release_build(),
|
||||
current_update_strategy(),
|
||||
current_deployment_topology(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn current_self_update_blocker() -> &'static str {
|
||||
if !is_release_build() {
|
||||
return SOURCE_BUILD_UPDATE_BLOCKER;
|
||||
}
|
||||
if current_deployment_topology() == DeploymentTopology::MultiNode {
|
||||
return MULTI_NODE_UPDATE_BLOCKER;
|
||||
}
|
||||
|
||||
match current_update_strategy() {
|
||||
UpdateStrategy::SelfManaged => "一键更新可用",
|
||||
UpdateStrategy::Docker => DOCKER_UPDATE_BLOCKER,
|
||||
UpdateStrategy::Manual => MANUAL_UPDATE_BLOCKER,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_logs_dir() -> PathBuf {
|
||||
std::env::var("AETHER_LOG_DIR")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| aether_base_dir().join("logs"))
|
||||
}
|
||||
|
||||
fn docker_update_command() -> String {
|
||||
std::env::var("AETHER_DOCKER_UPDATE_COMMAND")
|
||||
.ok()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "./update.sh".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn build_admin_system_update_capability_payload() -> serde_json::Value {
|
||||
let supported = is_release_build();
|
||||
let build_type = current_build_type();
|
||||
let rollback_available = find_rollback_target().is_some();
|
||||
let update_strategy = current_update_strategy();
|
||||
let deployment_topology = current_deployment_topology();
|
||||
let supported =
|
||||
self_update_supported_for(is_release_build(), update_strategy, deployment_topology);
|
||||
let rollback_available = supported && find_rollback_target().is_some();
|
||||
let task_status = read_update_task_status();
|
||||
let base_dir = aether_base_dir();
|
||||
let docker_command = if update_strategy == UpdateStrategy::Docker {
|
||||
Some(docker_update_command())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let data_dir = base_dir.join("data");
|
||||
json!({
|
||||
"supported": supported,
|
||||
"enabled": supported,
|
||||
@@ -262,11 +402,19 @@ pub(crate) fn build_admin_system_update_capability_payload() -> serde_json::Valu
|
||||
"task_status": task_status.phase,
|
||||
"task_error": task_status.error,
|
||||
"build_type": build_type,
|
||||
"install_root": base_dir,
|
||||
"update_strategy": update_strategy.as_str(),
|
||||
"strategy": update_strategy.as_str(),
|
||||
"deployment_topology": deployment_topology.as_str(),
|
||||
"topology": deployment_topology.as_str(),
|
||||
"install_root": base_dir.clone(),
|
||||
"base_dir": base_dir,
|
||||
"data_dir": data_dir,
|
||||
"logs_dir": update_logs_dir(),
|
||||
"docker_update_command": docker_command,
|
||||
"message": if supported {
|
||||
"一键更新可用"
|
||||
} else {
|
||||
"源码构建不支持在线更新"
|
||||
current_self_update_blocker()
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -291,8 +439,8 @@ pub(crate) async fn prepare_admin_system_update_task(
|
||||
tarball_url: String,
|
||||
sha256sums_url: Option<String>,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
let Some(sha256sums_url) = sha256sums_url.filter(|url| !url.trim().is_empty()) else {
|
||||
return Ok(Err((
|
||||
@@ -692,8 +840,8 @@ fn remove_path_if_exists(path: &Path) -> std::io::Result<()> {
|
||||
pub(crate) async fn start_admin_system_update_task(
|
||||
version: Option<String>,
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
|
||||
let version = match version.or_else(get_prepared_version) {
|
||||
@@ -800,8 +948,8 @@ fn switch_current_symlink(version: &str) -> Result<(), String> {
|
||||
|
||||
pub(crate) async fn start_admin_system_rollback_task(
|
||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||
if !is_release_build() {
|
||||
return Ok(Err(source_build_rejection_response()));
|
||||
if !self_update_supported() {
|
||||
return Ok(Err(self_update_rejection_response()));
|
||||
}
|
||||
|
||||
let Some(previous) = find_rollback_target() else {
|
||||
@@ -861,10 +1009,10 @@ fn update_already_running_response() -> (http::StatusCode, serde_json::Value) {
|
||||
)
|
||||
}
|
||||
|
||||
fn source_build_rejection_response() -> (http::StatusCode, serde_json::Value) {
|
||||
fn self_update_rejection_response() -> (http::StatusCode, serde_json::Value) {
|
||||
(
|
||||
http::StatusCode::PRECONDITION_REQUIRED,
|
||||
json!({ "detail": "\u{6e90}\u{7801}\u{6784}\u{5efa}\u{4e0d}\u{652f}\u{6301}\u{5728}\u{7ebf}\u{66f4}\u{65b0}\u{ff0c}\u{8bf7}\u{4f7f}\u{7528}\u{6b63}\u{5f0f}\u{53d1}\u{5e03}\u{7248}" }),
|
||||
json!({ "detail": current_self_update_blocker() }),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -893,6 +1041,60 @@ mod tests {
|
||||
.expect("frontend index should be written");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_strategy_defaults_to_self_only_for_release_builds() {
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(None, true),
|
||||
UpdateStrategy::SelfManaged
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(None, false),
|
||||
UpdateStrategy::Manual
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_strategy_parses_docker_as_non_self_update() {
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("docker"), true),
|
||||
UpdateStrategy::Docker
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("compose"), true),
|
||||
UpdateStrategy::Docker
|
||||
);
|
||||
assert_eq!(
|
||||
UpdateStrategy::from_env_value(Some("unknown"), true),
|
||||
UpdateStrategy::Manual
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deployment_topology_defaults_to_single_node() {
|
||||
assert_eq!(
|
||||
DeploymentTopology::from_env_value(None),
|
||||
DeploymentTopology::SingleNode
|
||||
);
|
||||
assert_eq!(
|
||||
DeploymentTopology::from_env_value(Some("multi-node")),
|
||||
DeploymentTopology::MultiNode
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_node_topology_disables_self_update() {
|
||||
assert!(self_update_supported_for(
|
||||
true,
|
||||
UpdateStrategy::SelfManaged,
|
||||
DeploymentTopology::SingleNode,
|
||||
));
|
||||
assert!(!self_update_supported_for(
|
||||
true,
|
||||
UpdateStrategy::SelfManaged,
|
||||
DeploymentTopology::MultiNode,
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_finds_nested_release_payload_dir() {
|
||||
let staging = temp_test_dir("nested");
|
||||
|
||||
Reference in New Issue
Block a user