mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
feat: clarify deployment update strategies
This commit is contained in:
14
.env.example
14
.env.example
@@ -57,10 +57,18 @@ ADMIN_USERNAME=admin123456
|
|||||||
# docker compose 下 app 启动前自动执行 pending migration/backfill(默认 true)
|
# docker compose 下 app 启动前自动执行 pending migration/backfill(默认 true)
|
||||||
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
# AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||||
|
|
||||||
# 管理后台一键更新(正式发布版默认可用)
|
# 管理后台更新策略:
|
||||||
# 正式发布版会下载 GitHub Release 包,校验 SHA256 后切换 /opt/aether/current 并重启。
|
# - systemd/二进制部署使用 self:下载 GitHub Release 包,校验 SHA256 后切换 current 并重启。
|
||||||
# 源码/本地构建不支持后台在线更新。
|
# - Docker Compose 使用 docker:后台只提示版本,实际更新请在 compose 目录执行 ./update.sh。
|
||||||
|
# - 源码/本地构建使用 manual:手动拉取源码或下载 release。
|
||||||
|
# Compose 默认把持久化文件放在 ./datas/{postgres,mysql,sqlite,redis},日志放在 ./logs。
|
||||||
|
# 分布式/多节点部署不要使用 ./datas 作为共享数据目录;应使用外部共享 Postgres/MySQL 和 Redis。
|
||||||
|
# 多节点不要从管理后台一键更新单个节点,应使用镜像滚动更新、systemd 分批发布或外部编排。
|
||||||
# AETHER_BASE_DIR=/opt/aether
|
# AETHER_BASE_DIR=/opt/aether
|
||||||
|
# AETHER_UPDATE_STRATEGY=docker
|
||||||
|
# AETHER_DOCKER_UPDATE_COMMAND=./update.sh
|
||||||
|
# AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=single-node
|
||||||
|
# AETHER_GATEWAY_NODE_ROLE=all
|
||||||
# Docker Compose 默认把应用日志输出到 stdout/stderr。
|
# Docker Compose 默认把应用日志输出到 stdout/stderr。
|
||||||
# 如需文件日志,可改成 file 或 both,并把可写目录挂载到 /opt/aether/logs。
|
# 如需文件日志,可改成 file 或 both,并把可写目录挂载到 /opt/aether/logs。
|
||||||
# AETHER_LOG_DESTINATION=stdout
|
# AETHER_LOG_DESTINATION=stdout
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ WORKDIR /opt/aether
|
|||||||
|
|
||||||
ENV RUST_LOG=aether_gateway=info \
|
ENV RUST_LOG=aether_gateway=info \
|
||||||
APP_PORT=8084 \
|
APP_PORT=8084 \
|
||||||
|
AETHER_UPDATE_STRATEGY=docker \
|
||||||
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
||||||
|
|
||||||
EXPOSE 8084
|
EXPOSE 8084
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ ENV LANG=C.UTF-8 \
|
|||||||
LC_ALL=C.UTF-8 \
|
LC_ALL=C.UTF-8 \
|
||||||
RUST_LOG=aether_gateway=info \
|
RUST_LOG=aether_gateway=info \
|
||||||
APP_PORT=8084 \
|
APP_PORT=8084 \
|
||||||
|
AETHER_UPDATE_STRATEGY=manual \
|
||||||
AETHER_GATEWAY_STATIC_DIR=/srv/frontend
|
AETHER_GATEWAY_STATIC_DIR=/srv/frontend
|
||||||
|
|
||||||
EXPOSE 8084
|
EXPOSE 8084
|
||||||
|
|||||||
@@ -139,6 +139,7 @@ ENV LANG=C.UTF-8 \
|
|||||||
RUST_LOG=aether_gateway=info \
|
RUST_LOG=aether_gateway=info \
|
||||||
APP_PORT=8084 \
|
APP_PORT=8084 \
|
||||||
AETHER_BASE_DIR=/opt/aether \
|
AETHER_BASE_DIR=/opt/aether \
|
||||||
|
AETHER_UPDATE_STRATEGY=self \
|
||||||
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend
|
||||||
|
|
||||||
EXPOSE 8084
|
EXPOSE 8084
|
||||||
|
|||||||
@@ -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::smtp::build_admin_smtp_test_payload;
|
||||||
use crate::handlers::admin::system::shared::update::{
|
use crate::handlers::admin::system::shared::update::{
|
||||||
build_admin_system_update_capability_payload, prepare_admin_system_update_task,
|
build_admin_system_update_capability_payload, current_self_update_blocker,
|
||||||
read_update_history, read_update_task_status, start_admin_system_rollback_task,
|
prepare_admin_system_update_task, read_update_history, read_update_task_status,
|
||||||
start_admin_system_update_task,
|
self_update_supported, start_admin_system_rollback_task, start_admin_system_update_task,
|
||||||
};
|
};
|
||||||
use crate::important_notification::build_important_notification_test_payload;
|
use crate::important_notification::build_important_notification_test_payload;
|
||||||
use crate::maintenance::{ManualUsageCleanupMode, ManualUsageCleanupOptions};
|
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_method == http::Method::POST
|
||||||
&& request_path == "/api/admin/system/prepare-update"
|
&& 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
|
let target_version = request_body
|
||||||
.filter(|b| !b.is_empty())
|
.filter(|b| !b.is_empty())
|
||||||
.and_then(|body| serde_json::from_slice::<serde_json::Value>(body).ok())
|
.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::request::AdminAppState;
|
||||||
use crate::handlers::admin::shared::build_admin_usage_counter_health_payload;
|
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::{
|
use crate::handlers::admin::system::shared::update_client::{
|
||||||
build_direct_update_http_client, build_update_http_client, has_explicit_update_proxy_env,
|
build_direct_update_http_client, build_update_http_client, has_explicit_update_proxy_env,
|
||||||
update_github_token_from_env,
|
update_github_token_from_env,
|
||||||
@@ -52,7 +55,7 @@ pub(crate) fn build_admin_system_check_update_payload_from_release(
|
|||||||
latest_release,
|
latest_release,
|
||||||
error,
|
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
|
payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +65,7 @@ pub(crate) fn build_admin_system_releases_list_payload(
|
|||||||
) -> serde_json::Value {
|
) -> serde_json::Value {
|
||||||
let mut payload =
|
let mut payload =
|
||||||
build_admin_system_releases_payload(current_aether_version(), releases, error);
|
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
|
payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,8 +73,20 @@ fn current_build_is_release() -> bool {
|
|||||||
option_env!("AETHER_BUILD_TYPE").unwrap_or("source") == "release"
|
option_env!("AETHER_BUILD_TYPE").unwrap_or("source") == "release"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_source_build_check_update_override(payload: &mut Value, release_build: bool) {
|
fn apply_self_update_check_update_override(payload: &mut Value, supported: bool) {
|
||||||
if release_build {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
if payload.get("has_update").and_then(Value::as_bool) != Some(true) {
|
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["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) {
|
fn apply_self_update_releases_override(payload: &mut Value, supported: bool) {
|
||||||
if release_build {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let Some(releases) = payload.get_mut("releases").and_then(Value::as_array_mut) else {
|
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);
|
release["updatable"] = json!(false);
|
||||||
if release.get("update_blocker").is_none() || release["update_blocker"].is_null() {
|
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))]
|
#[cfg(not(test))]
|
||||||
struct CachedReleases {
|
struct CachedReleases {
|
||||||
all: Vec<AdminSystemUpdateRelease>,
|
all: Vec<AdminSystemUpdateRelease>,
|
||||||
@@ -719,21 +758,25 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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!({
|
let mut payload = json!({
|
||||||
"has_update": true,
|
"has_update": true,
|
||||||
"updatable": true,
|
"updatable": true,
|
||||||
"update_blocker": serde_json::Value::Null
|
"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["updatable"], false);
|
||||||
assert_eq!(payload["update_blocker"], SOURCE_BUILD_UPDATE_BLOCKER);
|
assert_eq!(payload["update_blocker"], SOURCE_BUILD_UPDATE_BLOCKER);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[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!({
|
let mut payload = json!({
|
||||||
"releases": [
|
"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!(payload["releases"][0]["updatable"], false);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|||||||
@@ -113,6 +113,74 @@ const MAX_SHA256SUMS_DOWNLOAD_BYTES: u64 = 1024 * 1024;
|
|||||||
const MAX_EXTRACTED_RELEASE_BYTES: u64 = 1024 * 1024 * 1024;
|
const MAX_EXTRACTED_RELEASE_BYTES: u64 = 1024 * 1024 * 1024;
|
||||||
const DEFAULT_UPDATE_DOWNLOAD_TIMEOUT_SECS: u64 = 600;
|
const DEFAULT_UPDATE_DOWNLOAD_TIMEOUT_SECS: u64 = 600;
|
||||||
const DEFAULT_UPDATE_DOWNLOAD_IDLE_TIMEOUT_SECS: u64 = 30;
|
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)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub(crate) struct UpdateHistoryEntry {
|
pub(crate) struct UpdateHistoryEntry {
|
||||||
@@ -249,12 +317,84 @@ fn is_release_build() -> bool {
|
|||||||
true
|
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 {
|
pub(crate) fn build_admin_system_update_capability_payload() -> serde_json::Value {
|
||||||
let supported = is_release_build();
|
|
||||||
let build_type = current_build_type();
|
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 task_status = read_update_task_status();
|
||||||
let base_dir = aether_base_dir();
|
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!({
|
json!({
|
||||||
"supported": supported,
|
"supported": supported,
|
||||||
"enabled": 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_status": task_status.phase,
|
||||||
"task_error": task_status.error,
|
"task_error": task_status.error,
|
||||||
"build_type": build_type,
|
"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 {
|
"message": if supported {
|
||||||
"一键更新可用"
|
"一键更新可用"
|
||||||
} else {
|
} else {
|
||||||
"源码构建不支持在线更新"
|
current_self_update_blocker()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -291,8 +439,8 @@ pub(crate) async fn prepare_admin_system_update_task(
|
|||||||
tarball_url: String,
|
tarball_url: String,
|
||||||
sha256sums_url: Option<String>,
|
sha256sums_url: Option<String>,
|
||||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
if !is_release_build() {
|
if !self_update_supported() {
|
||||||
return Ok(Err(source_build_rejection_response()));
|
return Ok(Err(self_update_rejection_response()));
|
||||||
}
|
}
|
||||||
let Some(sha256sums_url) = sha256sums_url.filter(|url| !url.trim().is_empty()) else {
|
let Some(sha256sums_url) = sha256sums_url.filter(|url| !url.trim().is_empty()) else {
|
||||||
return Ok(Err((
|
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(
|
pub(crate) async fn start_admin_system_update_task(
|
||||||
version: Option<String>,
|
version: Option<String>,
|
||||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
if !is_release_build() {
|
if !self_update_supported() {
|
||||||
return Ok(Err(source_build_rejection_response()));
|
return Ok(Err(self_update_rejection_response()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let version = match version.or_else(get_prepared_version) {
|
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(
|
pub(crate) async fn start_admin_system_rollback_task(
|
||||||
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
) -> Result<Result<serde_json::Value, (http::StatusCode, serde_json::Value)>, GatewayError> {
|
||||||
if !is_release_build() {
|
if !self_update_supported() {
|
||||||
return Ok(Err(source_build_rejection_response()));
|
return Ok(Err(self_update_rejection_response()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(previous) = find_rollback_target() else {
|
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,
|
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");
|
.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]
|
#[test]
|
||||||
fn update_finds_nested_release_payload_dir() {
|
fn update_finds_nested_release_payload_dir() {
|
||||||
let staging = temp_test_dir("nested");
|
let staging = temp_test_dir("nested");
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ services:
|
|||||||
AETHER_UPDATE_PROXY_URL: ${AETHER_UPDATE_PROXY_URL:-}
|
AETHER_UPDATE_PROXY_URL: ${AETHER_UPDATE_PROXY_URL:-}
|
||||||
UPDATE_PROXY_URL: ${UPDATE_PROXY_URL:-}
|
UPDATE_PROXY_URL: ${UPDATE_PROXY_URL:-}
|
||||||
AETHER_BASE_DIR: /opt/aether
|
AETHER_BASE_DIR: /opt/aether
|
||||||
|
AETHER_UPDATE_STRATEGY: self
|
||||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE: ${AETHER_GATEWAY_AUTO_PREPARE_DATABASE:-true}
|
AETHER_GATEWAY_AUTO_PREPARE_DATABASE: ${AETHER_GATEWAY_AUTO_PREPARE_DATABASE:-true}
|
||||||
ports:
|
ports:
|
||||||
- "${AETHER_RELEASE_LOCAL_PORT:-18085}:${AETHER_RELEASE_LOCAL_PORT:-18085}"
|
- "${AETHER_RELEASE_LOCAL_PORT:-18085}:${AETHER_RELEASE_LOCAL_PORT:-18085}"
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
AETHER_BASE_DIR: /opt/aether
|
AETHER_BASE_DIR: /opt/aether
|
||||||
|
AETHER_UPDATE_STRATEGY: docker
|
||||||
|
AETHER_DOCKER_UPDATE_COMMAND: ${AETHER_DOCKER_UPDATE_COMMAND:-./update.sh}
|
||||||
AETHER_DATABASE_DRIVER: sqlite
|
AETHER_DATABASE_DRIVER: sqlite
|
||||||
AETHER_DATABASE_URL: sqlite:///opt/aether/data/aether.db
|
AETHER_DATABASE_URL: sqlite:///opt/aether/data/aether.db
|
||||||
AETHER_RUNTIME_BACKEND: memory
|
AETHER_RUNTIME_BACKEND: memory
|
||||||
@@ -23,5 +25,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/opt/aether/data
|
- ./datas/sqlite:/opt/aether/data
|
||||||
|
- ./logs:/opt/aether/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ services:
|
|||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- ./datas/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${DB_PORT:-5432}:5432"
|
- "127.0.0.1:${DB_PORT:-5432}:5432"
|
||||||
command: >
|
command: >
|
||||||
@@ -39,7 +39,7 @@ services:
|
|||||||
container_name: aether-redis
|
container_name: aether-redis
|
||||||
command: redis-server --appendonly yes --appendfsync everysec --save 60 1000 --requirepass ${REDIS_PASSWORD} --maxclients ${REDIS_MAXCLIENTS:-10000}
|
command: redis-server --appendonly yes --appendfsync everysec --save 60 1000 --requirepass ${REDIS_PASSWORD} --maxclients ${REDIS_MAXCLIENTS:-10000}
|
||||||
volumes:
|
volumes:
|
||||||
- redis_data:/data
|
- ./datas/redis:/data
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
- "127.0.0.1:${REDIS_PORT:-6379}:6379"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-aether_root}
|
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-aether_root}
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
volumes:
|
volumes:
|
||||||
- mysql_data:/var/lib/mysql
|
- ./datas/mysql:/var/lib/mysql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test:
|
test:
|
||||||
[
|
[
|
||||||
@@ -83,6 +83,8 @@ services:
|
|||||||
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
|
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379/0
|
||||||
TZ: Asia/Shanghai
|
TZ: Asia/Shanghai
|
||||||
AETHER_BASE_DIR: /opt/aether
|
AETHER_BASE_DIR: /opt/aether
|
||||||
|
AETHER_UPDATE_STRATEGY: docker
|
||||||
|
AETHER_DOCKER_UPDATE_COMMAND: ${AETHER_DOCKER_UPDATE_COMMAND:-./update.sh}
|
||||||
AETHER_LOG_DESTINATION: ${AETHER_LOG_DESTINATION:-stdout}
|
AETHER_LOG_DESTINATION: ${AETHER_LOG_DESTINATION:-stdout}
|
||||||
AETHER_LOG_FORMAT: ${AETHER_LOG_FORMAT:-pretty}
|
AETHER_LOG_FORMAT: ${AETHER_LOG_FORMAT:-pretty}
|
||||||
AETHER_LOG_DIR: ${AETHER_LOG_DIR:-/opt/aether/logs}
|
AETHER_LOG_DIR: ${AETHER_LOG_DIR:-/opt/aether/logs}
|
||||||
@@ -98,9 +100,6 @@ services:
|
|||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
- "${APP_PORT:-8084}:${APP_PORT:-8084}"
|
||||||
|
volumes:
|
||||||
|
- ./logs:/opt/aether/logs
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
mysql_data:
|
|
||||||
redis_data:
|
|
||||||
|
|||||||
@@ -382,11 +382,19 @@ export interface CheckUpdateResponse {
|
|||||||
export interface SystemUpdateCapabilityResponse {
|
export interface SystemUpdateCapabilityResponse {
|
||||||
supported: boolean
|
supported: boolean
|
||||||
build_type: string
|
build_type: string
|
||||||
|
update_strategy?: 'self' | 'docker' | 'manual' | string
|
||||||
|
strategy?: 'self' | 'docker' | 'manual' | string
|
||||||
|
deployment_topology?: 'single-node' | 'multi-node' | string
|
||||||
|
topology?: 'single-node' | 'multi-node' | string
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
rollback_available: boolean
|
rollback_available: boolean
|
||||||
task_status: string
|
task_status: string
|
||||||
task_error: string | null
|
task_error: string | null
|
||||||
install_root?: string
|
install_root?: string
|
||||||
|
base_dir?: string
|
||||||
|
data_dir?: string
|
||||||
|
logs_dir?: string
|
||||||
|
docker_update_command?: string | null
|
||||||
message: string
|
message: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,17 @@
|
|||||||
>
|
>
|
||||||
{{ updateBlockerText }}
|
{{ updateBlockerText }}
|
||||||
</p>
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="isDockerUpdate && dockerUpdateCommand"
|
||||||
|
class="mt-3 w-full max-w-sm rounded-lg border border-border/60 bg-muted/30 px-3 py-2 text-left"
|
||||||
|
>
|
||||||
|
<p class="text-xs text-muted-foreground">
|
||||||
|
在 docker-compose.yml 所在目录执行
|
||||||
|
</p>
|
||||||
|
<code class="mt-1 block break-all rounded bg-background/70 px-2 py-1.5 font-mono text-xs text-foreground">
|
||||||
|
{{ dockerUpdateCommand }}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -199,8 +210,10 @@ const props = defineProps<{
|
|||||||
updatePhase?: 'download' | 'restart' | 'reconnecting'
|
updatePhase?: 'download' | 'restart' | 'reconnecting'
|
||||||
updating?: boolean
|
updating?: boolean
|
||||||
updateSupported?: boolean
|
updateSupported?: boolean
|
||||||
|
updateStrategy?: string
|
||||||
updatable?: boolean
|
updatable?: boolean
|
||||||
updateBlocker?: string | null
|
updateBlocker?: string | null
|
||||||
|
dockerUpdateCommand?: string | null
|
||||||
reconnectMessage?: string
|
reconnectMessage?: string
|
||||||
rollbackAvailable?: boolean
|
rollbackAvailable?: boolean
|
||||||
rollingBack?: boolean
|
rollingBack?: boolean
|
||||||
@@ -220,6 +233,9 @@ const updatePhase = computed(() => props.updatePhase ?? 'download')
|
|||||||
const updateSupported = computed(() => props.updateSupported ?? true)
|
const updateSupported = computed(() => props.updateSupported ?? true)
|
||||||
const updatable = computed(() => props.updatable ?? true)
|
const updatable = computed(() => props.updatable ?? true)
|
||||||
const canApplyUpdate = computed(() => updateSupported.value && updatable.value)
|
const canApplyUpdate = computed(() => updateSupported.value && updatable.value)
|
||||||
|
const updateStrategy = computed(() => props.updateStrategy ?? 'manual')
|
||||||
|
const isDockerUpdate = computed(() => updateStrategy.value === 'docker' && !canApplyUpdate.value)
|
||||||
|
const dockerUpdateCommand = computed(() => props.dockerUpdateCommand || '')
|
||||||
const updateBlockerText = computed(() => {
|
const updateBlockerText = computed(() => {
|
||||||
if (!updateSupported.value) return props.updateBlocker || SOURCE_BUILD_UPDATE_HINT
|
if (!updateSupported.value) return props.updateBlocker || SOURCE_BUILD_UPDATE_HINT
|
||||||
return props.updateBlocker || '当前版本暂不支持在线更新'
|
return props.updateBlocker || '当前版本暂不支持在线更新'
|
||||||
|
|||||||
@@ -413,6 +413,8 @@
|
|||||||
:update-supported="updateSupported"
|
:update-supported="updateSupported"
|
||||||
:updatable="updateInfo.updatable"
|
:updatable="updateInfo.updatable"
|
||||||
:update-blocker="updateInfo.update_blocker"
|
:update-blocker="updateInfo.update_blocker"
|
||||||
|
:update-strategy="updateStrategy"
|
||||||
|
:docker-update-command="dockerUpdateCommand"
|
||||||
:reconnect-message="reconnectMessage"
|
:reconnect-message="reconnectMessage"
|
||||||
:rollback-available="rollbackAvailable"
|
:rollback-available="rollbackAvailable"
|
||||||
:rolling-back="rollingBack"
|
:rolling-back="rollingBack"
|
||||||
@@ -434,7 +436,7 @@ import { useDarkMode } from '@/composables/useDarkMode'
|
|||||||
import { useSiteInfo } from '@/composables/useSiteInfo'
|
import { useSiteInfo } from '@/composables/useSiteInfo'
|
||||||
import { useToast } from '@/composables/useToast'
|
import { useToast } from '@/composables/useToast'
|
||||||
import { isDemoMode } from '@/config/demo'
|
import { isDemoMode } from '@/config/demo'
|
||||||
import { adminApi, type CheckUpdateResponse, type ReleaseEntry, type UpdateTaskStatusResponse } from '@/api/admin'
|
import { adminApi, type CheckUpdateResponse, type ReleaseEntry, type SystemUpdateCapabilityResponse, type UpdateTaskStatusResponse } from '@/api/admin'
|
||||||
import { announcementApi, type Announcement } from '@/api/announcements'
|
import { announcementApi, type Announcement } from '@/api/announcements'
|
||||||
import { parseApiError } from '@/utils/errorParser'
|
import { parseApiError } from '@/utils/errorParser'
|
||||||
import Button from '@/components/ui/button.vue'
|
import Button from '@/components/ui/button.vue'
|
||||||
@@ -518,6 +520,9 @@ const versionStatus = ref<CheckUpdateResponse | null>(null)
|
|||||||
const loadingVersionStatus = ref(false)
|
const loadingVersionStatus = ref(false)
|
||||||
const applyingSystemUpdate = ref(false)
|
const applyingSystemUpdate = ref(false)
|
||||||
const updateSupported = ref(true)
|
const updateSupported = ref(true)
|
||||||
|
const updateStrategy = ref('manual')
|
||||||
|
const updateCapabilityMessage = ref<string | null>(null)
|
||||||
|
const dockerUpdateCommand = ref<string | null>(null)
|
||||||
const reconnectMessage = ref('等待服务恢复...')
|
const reconnectMessage = ref('等待服务恢复...')
|
||||||
const rollbackAvailable = ref(false)
|
const rollbackAvailable = ref(false)
|
||||||
const rollingBack = ref(false)
|
const rollingBack = ref(false)
|
||||||
@@ -529,6 +534,7 @@ const preparedUpdateVersion = ref<string | null>(
|
|||||||
)
|
)
|
||||||
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
const SOURCE_BUILD_UPDATE_HINT = '当前为源码构建,请使用 git pull 后重新编译。'
|
||||||
const SOURCE_BUILD_RELEASE_HINT = '当前为源码构建,请手动切换到对应标签后重新编译。'
|
const SOURCE_BUILD_RELEASE_HINT = '当前为源码构建,请手动切换到对应标签后重新编译。'
|
||||||
|
const MANUAL_UPDATE_HINT = '当前部署策略不支持在线自更新,请手动下载 Release 或使用安装脚本更新。'
|
||||||
let versionStatusLoadPromise: Promise<CheckUpdateResponse | null> | null = null
|
let versionStatusLoadPromise: Promise<CheckUpdateResponse | null> | null = null
|
||||||
let updateStatusPollTimer: number | null = null
|
let updateStatusPollTimer: number | null = null
|
||||||
const updateProgressPercent = computed(() => updateTaskStatus.value?.progress_percent ?? null)
|
const updateProgressPercent = computed(() => updateTaskStatus.value?.progress_percent ?? null)
|
||||||
@@ -700,14 +706,13 @@ async function loadVersionStatus(force = false) {
|
|||||||
adminApi.getSystemUpdateCapability().catch(() => null),
|
adminApi.getSystemUpdateCapability().catch(() => null),
|
||||||
])
|
])
|
||||||
if (capability) {
|
if (capability) {
|
||||||
rollbackAvailable.value = capability.rollback_available
|
applyUpdateCapability(capability)
|
||||||
updateSupported.value = capability.supported
|
|
||||||
}
|
}
|
||||||
versionStatus.value = capability?.supported === false && status.has_update
|
versionStatus.value = updateSupported.value === false && status.has_update
|
||||||
? {
|
? {
|
||||||
...status,
|
...status,
|
||||||
updatable: false,
|
updatable: false,
|
||||||
update_blocker: SOURCE_BUILD_UPDATE_HINT,
|
update_blocker: updateUnsupportedMessage(SOURCE_BUILD_UPDATE_HINT),
|
||||||
}
|
}
|
||||||
: status
|
: status
|
||||||
syncSystemUpdatePhase(versionStatus.value)
|
syncSystemUpdatePhase(versionStatus.value)
|
||||||
@@ -724,6 +729,18 @@ async function loadVersionStatus(force = false) {
|
|||||||
return versionStatusLoadPromise
|
return versionStatusLoadPromise
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyUpdateCapability(capability: SystemUpdateCapabilityResponse) {
|
||||||
|
updateSupported.value = capability.supported
|
||||||
|
rollbackAvailable.value = capability.supported && capability.rollback_available
|
||||||
|
updateStrategy.value = capability.update_strategy || capability.strategy || 'manual'
|
||||||
|
updateCapabilityMessage.value = capability.message || null
|
||||||
|
dockerUpdateCommand.value = capability.docker_update_command || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUnsupportedMessage(fallback = MANUAL_UPDATE_HINT): string {
|
||||||
|
return updateCapabilityMessage.value || fallback
|
||||||
|
}
|
||||||
|
|
||||||
function syncSystemUpdatePhase(status: CheckUpdateResponse | null) {
|
function syncSystemUpdatePhase(status: CheckUpdateResponse | null) {
|
||||||
if (systemUpdatePhase.value === 'reconnecting') return
|
if (systemUpdatePhase.value === 'reconnecting') return
|
||||||
if (systemUpdatePhase.value === 'restart') {
|
if (systemUpdatePhase.value === 'restart') {
|
||||||
@@ -754,16 +771,16 @@ function buildUpdateInfoFromRelease(release: ReleaseEntry): CheckUpdateResponse
|
|||||||
updateInfo.value?.current_version ||
|
updateInfo.value?.current_version ||
|
||||||
__APP_VERSION__ ||
|
__APP_VERSION__ ||
|
||||||
''
|
''
|
||||||
const sourceBuild = !updateSupported.value
|
const canSelfUpdate = updateSupported.value
|
||||||
return {
|
return {
|
||||||
current_version: currentVersion,
|
current_version: currentVersion,
|
||||||
latest_version: release.version,
|
latest_version: release.version,
|
||||||
has_update: !release.is_current,
|
has_update: !release.is_current,
|
||||||
updatable: !sourceBuild && !release.is_current && release.updatable,
|
updatable: canSelfUpdate && !release.is_current && release.updatable,
|
||||||
update_blocker: release.is_current
|
update_blocker: release.is_current
|
||||||
? '当前已是这个版本'
|
? '当前已是这个版本'
|
||||||
: sourceBuild
|
: !canSelfUpdate
|
||||||
? SOURCE_BUILD_RELEASE_HINT
|
? updateUnsupportedMessage(SOURCE_BUILD_RELEASE_HINT)
|
||||||
: release.update_blocker,
|
: release.update_blocker,
|
||||||
release_url: release.release_url,
|
release_url: release.release_url,
|
||||||
release_notes: release.release_notes,
|
release_notes: release.release_notes,
|
||||||
@@ -787,16 +804,14 @@ async function handleApplySystemUpdate() {
|
|||||||
applyingSystemUpdate.value = true
|
applyingSystemUpdate.value = true
|
||||||
try {
|
try {
|
||||||
const capability = await adminApi.getSystemUpdateCapability()
|
const capability = await adminApi.getSystemUpdateCapability()
|
||||||
rollbackAvailable.value = capability.rollback_available
|
applyUpdateCapability(capability)
|
||||||
if (!capability.supported) {
|
if (!capability.supported) {
|
||||||
updateSupported.value = false
|
|
||||||
showError(
|
showError(
|
||||||
SOURCE_BUILD_UPDATE_HINT,
|
updateUnsupportedMessage('不支持在线自更新'),
|
||||||
'不支持在线更新'
|
'不支持在线更新'
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
updateSupported.value = true
|
|
||||||
|
|
||||||
if (systemUpdatePhase.value === 'download') {
|
if (systemUpdatePhase.value === 'download') {
|
||||||
const targetStatus = updateInfo.value || versionStatus.value
|
const targetStatus = updateInfo.value || versionStatus.value
|
||||||
|
|||||||
26
install.sh
26
install.sh
@@ -1697,6 +1697,8 @@ AETHER_LOG_RETENTION_DAYS=7
|
|||||||
AETHER_LOG_MAX_FILES=30
|
AETHER_LOG_MAX_FILES=30
|
||||||
|
|
||||||
APP_PORT=${APP_PORT:-8084}
|
APP_PORT=${APP_PORT:-8084}
|
||||||
|
AETHER_BASE_DIR=${INSTALL_ROOT}
|
||||||
|
AETHER_UPDATE_STRATEGY=self
|
||||||
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
||||||
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
||||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||||
@@ -1736,6 +1738,8 @@ AETHER_LOG_RETENTION_DAYS=7
|
|||||||
AETHER_LOG_MAX_FILES=30
|
AETHER_LOG_MAX_FILES=30
|
||||||
|
|
||||||
APP_PORT=${APP_PORT:-8084}
|
APP_PORT=${APP_PORT:-8084}
|
||||||
|
AETHER_BASE_DIR=${INSTALL_ROOT}
|
||||||
|
AETHER_UPDATE_STRATEGY=manual
|
||||||
AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=multi-node
|
AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=multi-node
|
||||||
AETHER_GATEWAY_NODE_ROLE=${role}
|
AETHER_GATEWAY_NODE_ROLE=${role}
|
||||||
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
AETHER_GATEWAY_STATIC_DIR=${INSTALL_ROOT}/current/frontend
|
||||||
@@ -1824,6 +1828,8 @@ generate_compose_env() {
|
|||||||
replace_or_append_env "${output}" "ADMIN_EMAIL" "${ADMIN_EMAIL:-admin@example.local}"
|
replace_or_append_env "${output}" "ADMIN_EMAIL" "${ADMIN_EMAIL:-admin@example.local}"
|
||||||
replace_or_append_env "${output}" "ADMIN_USERNAME" "${ADMIN_USERNAME:-admin}"
|
replace_or_append_env "${output}" "ADMIN_USERNAME" "${ADMIN_USERNAME:-admin}"
|
||||||
replace_or_append_env "${output}" "ADMIN_PASSWORD" "${ADMIN_PASSWORD}"
|
replace_or_append_env "${output}" "ADMIN_PASSWORD" "${ADMIN_PASSWORD}"
|
||||||
|
replace_or_append_env "${output}" "AETHER_UPDATE_STRATEGY" "docker"
|
||||||
|
replace_or_append_env "${output}" "AETHER_DOCKER_UPDATE_COMMAND" "./update.sh"
|
||||||
append_compose_log_env_defaults "${output}"
|
append_compose_log_env_defaults "${output}"
|
||||||
replace_or_append_env "${output}" "AETHER_GATEWAY_AUTO_PREPARE_DATABASE" "true"
|
replace_or_append_env "${output}" "AETHER_GATEWAY_AUTO_PREPARE_DATABASE" "true"
|
||||||
}
|
}
|
||||||
@@ -1843,6 +1849,8 @@ $(compose_log_env_block)
|
|||||||
|
|
||||||
APP_IMAGE=$(compose_image)
|
APP_IMAGE=$(compose_image)
|
||||||
APP_PORT=$(compose_app_port)
|
APP_PORT=$(compose_app_port)
|
||||||
|
AETHER_UPDATE_STRATEGY=docker
|
||||||
|
AETHER_DOCKER_UPDATE_COMMAND=./update.sh
|
||||||
AETHER_GATEWAY_STATIC_DIR=${COMPOSE_RELEASE_FRONTEND_DIR}
|
AETHER_GATEWAY_STATIC_DIR=${COMPOSE_RELEASE_FRONTEND_DIR}
|
||||||
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE=rust-authoritative
|
||||||
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
AETHER_GATEWAY_AUTO_PREPARE_DATABASE=true
|
||||||
@@ -2223,6 +2231,12 @@ install_compose_mode() {
|
|||||||
resolve_compose_dir
|
resolve_compose_dir
|
||||||
info "preparing Docker Compose deployment in ${COMPOSE_DIR}"
|
info "preparing Docker Compose deployment in ${COMPOSE_DIR}"
|
||||||
ensure_directory "${COMPOSE_DIR}"
|
ensure_directory "${COMPOSE_DIR}"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/logs"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas/postgres"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas/redis"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas/mysql"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas/sqlite"
|
||||||
install_project_file "docker-compose.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
install_project_file "docker-compose.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
||||||
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
||||||
install_project_file "update.sh" "${COMPOSE_DIR}/update.sh" "0755"
|
install_project_file "update.sh" "${COMPOSE_DIR}/update.sh" "0755"
|
||||||
@@ -2244,6 +2258,11 @@ Docker Compose files are ready:
|
|||||||
${COMPOSE_DIR}/.env.example
|
${COMPOSE_DIR}/.env.example
|
||||||
${COMPOSE_DIR}/update.sh
|
${COMPOSE_DIR}/update.sh
|
||||||
${COMPOSE_DIR}/generate_keys.sh
|
${COMPOSE_DIR}/generate_keys.sh
|
||||||
|
${COMPOSE_DIR}/logs
|
||||||
|
${COMPOSE_DIR}/datas/postgres
|
||||||
|
${COMPOSE_DIR}/datas/redis
|
||||||
|
${COMPOSE_DIR}/datas/mysql
|
||||||
|
${COMPOSE_DIR}/datas/sqlite
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [[ "${SKIP_START}" == "true" ]]; then
|
if [[ "${SKIP_START}" == "true" ]]; then
|
||||||
@@ -2260,7 +2279,9 @@ install_compose_single_node_mode() {
|
|||||||
resolve_compose_dir
|
resolve_compose_dir
|
||||||
info "preparing Docker Compose single-node deployment in ${COMPOSE_DIR}"
|
info "preparing Docker Compose single-node deployment in ${COMPOSE_DIR}"
|
||||||
ensure_directory "${COMPOSE_DIR}"
|
ensure_directory "${COMPOSE_DIR}"
|
||||||
ensure_directory "${COMPOSE_DIR}/data"
|
ensure_directory "${COMPOSE_DIR}/datas"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/datas/sqlite"
|
||||||
|
ensure_directory "${COMPOSE_DIR}/logs"
|
||||||
|
|
||||||
install_project_file "docker-compose.single-node.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
install_project_file "docker-compose.single-node.yml" "${COMPOSE_DIR}/docker-compose.yml" "0644"
|
||||||
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
install_project_file ".env.example" "${COMPOSE_DIR}/.env.example" "0644"
|
||||||
@@ -2283,7 +2304,8 @@ Docker Compose single-node files are ready:
|
|||||||
${COMPOSE_DIR}/.env.example
|
${COMPOSE_DIR}/.env.example
|
||||||
${COMPOSE_DIR}/update.sh
|
${COMPOSE_DIR}/update.sh
|
||||||
${COMPOSE_DIR}/generate_keys.sh
|
${COMPOSE_DIR}/generate_keys.sh
|
||||||
${COMPOSE_DIR}/data
|
${COMPOSE_DIR}/datas/sqlite
|
||||||
|
${COMPOSE_DIR}/logs
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
if [[ "${SKIP_START}" == "true" ]]; then
|
if [[ "${SKIP_START}" == "true" ]]; then
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ validate_env_line_for_copy() {
|
|||||||
|
|
||||||
should_skip_single_node_env_key() {
|
should_skip_single_node_env_key() {
|
||||||
case "$1" in
|
case "$1" in
|
||||||
APP_IMAGE|LOCAL_APP_IMAGE|APP_PORT|DB_HOST|DB_PORT|DB_USER|DB_NAME|DB_PASSWORD|POSTGRES_*|MYSQL_*|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD|REDIS_URL|AETHER_GATEWAY_DATA_REDIS_URL|AETHER_GATEWAY_DATA_REDIS_KEY_PREFIX|DATABASE_URL|AETHER_DATABASE_URL|AETHER_DATABASE_DRIVER|AETHER_GATEWAY_DATA_POSTGRES_URL|AETHER_RUNTIME_BACKEND|AETHER_RUNTIME_REDIS_URL|AETHER_RUNTIME_REDIS_KEY_PREFIX|AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY|AETHER_GATEWAY_NODE_ROLE|AETHER_GATEWAY_STATIC_DIR|AETHER_LOG_DIR|AETHER_GATEWAY_AUTO_PREPARE_DATABASE)
|
APP_IMAGE|LOCAL_APP_IMAGE|APP_PORT|DB_HOST|DB_PORT|DB_USER|DB_NAME|DB_PASSWORD|POSTGRES_*|MYSQL_*|REDIS_HOST|REDIS_PORT|REDIS_PASSWORD|REDIS_URL|AETHER_GATEWAY_DATA_REDIS_URL|AETHER_GATEWAY_DATA_REDIS_KEY_PREFIX|DATABASE_URL|AETHER_DATABASE_URL|AETHER_DATABASE_DRIVER|AETHER_GATEWAY_DATA_POSTGRES_URL|AETHER_RUNTIME_BACKEND|AETHER_RUNTIME_REDIS_URL|AETHER_RUNTIME_REDIS_KEY_PREFIX|AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY|AETHER_GATEWAY_NODE_ROLE|AETHER_GATEWAY_STATIC_DIR|AETHER_UPDATE_STRATEGY|AETHER_DOCKER_UPDATE_COMMAND|AETHER_LOG_DIR|AETHER_GATEWAY_AUTO_PREPARE_DATABASE)
|
||||||
return 0
|
return 0
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -316,12 +316,14 @@ write_single_node_env() {
|
|||||||
printf 'APP_IMAGE=%s\n' "$app_image"
|
printf 'APP_IMAGE=%s\n' "$app_image"
|
||||||
printf 'APP_PORT=%s\n' "$app_port"
|
printf 'APP_PORT=%s\n' "$app_port"
|
||||||
printf 'AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend\n'
|
printf 'AETHER_GATEWAY_STATIC_DIR=/opt/aether/current/frontend\n'
|
||||||
printf 'AETHER_LOG_DESTINATION=both\n'
|
printf 'AETHER_UPDATE_STRATEGY=docker\n'
|
||||||
|
printf 'AETHER_DOCKER_UPDATE_COMMAND=./update.sh\n'
|
||||||
|
printf 'AETHER_LOG_DESTINATION=stdout\n'
|
||||||
printf 'AETHER_LOG_FORMAT=pretty\n'
|
printf 'AETHER_LOG_FORMAT=pretty\n'
|
||||||
printf 'AETHER_LOG_DIR=/app/logs\n'
|
printf 'AETHER_LOG_DIR=/opt/aether/logs\n'
|
||||||
printf 'AETHER_DATABASE_DRIVER=sqlite\n'
|
printf 'AETHER_DATABASE_DRIVER=sqlite\n'
|
||||||
printf 'AETHER_DATABASE_URL=sqlite://./data/aether.db\n'
|
printf 'AETHER_DATABASE_URL=sqlite:///opt/aether/data/aether.db\n'
|
||||||
printf 'DATABASE_URL=sqlite://./data/aether.db\n'
|
printf 'DATABASE_URL=sqlite:///opt/aether/data/aether.db\n'
|
||||||
printf 'AETHER_RUNTIME_BACKEND=memory\n'
|
printf 'AETHER_RUNTIME_BACKEND=memory\n'
|
||||||
printf 'AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=single-node\n'
|
printf 'AETHER_GATEWAY_DEPLOYMENT_TOPOLOGY=single-node\n'
|
||||||
printf 'AETHER_GATEWAY_NODE_ROLE=all\n'
|
printf 'AETHER_GATEWAY_NODE_ROLE=all\n'
|
||||||
@@ -728,13 +730,15 @@ preflight() {
|
|||||||
|
|
||||||
NOW="$(date +%Y%m%d%H%M%S)"
|
NOW="$(date +%Y%m%d%H%M%S)"
|
||||||
if [[ -z "$WORK_DIR" ]]; then
|
if [[ -z "$WORK_DIR" ]]; then
|
||||||
WORK_DIR="${SOURCE_COMPOSE_DIR}/data/pg-compose-to-single-node-${NOW}"
|
WORK_DIR="${SOURCE_COMPOSE_DIR}/datas/sqlite/pg-compose-to-single-node-${NOW}"
|
||||||
fi
|
fi
|
||||||
WORK_DIR="$(absolute_path_maybe_missing "$WORK_DIR")"
|
WORK_DIR="$(absolute_path_maybe_missing "$WORK_DIR")"
|
||||||
mkdir -p "$WORK_DIR"
|
mkdir -p "$WORK_DIR"
|
||||||
|
|
||||||
prepare_target_compose
|
prepare_target_compose
|
||||||
TARGET_DB="${TARGET_DB:-${TARGET_COMPOSE_DIR}/data/aether.db}"
|
mkdir -p "${TARGET_COMPOSE_DIR}/logs"
|
||||||
|
mkdir -p "${TARGET_COMPOSE_DIR}/datas/sqlite"
|
||||||
|
TARGET_DB="${TARGET_DB:-${TARGET_COMPOSE_DIR}/datas/sqlite/aether.db}"
|
||||||
TARGET_DB="$(absolute_path_maybe_missing "$TARGET_DB")"
|
TARGET_DB="$(absolute_path_maybe_missing "$TARGET_DB")"
|
||||||
|
|
||||||
DB_USER="$(env_file_get "$SOURCE_ENV" "DB_USER")"
|
DB_USER="$(env_file_get "$SOURCE_ENV" "DB_USER")"
|
||||||
|
|||||||
Reference in New Issue
Block a user