Skip to content

Commit 2c017fc

Browse files
committed
improved gateway <-> agent updater communication
1 parent e61aebd commit 2c017fc

File tree

12 files changed

+432
-95
lines changed

12 files changed

+432
-95
lines changed

crates/devolutions-agent-shared/src/lib.rs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,20 @@
22
pub mod windows;
33

44
mod date_version;
5-
mod update_json;
5+
mod update_manifest;
6+
mod update_status;
67

78
use std::env;
89

910
use camino::Utf8PathBuf;
1011
use cfg_if::cfg_if;
11-
12-
#[rustfmt::skip]
1312
pub use date_version::{DateVersion, DateVersionError};
14-
#[rustfmt::skip]
15-
pub use update_json::{
16-
ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, UpdateJson, UpdateManifest,
17-
UpdateManifestV2, UpdateProductKey, UpdateSchedule, VersionMajorV2, VersionSpecification,
13+
pub use update_manifest::{
14+
ProductUpdateInfo, UPDATE_MANIFEST_V2_MINOR_VERSION, UpdateManifest, UpdateManifestV1, UpdateManifestV2,
15+
UpdateProductKey, UpdateSchedule, VersionMajorV2, VersionSpecification, default_schedule_window_start,
1816
detect_update_manifest_major_version,
1917
};
18+
pub use update_status::UpdateStatus;
2019

2120
cfg_if! {
2221
if #[cfg(target_os = "windows")] {
@@ -78,7 +77,12 @@ pub fn get_data_dir() -> Utf8PathBuf {
7877
}
7978
}
8079

81-
/// Returns the path to the `update.json` file
80+
/// Returns the path to the `update.json` file.
8281
pub fn get_updater_file_path() -> Utf8PathBuf {
8382
get_data_dir().join("update.json")
8483
}
84+
85+
/// Returns the path to the `agent_status.json` file.
86+
pub fn get_agent_status_file_path() -> Utf8PathBuf {
87+
get_data_dir().join("agent_status.json")
88+
}

crates/devolutions-agent-shared/src/update_json.rs renamed to crates/devolutions-agent-shared/src/update_manifest.rs

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ use serde::{Deserialize, Deserializer, Serialize, Serializer};
55

66
use crate::DateVersion;
77

8-
// ── V1 ── (keep for backward-compat; written by gateways <= 2026.1.0) ───────
9-
8+
/// Old gateway-written manifest format (v2026.1.0 and prior), supported for backward compatibility.
9+
///
1010
/// Example V1 JSON structure:
1111
///
1212
/// ```json
@@ -17,7 +17,7 @@ use crate::DateVersion;
1717
/// ```
1818
#[derive(Debug, Default, Deserialize, Serialize)]
1919
#[serde(rename_all = "PascalCase")]
20-
pub struct UpdateJson {
20+
pub struct UpdateManifestV1 {
2121
#[serde(skip_serializing_if = "Option::is_none")]
2222
pub gateway: Option<ProductUpdateInfo>,
2323
#[serde(skip_serializing_if = "Option::is_none")]
@@ -62,25 +62,27 @@ pub struct ProductUpdateInfo {
6262
pub target_version: VersionSpecification,
6363
}
6464

65-
// ── V2 ── (new agents init update.json with `{"VersionMajor": "2"}`) ─────────
66-
67-
/// Minor version of the V2 manifest format written by this build of the agent.
65+
/// Minor version of the V2 manifest format written by current build of the agent.
6866
///
69-
/// The minor version tracks feature-set additions within V2 (major version):
70-
/// a new updatable product or capability increments this value so the gateway
71-
/// can detect what the agent supports and reject requests for unsupported features.
67+
/// The minor version tracks backwards-compatible changes within V2 (major version) manifest.
68+
/// Increment this value when adding new fields to `UpdateManifestV2` or making other non-breaking
69+
/// changes that require gateway and agent to be updated in tandem to take advantage of the new
70+
/// features.
7271
pub const UPDATE_MANIFEST_V2_MINOR_VERSION: u32 = 1;
7372

74-
fn default_schedule_window_start() -> u32 {
73+
pub fn default_schedule_window_start() -> u32 {
7574
7_200
7675
}
7776

7877
/// Auto-update schedule for the Devolutions Agent, embedded in [`UpdateManifestV2`].
7978
///
8079
/// Written by the gateway via `POST /jet/update/schedule` and consumed by the agent,
81-
/// which validates the values, applies them to the running poll loop, and persists them
82-
/// to `agent.json`. Exposed to the gateway via `GET /jet/update/schedule` so that the
83-
/// current schedule is always readable without touching `agent.json`.
80+
/// which validates the values, applies them to the running scheduling loop, and persists them
81+
/// to `agent.json`.
82+
///
83+
/// Additionally, Agent writes the current scheduler recorded in `agent.json`
84+
/// so gateway can retrieve it back via `GET /jet/update/schedule` without needing to introduce
85+
/// knowledge of agent's configuration file format on the gateway side.
8486
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
8587
#[serde(rename_all = "PascalCase")]
8688
pub struct UpdateSchedule {
@@ -99,7 +101,7 @@ pub struct UpdateSchedule {
99101

100102
/// End of the maintenance window as seconds past midnight, local time, exclusive.
101103
///
102-
/// `None` means no upper bound (only single update check at update_window_start).
104+
/// `None` means no upper bound.
103105
/// When end < start the window crosses midnight.
104106
#[serde(default, skip_serializing_if = "Option::is_none")]
105107
pub update_window_end: Option<u32>,
@@ -109,13 +111,13 @@ pub struct UpdateSchedule {
109111
pub products: Vec<UpdateProductKey>,
110112
}
111113

112-
/// Sentinel type that always serializes/deserializes as the number `2`.
114+
/// Marker type that always serializes/deserializes as the number `2`.
113115
///
114116
/// Embedded as the `VersionMajor` field in [`UpdateManifestV2`] so that the
115117
/// untagged [`UpdateManifest`] enum can distinguish V2 from legacy V1 payloads:
116118
/// if `VersionMajor` is absent or not `"2"`, `ManifestV2` deserialization fails
117119
/// and the `Legacy` variant is tried next. When a V3 format is introduced, a new
118-
/// sentinel and `ManifestV3` variant are added in the same way.
120+
/// marker type and `ManifestV3` variant are added in a similar way.
119121
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
120122
pub struct VersionMajorV2;
121123

@@ -145,7 +147,9 @@ impl<'de> Deserialize<'de> for VersionMajorV2 {
145147
}
146148
}
147149

148-
/// V2 manifest: major-version sentinel, minor feature version, product map, and schedule.
150+
/// Version 2 of the update manifest format, written by agent/gateway >=2026.2.0.
151+
/// Includes product update list and auto-update schedule. Adding a product name should always
152+
/// increase the minor version, to allow the gateway API caller to know supported products list.
149153
///
150154
/// Example (full V2 file):
151155
/// ```json
@@ -163,10 +167,10 @@ impl<'de> Deserialize<'de> for VersionMajorV2 {
163167
#[serde(rename_all = "PascalCase")]
164168
pub struct UpdateManifestV2 {
165169
/// Always `2` — the presence and value of this field let the untagged
166-
/// [`UpdateManifest`] distinguish V2 from legacy V1 payloads.
170+
/// [`UpdateManifest`] distinguish V2 from legacy V1 payloads and prevent further parsing
171+
/// attempt of V2 structure
167172
pub version_major: VersionMajorV2,
168173
/// Feature-set version within V2. Defaults to `0` when the field is absent in the file.
169-
#[serde(default)]
170174
pub version_minor: u32,
171175
/// Auto-update schedule set by the gateway. Agent persists it to `agent.json`.
172176
#[serde(skip_serializing_if = "Option::is_none")]
@@ -187,8 +191,6 @@ impl Default for UpdateManifestV2 {
187191
}
188192
}
189193

190-
// ── Unified manifest ─────────────────────────────────────────────────────────
191-
192194
/// A parsed update manifest: either a V2 file or a legacy V1 file.
193195
///
194196
/// New agents initialise `update.json` with `{"VersionMajor": "2", "VersionMinor": 0}`;
@@ -205,7 +207,7 @@ pub enum UpdateManifest {
205207
/// V2 format: contains `"VersionMajor": 2`.
206208
ManifestV2(UpdateManifestV2),
207209
/// Legacy V1 format: no `"VersionMajor"` field.
208-
Legacy(UpdateJson),
210+
Legacy(UpdateManifestV1),
209211
}
210212

211213
fn strip_bom(data: &[u8]) -> &[u8] {
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use std::collections::HashMap;
2+
3+
use serde::{Deserialize, Serialize};
4+
5+
use crate::{ProductUpdateInfo, UpdateProductKey, UpdateSchedule};
6+
7+
/// Agent runtime status written to `agent_status.json` on agent start and refreshed
8+
/// after each updater run or auto-update schedule change.
9+
///
10+
/// The gateway reads this file for `GET /jet/update/schedule` so that it can surface
11+
/// current agent state without needing knowledge of the agent's internal `agent.json`
12+
/// configuration format.
13+
///
14+
/// Unlike [`crate::UpdateManifest`] (`update.json`), this file is **read-only** for
15+
/// the Gateway service: its DACL grants NETWORK SERVICE read access but **no write
16+
/// access**. The agent is the sole writer.
17+
///
18+
/// Note: if the agent itself is being updated, `agent_status.json` will be
19+
/// automatically refreshed when the agent restarts after the update completes.
20+
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
21+
#[serde(rename_all = "PascalCase")]
22+
pub struct UpdateStatus {
23+
/// Current auto-update schedule configured for this agent.
24+
#[serde(skip_serializing_if = "Option::is_none")]
25+
pub schedule: Option<UpdateSchedule>,
26+
27+
/// Map of product name → currently **installed** version.
28+
///
29+
/// Each entry's `TargetVersion` field holds the installed version of the product,
30+
/// not a requested upgrade target. Products that are not installed are omitted.
31+
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
32+
pub products: HashMap<UpdateProductKey, ProductUpdateInfo>,
33+
}
34+
35+
impl UpdateStatus {
36+
/// Parse `agent_status.json` bytes.
37+
pub fn parse(data: &[u8]) -> serde_json::Result<Self> {
38+
serde_json::from_slice(data)
39+
}
40+
}

devolutions-agent/src/config.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::sync::Arc;
55

66
use anyhow::{Context, bail};
77
use camino::{Utf8Path, Utf8PathBuf};
8-
use devolutions_agent_shared::get_data_dir;
8+
use devolutions_agent_shared::{default_schedule_window_start, get_data_dir};
99
use serde::{Deserialize, Serialize};
1010
use tap::prelude::*;
1111
use url::Url;
@@ -220,7 +220,7 @@ pub mod dto {
220220
pub interval: u64,
221221

222222
/// Start of the maintenance window as seconds past midnight, local time.
223-
#[serde(default)]
223+
#[serde(default = "default_schedule_window_start")]
224224
pub update_window_start: u32,
225225

226226
/// End of the maintenance window as seconds past midnight, local time, exclusive.

0 commit comments

Comments
 (0)