From b9fff6b2fdf9c1593a5a0b7e856a5f01c2c5ad5b Mon Sep 17 00:00:00 2001 From: Damon Barry Date: Mon, 22 Apr 2024 17:38:02 -0700 Subject: [PATCH] Read latest version from product-versions.json (#609) The "aziot-version" check in `aziotctl check` reads the latest identity service version from https://github.com/Azure/azure-iotedge/blob/main/latest-aziot-identity-service.json. This JSON file has no ability to accomodate multiple releases (e.g., 1.4 and 1.5). However, https://github.com/Azure/azure-iotedge/blob/main/product-versions.json was recently introduced and, while it has a more complex structure, it is capable of providing information about multiple versions of the same product. This change updates `aziotctl check` to read the latest identity service version from product-versions.json. Since the logic needs to contend with multiple versions, it now uses the MAJOR.MINOR version from the actual version string to match the expected version. For example, if the user has 1.5.0 installed and 1.5.2 and 1.4.10 are the latest versions, it will match on 1.5 and return 1.5.2 as the expected version. It does not differentiate between the same versions on different channels (e.g., stable vs. LTS); it assumes they are the same (in other words, it assumes product-versions.json will never legitimately show version 1.5.3 in the stable channel and 1.5.2 in the LTS channel). --- Cargo.lock | 7 ++ aziotctl/Cargo.toml | 1 + .../internal/check/checks/aziot_version.rs | 77 +++++++++++++++---- aziotctl/src/internal/check/mod.rs | 2 +- docs/installation.md | 14 +++- 5 files changed, 83 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa4eaf6f2..ec72a8ee4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -577,6 +577,7 @@ dependencies = [ "openssl", "openssl-sys2", "openssl2", + "semver", "serde", "serde_json", "sysinfo", @@ -2225,6 +2226,12 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.164" diff --git a/aziotctl/Cargo.toml b/aziotctl/Cargo.toml index edf3cdafc..547356d0b 100644 --- a/aziotctl/Cargo.toml +++ b/aziotctl/Cargo.toml @@ -23,6 +23,7 @@ libc = "0.2" nix = "0.26" log = { version = "0.4", features = ["std"] } openssl = "0.10" +semver = "1.0" serde = { version = "1", features = ["derive"] } serde_json = "1.0.59" clap = { version = "4", features = ["derive"] } diff --git a/aziotctl/src/internal/check/checks/aziot_version.rs b/aziotctl/src/internal/check/checks/aziot_version.rs index bbed0ce49..1e0ab36e0 100644 --- a/aziotctl/src/internal/check/checks/aziot_version.rs +++ b/aziotctl/src/internal/check/checks/aziot_version.rs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. use anyhow::{anyhow, Context, Result}; +use semver::Version; use serde::{Deserialize, Serialize}; use crate::internal::check::{CheckResult, Checker, CheckerCache, CheckerMeta, CheckerShared}; @@ -33,6 +34,8 @@ impl AziotVersion { shared: &CheckerShared, cache: &mut CheckerCache, ) -> Result { + const URI: &str = "https://aka.ms/azure-iotedge-latest-versions"; + let actual_version = env!("CARGO_PKG_VERSION"); let expected_version = if let Some(expected_aziot_version) = &shared.cfg.expected_aziot_version { @@ -52,13 +55,8 @@ impl AziotVersion { http_common::MaybeProxyConnector::new(shared.cfg.proxy_uri.clone(), None, &[]) .context("could not initialize HTTP connector")?; let client: hyper::Client<_, hyper::Body> = hyper::Client::builder().build(connector); - - let mut uri: hyper::Uri = "https://aka.ms/latest-aziot-identity-service" - .parse() - .expect("hard-coded URI cannot fail to parse"); - let LatestVersions { - aziot_identity_service, - } = loop { + let mut uri: hyper::Uri = URI.parse().expect("hard-coded URI cannot fail to parse"); + let latest_versions = loop { let req = { let mut req = hyper::Request::new(Default::default()); *req.uri_mut() = uri.clone(); @@ -66,8 +64,9 @@ impl AziotVersion { }; let res = client.request(req).await.with_context(|| { - format!("could not query {uri} for latest available version") + format!("could not query {URI} for latest available version") })?; + match res.status() { status_code if status_code.is_redirection() => { uri = res @@ -88,8 +87,9 @@ impl AziotVersion { let body = hyper::body::aggregate(res.into_body()) .await .context("could not read HTTP response")?; - let body = serde_json::from_reader(hyper::body::Buf::reader(body)) - .context("could not read HTTP response")?; + let body: LatestVersions = + serde_json::from_reader(hyper::body::Buf::reader(body)) + .context("could not read HTTP response")?; break body; } @@ -98,11 +98,42 @@ impl AziotVersion { } } }; - aziot_identity_service + + let actual_semver = Version::parse(actual_version) + .context("could not parse actual version as semver")?; + + let versions: Vec = latest_versions + .channels + .iter() + .flat_map(|channel| channel.products.iter()) + .filter(|product| product.id == "aziot-edge") + .flat_map(|product| product.components.iter()) + .filter(|component| component.name == "aziot-identity-service") + .map(|component| component.version.clone()) + .collect(); + + let parsed_versions = versions + .iter() + .map(|version| { + Version::parse(version).context("could not parse expected version as semver") + }) + .collect::>>()?; + + let expected_version = parsed_versions + .iter() + .find(|semver| semver.major == actual_semver.major && semver.minor == actual_semver.minor) + .ok_or_else(|| { + anyhow!( + "could not find aziot-identity-service version {}.{}.x in list of supported products at {}", + actual_semver.major, + actual_semver.minor, + URI + ) + })?; + expected_version.to_string() }; - self.expected_version = Some(expected_version.clone()); - let actual_version = env!("CARGO_PKG_VERSION"); + self.expected_version = Some(expected_version.clone()); self.actual_version = Some(actual_version.to_owned()); if expected_version != actual_version { @@ -121,6 +152,22 @@ impl AziotVersion { #[derive(Debug, Deserialize)] struct LatestVersions { - #[serde(rename = "aziot-identity-service")] - aziot_identity_service: String, + channels: Vec, +} + +#[derive(Debug, Deserialize)] +struct Channel { + products: Vec, +} + +#[derive(Debug, Deserialize)] +struct Product { + id: String, + components: Vec, +} + +#[derive(Debug, Deserialize)] +struct Component { + name: String, + version: String, } diff --git a/aziotctl/src/internal/check/mod.rs b/aziotctl/src/internal/check/mod.rs index 123016711..a377eaffb 100644 --- a/aziotctl/src/internal/check/mod.rs +++ b/aziotctl/src/internal/check/mod.rs @@ -32,7 +32,7 @@ pub struct CheckerCfg { pub proxy_uri: Option, /// If set, the check compares the installed package version to this string. - /// Otherwise, the version is fetched from + /// Otherwise, the version is fetched from #[arg(long, value_name = "VERSION")] pub expected_aziot_version: Option, } diff --git a/docs/installation.md b/docs/installation.md index b8b464dad..cf849ec3a 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -19,9 +19,19 @@ Using Ubuntu 22.04 amd64 as an example: ```bash # query GitHub for the latest versions of IoT Edge and IoT Identity Service -wget -qO- https://raw.githubusercontent.com/Azure/azure-iotedge/main/latest-aziot-edge.json | jq -r '."aziot-edge"' +wget -qO- https://raw.githubusercontent.com/Azure/azure-iotedge/main/product-versions.json | jq -r ' + .channels[] + | select(.name == "stable").products[] + | select(.id == "aziot-edge").components[] + | select(.name == "aziot-edge").version +' # example output: 1.4.20 -wget -qO- https://raw.githubusercontent.com/Azure/azure-iotedge/main/latest-aziot-identity-service.json | jq -r '."aziot-identity-service"' +wget -qO- https://raw.githubusercontent.com/Azure/azure-iotedge/main/product-versions.json | jq -r ' + .channels[] + | select(.name == "stable").products[] + | select(.id == "aziot-edge").components[] + | select(.name == "aziot-identity-service").version +' # example output: 1.4.6 # download and install