Skip to content

Commit

Permalink
feat(agent): Gateway updater logic
Browse files Browse the repository at this point in the history
  • Loading branch information
pacmancoder committed Jun 18, 2024
1 parent 99039d2 commit 83f96e6
Show file tree
Hide file tree
Showing 22 changed files with 1,647 additions and 109 deletions.
263 changes: 235 additions & 28 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 17 additions & 0 deletions crates/devolutions-agent-shared/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "devolutions-agent-shared"
version = "0.0.0"
authors = ["Devolutions Inc. <[email protected]>"]
description = "Shared code between the Devolutions Agent and the Devolutions Gateway"
edition = "2021"
publish = false


[dependencies]
camino = { version = "1.1" }
cfg-if = "1.0"
serde = { version = "1.0", features = ["derive"] }
thiserror = "1.0"

[dev-dependencies]
serde_json = "1.0"
110 changes: 110 additions & 0 deletions crates/devolutions-agent-shared/src/date_version.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
use std::{fmt, str::FromStr};

use serde::{Deserialize, Serialize};
use thiserror::Error;

#[derive(Debug, Error)]
#[error("invalid date (`YYYY.MM.DD.R`) version")]
pub struct DateVersionError;

/// Parsed application version represented in the format `YYYY.MM.DD.R`
#[derive(Debug, Default, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)]
pub struct DateVersion {
// NOTE: Field order is important for `PartialOrd` and `Ord` derives
pub year: u32,
pub month: u32,
pub day: u32,
pub revision: u32,
}

impl Serialize for DateVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
self.to_string().serialize(serializer)
}
}

impl<'de> Deserialize<'de> for DateVersion {
fn deserialize<D>(deserializer: D) -> Result<DateVersion, D::Error>
where
D: serde::de::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
DateVersion::from_str(&s).map_err(serde::de::Error::custom)
}
}

impl FromStr for DateVersion {
type Err = DateVersionError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut parts = s.splitn(4, '.');

let mut next_part = || parts.next().and_then(|s| u32::from_str(s).ok()).ok_or(DateVersionError);

let year = next_part()?;
let month = next_part()?;
let day = next_part()?;
// Allow version without revision
let revision = next_part().unwrap_or(0);

if parts.next().is_some() {
return Err(DateVersionError);
}

Ok(DateVersion {
year,
month,
day,
revision,
})
}
}

impl fmt::Display for DateVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}.{}", self.year, self.month, self.day, self.revision)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn date_version_rountrip() {
let version = DateVersion {
year: 2022,
month: 10,
day: 1,
revision: 2,
};

let version_str = version.to_string();
assert_eq!(version_str, "2022.10.1.2");
let parsed_version = DateVersion::from_str(&version_str).unwrap();

assert_eq!(version, parsed_version);
}

// Regression test in case field order gets changed
#[test]
fn date_version_ordering() {
const VERSIONS_ASCENDING_PAIRS: &[(&'static str, &'static str)] = &[
// cases (>) for fields in order
("2022.10.1.2", "2022.10.1.1"),
("2022.10.2.1", "2022.10.1.1"),
("2022.11.1.1", "2022.10.1.1"),
("2023.10.1.1", "2022.10.1.1"),
];

for (v1, v2) in VERSIONS_ASCENDING_PAIRS {
let greater = DateVersion::from_str(v1).unwrap();
let lesser = DateVersion::from_str(v2).unwrap();

assert!(greater > lesser);
}
}
}
54 changes: 54 additions & 0 deletions crates/devolutions-agent-shared/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
mod date_version;
mod update_json;

use std::env;

use camino::Utf8PathBuf;
use cfg_if::cfg_if;

pub use date_version::{DateVersion, DateVersionError};
pub use update_json::{ProductUpdateInfo, UpdateJson, VersionSpecification};

cfg_if! {
if #[cfg(target_os = "windows")] {
const COMPANY_DIR: &str = "Devolutions";
const PROGRAM_DIR: &str = "Agent";
const APPLICATION_DIR: &str = "Devolutions\\Agent";
} else if #[cfg(target_os = "macos")] {
const COMPANY_DIR: &str = "Devolutions";
const PROGRAM_DIR: &str = "Agent";
const APPLICATION_DIR: &str = "Devolutions Agent";
} else {
const COMPANY_DIR: &str = "devolutions";
const PROGRAM_DIR: &str = "agent";
const APPLICATION_DIR: &str = "devolutions-agent";
}
}

pub fn get_data_dir() -> Utf8PathBuf {
if let Ok(config_path_env) = env::var("DAGENT_CONFIG_PATH") {
Utf8PathBuf::from(config_path_env)
} else {
let mut config_path = Utf8PathBuf::new();

if cfg!(target_os = "windows") {
let program_data_env = env::var("ProgramData").expect("ProgramData env variable");
config_path.push(program_data_env);
config_path.push(COMPANY_DIR);
config_path.push(PROGRAM_DIR);
} else if cfg!(target_os = "macos") {
config_path.push("/Library/Application Support");
config_path.push(APPLICATION_DIR);
} else {
config_path.push("/etc");
config_path.push(APPLICATION_DIR);
}

config_path
}
}

/// Returns the path to the `update.json` file
pub fn get_updater_file_path() -> Utf8PathBuf {
get_data_dir().join("update.json")
}
68 changes: 68 additions & 0 deletions crates/devolutions-agent-shared/src/update_json.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use crate::DateVersion;
use serde::{Deserialize, Serialize};
use std::fmt;

/// Example JSON structure:
///
/// ```json
/// {
/// "Gateway": {
/// "TargetVersion": "1.2.3.4"
/// }
/// }
/// ```
///
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct UpdateJson {
#[serde(skip_serializing_if = "Option::is_none")]
pub gateway: Option<ProductUpdateInfo>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum VersionSpecification {
Latest,
#[serde(untagged)]
Specific(DateVersion),
}

impl fmt::Display for VersionSpecification {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VersionSpecification::Latest => write!(f, "latest"),
VersionSpecification::Specific(version) => write!(f, "{}", version),
}
}
}

#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "PascalCase")]
pub struct ProductUpdateInfo {
/// The version of the product to update to.
pub target_version: VersionSpecification,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn version_specification_roundtrip() {
let cases: &[(&'static str, VersionSpecification)] = &[
(
"2022.2.24.0",
VersionSpecification::Specific("2022.2.24.0".parse().unwrap()),
),
("latest", VersionSpecification::Latest),
];

for (serialized, deserizlized) in cases {
let parsed = serde_json::from_str::<VersionSpecification>(&format!("\"{}\"", serialized)).unwrap();
assert_eq!(parsed, *deserizlized);

let reserialized = serde_json::to_string(&parsed).unwrap();
assert_eq!(reserialized, format!("\"{}\"", serialized));
}
}
}
67 changes: 46 additions & 21 deletions devolutions-agent/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,38 +4,63 @@ version = "2024.1.2"
edition = "2021"
license = "MIT/Apache-2.0"
authors = ["Devolutions Inc. <[email protected]>"]
description = "Agent companion service for Devolutions Gateway"
build = "build.rs"
publish = false

[dependencies]
# In-house
devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" }
devolutions-log = { path = "../crates/devolutions-log" }

# Lifecycle
anyhow = "1.0"
async-trait = "0.1"
# TODO: serde not needed?
camino = { version = "1.1", features = ["serde1"] }
ceviche = "0.6"
ctrlc = "3.1"

# Serialization
devolutions-agent-shared = { path = "../crates/devolutions-agent-shared" }
devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" }
devolutions-log = { path = "../crates/devolutions-log" }
futures = "0.3"
hex = "0.4"
notify-debouncer-mini = "0.4.1"
parking_lot = "0.12"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-native-roots"] }
serde = "1.0"
serde_derive = "1.0"
serde_json = "1.0"

# Error handling
anyhow = "1.0"

# Utils, misc
camino = { version = "1.1", features = ["serde1"] }
cfg-if = "1.0"
parking_lot = "0.12"
sha2 = "0.10"
smallvec = "1"
tap = "1.0"
thiserror = "1"
tracing = "0.1"
uuid = { version = "1.5", features = ["v4"] }

# Async
futures = "0.3"
tokio = { version = "1.37", features = ["signal", "net", "io-util", "time", "rt", "rt-multi-thread", "sync", "macros", "parking_lot", "fs"] }
[dependencies.tokio]
version = "1.37"
features = [
"signal",
"net",
"io-util",
"rt",
"rt-multi-thread",
"macros",
"parking_lot",
"fs",
"process"
]

# Logging
tracing = "0.1"
[target.'cfg(windows)'.dependencies]
winreg = "0.52"

[target.'cfg(windows)'.dependencies.windows]
version = "0.57"
features = [
"Win32_Foundation",
"Win32_Storage_FileSystem",
"Win32_Security",
"Win32_System_Threading",
"Win32_Security_Cryptography",
"Win32_Security_Authorization",
"Win32_System_ApplicationInstallationAndServicing"
]

[target.'cfg(windows)'.build-dependencies]
embed-resource = "2.4"
embed-resource = "2.4"
Loading

0 comments on commit 83f96e6

Please sign in to comment.