Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions crates/devolutions-agent-shared/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
64 changes: 54 additions & 10 deletions crates/devolutions-agent-shared/src/update_manifest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProductUpdateInfo>,
pub gateway: Option<ProductUpdateInfoV1>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hub_service: Option<ProductUpdateInfo>,
pub hub_service: Option<ProductUpdateInfoV1>,
}

// ── Shared value types ────────────────────────────────────────────────────────
Expand Down Expand Up @@ -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<ProductUpdateInfoV1> 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.
Expand Down Expand Up @@ -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<UpdateProductKey, ProductUpdateInfo> {
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
}
Expand Down Expand Up @@ -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();
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions devolutions-agent/src/updater/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -503,10 +503,10 @@ async fn read_update_json(update_file_path: &Utf8Path) -> anyhow::Result<UpdateM
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());
}
UpdateManifest::ManifestV2(UpdateManifestV2 {
products,
Expand Down
4 changes: 2 additions & 2 deletions devolutions-gateway/src/api/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading