diff --git a/crates/devolutions-agent-shared/src/lib.rs b/crates/devolutions-agent-shared/src/lib.rs index 23d155057..dabbc4619 100644 --- a/crates/devolutions-agent-shared/src/lib.rs +++ b/crates/devolutions-agent-shared/src/lib.rs @@ -11,9 +11,9 @@ use camino::Utf8PathBuf; use cfg_if::cfg_if; pub use date_version::{DateVersion, DateVersionError}; pub use update_manifest::{ - InstalledProductUpdateInfo, ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, UpdateManifest, UpdateManifestV1, - UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, VersionSpecification, - default_schedule_window_start, detect_update_manifest_major_version, + InstalledProductUpdateInfo, ProductUpdateInfo, ProductUpdateInfoV1, UPDATE_MANIFEST_V2_MINOR_VERSION, + UpdateManifest, UpdateManifestV1, UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, + VersionSpecification, default_schedule_window_start, detect_update_manifest_major_version, }; pub use update_status::{UpdateStatus, UpdateStatusV2}; diff --git a/crates/devolutions-agent-shared/src/update_manifest.rs b/crates/devolutions-agent-shared/src/update_manifest.rs index 1ab43dfc4..3ea846b8f 100644 --- a/crates/devolutions-agent-shared/src/update_manifest.rs +++ b/crates/devolutions-agent-shared/src/update_manifest.rs @@ -11,17 +11,17 @@ use crate::DateVersion; /// /// ```json /// { -/// "Gateway": { "Version": "1.2.3.4" }, -/// "HubService": { "Version": "latest" } +/// "Gateway": { "TargetVersion": "1.2.3.4" }, +/// "HubService": { "TargetVersion": "latest" } /// } /// ``` #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "PascalCase")] pub struct UpdateManifestV1 { #[serde(skip_serializing_if = "Option::is_none")] - pub gateway: Option, + pub gateway: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub hub_service: Option, + pub hub_service: Option, } // ── Shared value types ──────────────────────────────────────────────────────── @@ -55,6 +55,28 @@ impl std::str::FromStr for VersionSpecification { } } +/// Product update info payload used by legacy V1 manifests. +/// +/// Canonical V1 field name is `TargetVersion`. +/// `Version` is accepted as an alias because of already released +/// gateway/agent with incorrect V1 manifest format, so we support both for +/// parsing but only emit the correct `TargetVersion` in serialization. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct ProductUpdateInfoV1 { + /// The version of the product to update to. + #[serde(rename = "TargetVersion", alias = "Version")] + pub target_version: VersionSpecification, +} + +impl From for ProductUpdateInfo { + fn from(value: ProductUpdateInfoV1) -> Self { + Self { + target_version: value.target_version, + } + } +} + +/// Product update info payload used by V2 manifests (`Products` map). #[derive(Debug, Clone, Deserialize, Serialize)] pub struct ProductUpdateInfo { /// The version of the product to update to. @@ -235,19 +257,18 @@ impl UpdateManifest { /// Normalise the manifest into a flat product map for uniform processing. /// /// - V2 `products` is used directly. - /// - V1 named fields are mapped to their [`UpdateProductKey`] equivalents. - /// - V1 `other` entries are best-effort converted; entries that do not match - /// [`ProductUpdateInfo`]'s schema are silently dropped. + /// - V1 named fields are mapped to their [`UpdateProductKey`] equivalents and + /// converted from `TargetVersion` payloads into [`ProductUpdateInfo`]. pub fn into_products(self) -> HashMap { match self { Self::ManifestV2(v2) => v2.products, Self::Legacy(v1) => { let mut map = HashMap::new(); if let Some(gw) = v1.gateway { - map.insert(UpdateProductKey::Gateway, gw); + map.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - map.insert(UpdateProductKey::HubService, hs); + map.insert(UpdateProductKey::HubService, hs.into()); } map } @@ -393,7 +414,7 @@ mod tests { #[test] fn v1_with_products_into_products() { - let json = r#"{"Gateway":{"Version":"2026.1.0"},"HubService":{"Version":"latest"}}"#; + let json = r#"{"Gateway":{"TargetVersion":"2026.1.0"},"HubService":{"TargetVersion":"latest"}}"#; let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); assert!(matches!(manifest, UpdateManifest::Legacy(_))); let products = manifest.into_products(); @@ -404,6 +425,29 @@ mod tests { )); } + #[test] + fn v1_accepted_with_version_alias() { + let json = r#"{"HubService":{"Version":"2026.2.1.7"}}"#; + let manifest = UpdateManifest::parse(json.as_bytes()).unwrap(); + assert!(matches!(manifest, UpdateManifest::Legacy(_))); + + // Re-serializing the parsed V1 manifest should emit the canonical `TargetVersion` field. + let UpdateManifest::Legacy(v1) = &manifest else { + unreachable!(); + }; + assert_eq!( + serde_json::to_string(v1).unwrap(), + r#"{"HubService":{"TargetVersion":"2026.2.1.7"}}"# + ); + + let products = manifest.into_products(); + assert_eq!(products.len(), 1); + assert_eq!( + products[&UpdateProductKey::HubService].target_version, + VersionSpecification::Specific("2026.2.1.7".parse().unwrap()) + ); + } + #[test] fn bom_is_stripped() { // UTF-8 BOM prefix diff --git a/devolutions-agent/src/updater/mod.rs b/devolutions-agent/src/updater/mod.rs index 084dddb9d..9b1652b4d 100644 --- a/devolutions-agent/src/updater/mod.rs +++ b/devolutions-agent/src/updater/mod.rs @@ -503,10 +503,10 @@ async fn read_update_json(update_file_path: &Utf8Path) -> anyhow::Result { let mut products = HashMap::new(); if let Some(gw) = v1.gateway { - products.insert(UpdateProductKey::Gateway, gw); + products.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - products.insert(UpdateProductKey::HubService, hs); + products.insert(UpdateProductKey::HubService, hs.into()); } UpdateManifest::ManifestV2(UpdateManifestV2 { products, diff --git a/devolutions-gateway/src/api/update.rs b/devolutions-gateway/src/api/update.rs index 83c54491c..e439c1892 100644 --- a/devolutions-gateway/src/api/update.rs +++ b/devolutions-gateway/src/api/update.rs @@ -56,10 +56,10 @@ async fn read_manifest() -> Result<(UpdateManifestV2, bool), HttpError> { UpdateManifest::Legacy(v1) => { let mut products = HashMap::new(); if let Some(gw) = v1.gateway { - products.insert(UpdateProductKey::Gateway, gw); + products.insert(UpdateProductKey::Gateway, gw.into()); } if let Some(hs) = v1.hub_service { - products.insert(UpdateProductKey::HubService, hs); + products.insert(UpdateProductKey::HubService, hs.into()); } Ok(( UpdateManifestV2 {