From e81acdd090bc9e22bc97e27cfafa3a7246a6db91 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Wed, 13 Dec 2023 15:23:55 +0000 Subject: [PATCH] feat: support node manager releases The main noteworthy thing here is that the node manager is in its own repository. This crate was originally setup to pull releases from the `safe_network` workspace repository, so at the moment the node manager is a bit of a special case. However, it could be the case that in the future we will pull binaries from more repositories. This change supports that. The node manager is a single-crate repository, which makes it much easier to get the latest version: you can use the specific API for that, rather than processing the whole release list. --- src/error.rs | 6 ++- src/lib.rs | 49 +++++++++++++++++++++- tests/test_download_from_s3.rs | 71 ++++++++++++++++++++++++++++++++ tests/test_get_latest_version.rs | 11 +++++ 4 files changed, 134 insertions(+), 3 deletions(-) diff --git a/src/error.rs b/src/error.rs index b112451..66c458a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -13,16 +13,18 @@ pub type Result = std::result::Result; #[derive(Debug, Error)] #[allow(missing_docs)] pub enum Error { - #[error(transparent)] - DateTimeParseError(#[from] chrono::ParseError), #[error("Cannot parse file name from the URL")] CannotParseFilenameFromUrl, + #[error(transparent)] + DateTimeParseError(#[from] chrono::ParseError), #[error("Could not convert API response header links to string")] HeaderLinksToStrError, #[error(transparent)] Io(#[from] std::io::Error), #[error("Latest release not found for {0}")] LatestReleaseNotFound(String), + #[error("The Github API's latest release response document was not in the expected format")] + MalformedLatestReleaseResponse, #[error("{0}")] PlatformNotSupported(String), #[error(transparent)] diff --git a/src/lib.rs b/src/lib.rs index 4c2fbe7..c9c78c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,6 +29,7 @@ const GITHUB_ORG_NAME: &str = "maidsafe"; const GITHUB_REPO_NAME: &str = "safe_network"; const SAFE_S3_BASE_URL: &str = "https://sn-cli.s3.eu-west-2.amazonaws.com"; const SAFENODE_S3_BASE_URL: &str = "https://sn-node.s3.eu-west-2.amazonaws.com"; +const SAFENODE_MANAGER_S3_BASE_URL: &str = "https://sn-node-manager.s3.eu-west-2.amazonaws.com"; const SAFENODE_RPC_CLIENT_S3_BASE_URL: &str = "https://sn-node-rpc-client.s3.eu-west-2.amazonaws.com"; const TESTNET_S3_BASE_URL: &str = "https://sn-testnet.s3.eu-west-2.amazonaws.com"; @@ -37,6 +38,7 @@ const TESTNET_S3_BASE_URL: &str = "https://sn-testnet.s3.eu-west-2.amazonaws.com pub enum ReleaseType { Safe, Safenode, + SafenodeManager, SafenodeRpcClient, Testnet, } @@ -49,6 +51,7 @@ impl fmt::Display for ReleaseType { match self { ReleaseType::Safe => "safe", ReleaseType::Safenode => "safenode", + ReleaseType::SafenodeManager => "safenode-manager", ReleaseType::SafenodeRpcClient => "safenode_rpc_client", ReleaseType::Testnet => "testnet", } @@ -56,11 +59,24 @@ impl fmt::Display for ReleaseType { } } +impl ReleaseType { + pub fn get_repo_name(&self) -> String { + match &self { + ReleaseType::Safe + | ReleaseType::Safenode + | ReleaseType::SafenodeRpcClient + | ReleaseType::Testnet => "safe_network".to_string(), + ReleaseType::SafenodeManager => "sn-node-manager".to_string(), + } + } +} + lazy_static! { static ref RELEASE_TYPE_CRATE_NAME_MAP: HashMap = { let mut m = HashMap::new(); m.insert(ReleaseType::Safe, "sn_cli"); m.insert(ReleaseType::Safenode, "sn_node"); + m.insert(ReleaseType::SafenodeManager, "sn-node-manager"); m.insert(ReleaseType::SafenodeRpcClient, "sn_node_rpc_client"); m.insert(ReleaseType::Testnet, "sn_testnet"); m @@ -135,6 +151,7 @@ impl dyn SafeReleaseRepositoryInterface { github_api_base_url: GITHUB_API_URL.to_string(), safe_base_url: SAFE_S3_BASE_URL.to_string(), safenode_base_url: SAFENODE_S3_BASE_URL.to_string(), + safenode_manager_base_url: SAFENODE_MANAGER_S3_BASE_URL.to_string(), safenode_rpc_client_base_url: SAFENODE_RPC_CLIENT_S3_BASE_URL.to_string(), testnet_base_url: TESTNET_S3_BASE_URL.to_string(), }) @@ -145,6 +162,7 @@ pub struct SafeReleaseRepository { pub github_api_base_url: String, pub safe_base_url: String, pub safenode_base_url: String, + pub safenode_manager_base_url: String, pub safenode_rpc_client_base_url: String, pub testnet_base_url: String, } @@ -154,11 +172,33 @@ impl SafeReleaseRepository { match release_type { ReleaseType::Safe => self.safe_base_url.clone(), ReleaseType::Safenode => self.safenode_base_url.clone(), + ReleaseType::SafenodeManager => self.safenode_manager_base_url.clone(), ReleaseType::SafenodeRpcClient => self.safenode_rpc_client_base_url.clone(), ReleaseType::Testnet => self.testnet_base_url.clone(), } } + async fn get_latest_release_tag(&self, release_type: &ReleaseType) -> Result { + let client = Client::new(); + let response = client + .get(format!( + "{}/repos/{}/{}/releases/latest", + self.github_api_base_url, + GITHUB_ORG_NAME, + release_type.get_repo_name() + )) + .header("User-Agent", "request") + .send() + .await?; + + let latest_release = response.json::().await?; + if let Some(Value::String(tag_name)) = latest_release.get("tag_name") { + return Ok(tag_name.trim_start_matches('v').to_string()); + } + + Err(Error::MalformedLatestReleaseResponse) + } + async fn get_releases_page(&self, page: u32, per_page: u32) -> Result { let client = Client::new(); let response = client @@ -225,7 +265,10 @@ impl SafeReleaseRepository { #[async_trait] impl SafeReleaseRepositoryInterface for SafeReleaseRepository { - /// Gets the latest version for a specified binary in the `safe_network` repository. + /// Gets the latest version for a specified binary. + /// + /// If we are looking for a node manager release, this is not a workspace repo, so we can + /// simply use the latest release API. Otherwise, we will query the `safe_network` repo. /// /// Each release in the repository is checked, starting from the most recent. The `safe_network` /// repository is a workspace to which many binaries are released, so it's not possible to use the @@ -252,6 +295,10 @@ impl SafeReleaseRepositoryInterface for SafeReleaseRepository { /// - The received JSON data from the API is not as expected /// - No releases are found that match the specified `ReleaseType` async fn get_latest_version(&self, release_type: &ReleaseType) -> Result { + if *release_type == ReleaseType::SafenodeManager { + return self.get_latest_release_tag(release_type).await; + } + let mut page = 1; let per_page = 100; let mut latest_release: Option<(String, DateTime)> = None; diff --git a/tests/test_download_from_s3.rs b/tests/test_download_from_s3.rs index 33922f6..d9ec3b6 100644 --- a/tests/test_download_from_s3.rs +++ b/tests/test_download_from_s3.rs @@ -13,6 +13,7 @@ use sn_releases::{ArchiveType, Platform, ReleaseType, SafeReleaseRepositoryInter const SAFE_VERSION: &str = "0.83.51"; const SAFENODE_VERSION: &str = "0.93.7"; +const SAFENODE_MANAGER_VERSION: &str = "0.1.8"; const SAFENODE_RPC_CLIENT_VERSION: &str = "0.1.40"; const TESTNET_VERSION: &str = "0.2.213"; @@ -50,6 +51,7 @@ async fn download_and_extract( let binary_name = match release_type { ReleaseType::Safe => "safe", ReleaseType::Safenode => "safenode", + ReleaseType::SafenodeManager => "safenode-manager", ReleaseType::SafenodeRpcClient => "safenode_rpc_client", ReleaseType::Testnet => "testnet", }; @@ -372,3 +374,72 @@ async fn should_download_and_extract_safenode_rpc_client_for_windows() { ) .await; } + +/// +/// Node Manager Tests +/// +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_linux_musl() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::LinuxMusl, + &ArchiveType::TarGz, + ) + .await; +} + +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_linux_musl_aarch64() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::LinuxMuslAarch64, + &ArchiveType::TarGz, + ) + .await; +} + +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_linux_musl_arm() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::LinuxMuslArm, + &ArchiveType::TarGz, + ) + .await; +} + +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_linux_musl_arm_v7() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::LinuxMuslArmV7, + &ArchiveType::TarGz, + ) + .await; +} + +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_macos() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::MacOs, + &ArchiveType::TarGz, + ) + .await; +} + +#[tokio::test] +async fn should_download_and_extract_safenode_manager_for_windows() { + download_and_extract( + &ReleaseType::SafenodeManager, + SAFENODE_MANAGER_VERSION, + &Platform::Windows, + &ArchiveType::Zip, + ) + .await; +} diff --git a/tests/test_get_latest_version.rs b/tests/test_get_latest_version.rs index c082724..e9e3ca0 100644 --- a/tests/test_get_latest_version.rs +++ b/tests/test_get_latest_version.rs @@ -47,6 +47,17 @@ async fn should_get_latest_version_of_safenode_rpc_client() { assert!(valid_semver_format(&version)); } +#[tokio::test] +async fn should_get_latest_version_of_safenode_manager() { + let release_type = ReleaseType::SafenodeManager; + let release_repo = ::default_config(); + let version = release_repo + .get_latest_version(&release_type) + .await + .unwrap(); + assert!(valid_semver_format(&version)); +} + #[tokio::test] async fn should_get_latest_version_of_testnet() { let release_type = ReleaseType::Testnet;