Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(agent): Devolutions Gateway service updater #889

Merged
merged 1 commit into from
Jun 20, 2024
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
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);
CBenoit marked this conversation as resolved.
Show resolved Hide resolved

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
Loading