use std::fmt::Display;
use std::path::PathBuf;
use crate::{
api_types::{EnhancedServiceManifest, PatchPackageInfo},
architecture::Architecture,
constants::docker::get_compose_file_path,
constants::docker::get_docker_work_dir,
version::Version,
};
use anyhow::Result;
use tracing::{debug, info};
#[derive(Debug, Clone, PartialEq)]
pub enum DownloadType {
Full,
Patch,
}
impl Display for DownloadType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DownloadType::Full => write!(f, "full"),
DownloadType::Patch => write!(f, "patch"),
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum UpgradeStrategy {
FullUpgrade {
url: String,
hash: String,
signature: String,
target_version: Version,
download_type: DownloadType,
},
PatchUpgrade {
patch_info: PatchPackageInfo,
target_version: Version,
download_type: DownloadType,
},
NoUpgrade {
target_version: Version,
},
}
impl UpgradeStrategy {
pub fn get_changed_files(&self) -> Vec<PathBuf> {
let change_files = match self {
UpgradeStrategy::FullUpgrade { .. } => vec!["data".to_string(), "upload".to_string()],
UpgradeStrategy::PatchUpgrade { patch_info, .. } => patch_info.get_changed_files(),
UpgradeStrategy::NoUpgrade { .. } => {
vec![]
}
};
change_files.into_iter().map(PathBuf::from).collect()
}
}
#[derive(Debug, Clone)]
pub struct DecisionFactors {
pub version_compatibility: f64,
pub network_condition: f64,
pub disk_space: f64,
pub risk_assessment: f64,
pub time_efficiency: f64,
}
#[derive(Debug)]
pub struct UpgradeStrategyManager {
manifest: EnhancedServiceManifest,
current_version: String,
force_full: bool,
architecture: Architecture,
}
impl UpgradeStrategyManager {
pub fn new(
current_version: String,
force_full: bool,
manifest: EnhancedServiceManifest,
) -> Self {
Self {
manifest,
current_version,
force_full,
architecture: Architecture::detect(),
}
}
pub fn determine_strategy(&self) -> Result<UpgradeStrategy> {
info!("Starting upgrade strategy decision");
info!(" Current version: {}", self.current_version);
info!(" Server version: {}", self.manifest.version);
info!(" Target architecture: {}", self.architecture.as_str());
info!(" Force full upgrade: {}", self.force_full);
let current_ver = self.current_version.parse::<Version>()?;
let server_ver = self.manifest.version.clone();
let base_comparison = current_ver.compare_detailed(&server_ver);
info!("Current version details: {:?}", current_ver);
info!("Server version details: {:?}", server_ver);
info!("Base version comparison result: {:?}", base_comparison);
if self.force_full {
info!("Force executing full upgrade");
return self.select_full_upgrade_strategy();
}
let work_dir = get_docker_work_dir();
let compose_file_path = get_compose_file_path();
if !work_dir.exists() || !compose_file_path.exists() {
info!("No docker directory or compose file found in working directory, selecting full upgrade strategy");
return self.select_full_upgrade_strategy();
}
match base_comparison {
crate::version::VersionComparison::Equal | crate::version::VersionComparison::Newer => {
info!("Current version is already latest, no upgrade needed");
Ok(UpgradeStrategy::NoUpgrade {
target_version: self.manifest.version.clone(),
})
}
crate::version::VersionComparison::PatchUpgradeable => {
if !self.has_patch_for_architecture() {
info!("No incremental upgrade package for current architecture, selecting full upgrade strategy");
self.select_full_upgrade_strategy()
} else {
info!("Selecting incremental upgrade strategy");
self.select_patch_upgrade_strategy()
}
}
crate::version::VersionComparison::FullUpgradeRequired => {
info!("Selecting full upgrade strategy");
self.select_full_upgrade_strategy()
}
}
}
pub fn select_full_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
debug!("Selecting full upgrade strategy");
if let Some(_) = &self.manifest.platforms {
let platform_info = self.get_platform_package()?;
debug!("Using architecture-specific full package: {}", &platform_info.url);
Ok(UpgradeStrategy::FullUpgrade {
url: platform_info.url.clone(),
hash: "external".to_string(), signature: platform_info.signature.clone(),
target_version: self.manifest.version.clone(),
download_type: DownloadType::Full,
})
} else {
if let Some(package_info) = &self.manifest.packages {
let full_info = &package_info.full;
debug!("Using generic full package: {}", &full_info.url);
Ok(UpgradeStrategy::FullUpgrade {
url: full_info.url.clone(),
hash: full_info.hash.clone(),
signature: full_info.signature.clone(),
target_version: self.manifest.version.clone(),
download_type: DownloadType::Full,
})
} else {
Err(anyhow::anyhow!("Full upgrade package for corresponding architecture not found"))
}
}
}
pub fn select_patch_upgrade_strategy(&self) -> Result<UpgradeStrategy> {
debug!("Selecting incremental upgrade strategy");
let patch_info = self.get_patch_package()?;
debug!("Using architecture-specific patch package: {}", &patch_info.url);
Ok(UpgradeStrategy::PatchUpgrade {
patch_info: patch_info.clone(),
target_version: self.manifest.version.clone(),
download_type: DownloadType::Patch,
})
}
fn get_platform_package<'a>(&self) -> Result<crate::api_types::PlatformPackageInfo> {
if let Some(platforms) = self.manifest.platforms.as_ref() {
match self.architecture {
Architecture::X86_64 => platforms
.x86_64
.clone()
.ok_or_else(|| anyhow::anyhow!("Full upgrade package for x86_64 architecture not found")),
Architecture::Aarch64 => platforms
.aarch64
.clone()
.ok_or_else(|| anyhow::anyhow!("Full upgrade package for aarch64 architecture not found")),
Architecture::Unsupported(_) => Err(anyhow::anyhow!("Full upgrade package for this architecture not found")),
}
} else {
Err(anyhow::anyhow!(
"Full upgrade package for the current architecture not found"
))
}
}
fn get_patch_package(&self) -> Result<&PatchPackageInfo> {
let patch_info = self
.manifest
.patch
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Server does not support incremental upgrade"))?;
match self.architecture {
Architecture::X86_64 => patch_info
.x86_64
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Patch package for x86_64 architecture not available")),
Architecture::Aarch64 => patch_info
.aarch64
.as_ref()
.ok_or_else(|| anyhow::anyhow!("Patch package for aarch64 architecture not available")),
Architecture::Unsupported(_) => Err(anyhow::anyhow!("Unsupported architecture")),
}
}
fn has_patch_for_architecture(&self) -> bool {
self.manifest
.patch
.as_ref()
.map(|patch| match self.architecture {
Architecture::X86_64 => patch.x86_64.is_some(),
Architecture::Aarch64 => patch.aarch64.is_some(),
Architecture::Unsupported(_) => false,
})
.unwrap_or(false)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api_types::*;
use std::fs;
use tempfile::TempDir;
fn create_test_manifest() -> EnhancedServiceManifest {
EnhancedServiceManifest {
version: "0.0.13.2".parse::<Version>().unwrap(),
release_date: "2025-01-12T10:00:00Z".to_string(),
release_notes: "测试版本".to_string(),
packages: Some(ServicePackages {
full: PackageInfo {
url: "https://example.com/docker.zip".to_string(),
hash: "sha256:full_hash".to_string(),
signature: "full_signature".to_string(),
size: 100 * 1024 * 1024, },
patch: None,
}),
platforms: Some(PlatformPackages {
x86_64: Some(PlatformPackageInfo {
signature: "x86_64_signature".to_string(),
url: "https://example.com/x86_64/docker.zip".to_string(),
}),
aarch64: Some(PlatformPackageInfo {
signature: "aarch64_signature".to_string(),
url: "https://example.com/aarch64/docker.zip".to_string(),
}),
}),
patch: Some(PatchInfo {
x86_64: Some(PatchPackageInfo {
url: "https://example.com/patches/x86_64-patch.tar.gz".to_string(),
hash: Some("sha256:patch_hash_x86_64".to_string()),
signature: Some("patch_signature_x86_64".to_string()),
operations: PatchOperations {
replace: Some(ReplaceOperations {
files: vec!["app.jar".to_string(), "config.yml".to_string()],
directories: vec!["front/".to_string()],
}),
delete: Some(ReplaceOperations {
files: vec![
"old-files/app.jar".to_string(),
"old-files/config.yml".to_string(),
],
directories: vec!["old-files/front/".to_string()],
}),
},
notes: None,
}),
aarch64: Some(PatchPackageInfo {
url: "https://example.com/patches/aarch64-patch.tar.gz".to_string(),
hash: Some("sha256:patch_hash_aarch64".to_string()),
signature: Some("patch_signature_aarch64".to_string()),
operations: PatchOperations {
replace: Some(ReplaceOperations {
files: vec!["app.jar".to_string(), "config.yml".to_string()],
directories: vec!["front/".to_string()],
}),
delete: Some(ReplaceOperations {
files: vec![
"old-files/app.jar".to_string(),
"old-files/config.yml".to_string(),
],
directories: vec!["old-files/front/".to_string()],
}),
},
notes: None,
}),
}),
}
}
fn setup_test_environment() -> TempDir {
let temp_dir = TempDir::new().unwrap();
let docker_dir = temp_dir.path().join("docker");
fs::create_dir(&docker_dir).unwrap();
let compose_file = docker_dir.join("docker-compose.yml");
fs::write(
&compose_file,
"version: '3.8'\nservices:\n test:\n image: hello-world",
)
.unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
temp_dir
}
#[test]
fn test_no_upgrade_needed() {
let _temp_dir = setup_test_environment();
let manager =
UpgradeStrategyManager::new("0.0.13.2".to_string(), false, create_test_manifest());
let strategy = manager.determine_strategy().unwrap();
assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
}
#[test]
fn test_current_version_newer() {
let _temp_dir = setup_test_environment();
let manager =
UpgradeStrategyManager::new("0.0.13.4".to_string(), false, create_test_manifest());
let strategy = manager.determine_strategy().unwrap();
assert!(matches!(strategy, UpgradeStrategy::NoUpgrade { .. }));
}
#[test]
fn test_full_upgrade_different_base_version() {
let _temp_dir = setup_test_environment();
let manager =
UpgradeStrategyManager::new("0.0.12".to_string(), false, create_test_manifest());
let strategy = manager.determine_strategy().unwrap();
match strategy {
UpgradeStrategy::FullUpgrade {
url,
target_version,
..
} => {
assert_eq!(url, "https://example.com/aarch64/docker.zip");
assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
}
_ => panic!("应该选择全量升级策略"),
}
}
#[test]
fn test_patch_upgrade_same_base_version() {
let _temp_dir = setup_test_environment();
let manager =
UpgradeStrategyManager::new("0.0.13".to_string(), false, create_test_manifest());
let strategy = manager.determine_strategy().unwrap();
match strategy {
UpgradeStrategy::PatchUpgrade { target_version, .. } => {
assert_eq!(target_version, "0.0.13.2".parse::<Version>().unwrap());
}
_ => panic!("应该选择增量升级策略"),
}
}
#[test]
fn test_force_full_upgrade() {
let _temp_dir = setup_test_environment();
let manager =
UpgradeStrategyManager::new("0.0.13.2".to_string(), true, create_test_manifest());
let strategy = manager.determine_strategy().unwrap();
assert!(matches!(strategy, UpgradeStrategy::FullUpgrade { .. }));
}
}