Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ All notable changes to this project will be documented in this file.
- Support simultaneous unicast and multicast tunnels in doublezerod
- Support publishing and subscribing to multiple multicast groups simultaneously
- CLI
- add `version` field to json output so no longer breaks the json output if the version is out of date
- Support publishing and subscribing a user to multiple multicast groups via `--group` flag
- SDK
- Go SDK can now perform batch writes to device.health and link.health as per rfc12
Expand Down
40 changes: 35 additions & 5 deletions client/doublezero/src/command/latency.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
use crate::command::util;
use clap::Args;
use doublezero_cli::doublezerocommand::CliCommand;
use doublezero_sdk::commands::device::list::ListDeviceCommand;
use doublezero_cli::{
checkversion::{get_version_status, VersionStatus},
doublezerocommand::CliCommand,
};
use doublezero_sdk::{commands::device::list::ListDeviceCommand, ProgramVersion};
use serde::{Deserialize, Serialize};

use crate::{
dzd_latency::retrieve_latencies, requirements::check_doublezero,
servicecontroller::ServiceControllerImpl,
dzd_latency::retrieve_latencies,
requirements::check_doublezero,
servicecontroller::{LatencyRecord, ServiceControllerImpl},
};

#[derive(Args, Debug)]
Expand All @@ -15,14 +20,39 @@ pub struct LatencyCliCommand {
json: bool,
}

/// JSON response wrapper that includes version status information
#[derive(Debug, Serialize, Deserialize)]
struct LatencyJsonResponse {
version: VersionStatus,
latencies: Vec<LatencyRecord>,
}

impl LatencyCliCommand {
pub async fn execute(self, client: &dyn CliCommand) -> eyre::Result<()> {
let controller = ServiceControllerImpl::new(None);
check_doublezero(&controller, client, None).await?;

// Get version status for JSON output
let version_status = get_version_status(client, ProgramVersion::current());

let devices = client.list_device(ListDeviceCommand)?;
let latencies = retrieve_latencies(&controller, &devices, false, None).await?;
util::show_output(latencies, self.json)?;

if self.json {
// For JSON output, include version status in the response
let json_response = LatencyJsonResponse {
version: version_status,
latencies,
};
let output = serde_json::to_string_pretty(&json_response)?;
println!("{output}");
} else {
// For table output, print version warning to stderr if needed
if let Some(msg) = version_status.message() {
eprintln!("{msg}");
}
util::show_output(latencies, false)?;
}

Ok(())
}
Expand Down
39 changes: 35 additions & 4 deletions client/doublezero/src/command/routes.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
use crate::command::util;
use clap::Args;
use doublezero_cli::doublezerocommand::CliCommand;
use doublezero_cli::{
checkversion::{get_version_status, VersionStatus},
doublezerocommand::CliCommand,
};
use doublezero_sdk::ProgramVersion;
use serde::{Deserialize, Serialize};

use crate::{
requirements::check_doublezero, routes::retrieve_routes,
servicecontroller::ServiceControllerImpl,
requirements::check_doublezero,
routes::retrieve_routes,
servicecontroller::{RouteRecord, ServiceControllerImpl},
};

#[derive(Args, Debug)]
Expand All @@ -14,13 +20,38 @@ pub struct RoutesCliCommand {
json: bool,
}

/// JSON response wrapper that includes version status information
#[derive(Debug, Serialize, Deserialize)]
struct RoutesJsonResponse {
version: VersionStatus,
routes: Vec<RouteRecord>,
}

impl RoutesCliCommand {
pub async fn execute(self, client: &dyn CliCommand) -> eyre::Result<()> {
let controller = ServiceControllerImpl::new(None);
check_doublezero(&controller, client, None).await?;

// Get version status for JSON output
let version_status = get_version_status(client, ProgramVersion::current());

let routes = retrieve_routes(&controller, None).await?;
util::show_output(routes, self.json)?;

if self.json {
// For JSON output, include version status in the response
let json_response = RoutesJsonResponse {
version: version_status,
routes,
};
let output = serde_json::to_string_pretty(&json_response)?;
println!("{output}");
} else {
// For table output, print version warning to stderr if needed
if let Some(msg) = version_status.message() {
eprintln!("{msg}");
}
util::show_output(routes, false)?;
}

Ok(())
}
Expand Down
57 changes: 51 additions & 6 deletions client/doublezero/src/command/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ use crate::{
servicecontroller::{ServiceController, ServiceControllerImpl, StatusResponse},
};
use clap::Args;
use doublezero_cli::{doublezerocommand::CliCommand, helpers::print_error};
use doublezero_sdk::commands::{
device::list::ListDeviceCommand, exchange::list::ListExchangeCommand,
user::list::ListUserCommand,
use doublezero_cli::{
checkversion::{get_version_status, VersionStatus},
doublezerocommand::CliCommand,
helpers::print_error,
};
use doublezero_sdk::{
commands::{
device::list::ListDeviceCommand, exchange::list::ListExchangeCommand,
user::list::ListUserCommand,
},
ProgramVersion,
};
use serde::{Deserialize, Serialize};
use tabled::Tabled;
Expand All @@ -34,14 +41,52 @@ struct AppendedStatusResponse {
network: String,
}

/// JSON response wrapper that includes version status information
#[derive(Debug, Serialize, Deserialize)]
struct StatusJsonResponse {
version: VersionStatus,
statuses: Vec<AppendedStatusResponse>,
}

impl StatusCliCommand {
pub async fn execute(&self, client: &dyn CliCommand) -> eyre::Result<()> {
let controller = ServiceControllerImpl::new(None);
check_doublezero(&controller, client, None).await?;

// Get version status for JSON output
let version_status = get_version_status(client, ProgramVersion::current());

match self.command_impl(client, &controller).await {
Ok(responses) => util::show_output(responses, self.json)?,
Ok(responses) => {
if self.json {
// For JSON output, include version status in the response
let json_response = StatusJsonResponse {
version: version_status,
statuses: responses,
};
let output = serde_json::to_string_pretty(&json_response)?;
println!("{output}");
} else {
// For table output, print version warning to stderr if needed
if let Some(msg) = version_status.message() {
eprintln!("{msg}");
}
util::show_output(responses, false)?;
}
}
Err(e) => {
print_error(e);
if self.json {
// For JSON output, include error in a structured format
let error_response = serde_json::json!({
"version": version_status,
"error": e.to_string(),
"statuses": []
});
let output = serde_json::to_string_pretty(&error_response)?;
println!("{output}");
} else {
print_error(e);
}
}
}
Ok(())
Expand Down
138 changes: 138 additions & 0 deletions smartcontract/cli/src/checkversion.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,80 @@
use crate::doublezerocommand::CliCommand;
use doublezero_sdk::{commands::programconfig::get::GetProgramConfigCommand, ProgramVersion};
use serde::{Deserialize, Serialize};
use std::io::Write;

/// Version check status for JSON-compatible output
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum VersionStatus {
/// Version is current or newer than the program version
Current,
/// Version is outdated but still compatible - upgrade recommended
Outdated {
current_version: String,
latest_version: String,
message: String,
},
/// Version is incompatible and must be upgraded
Incompatible {
current_version: String,
min_required_version: String,
message: String,
},
}

impl VersionStatus {
/// Returns true if the version is incompatible and the client should not proceed
pub fn is_incompatible(&self) -> bool {
matches!(self, VersionStatus::Incompatible { .. })
}

/// Returns a warning message if outdated, or an error message if incompatible
pub fn message(&self) -> Option<&str> {
match self {
VersionStatus::Current => None,
VersionStatus::Outdated { message, .. } => Some(message),
VersionStatus::Incompatible { message, .. } => Some(message),
}
}
}

/// Check the client version and return structured status information.
/// This is useful for JSON output where we want to include version warnings in the response.
pub fn get_version_status<C: CliCommand + ?Sized>(
client: &C,
client_version: ProgramVersion,
) -> VersionStatus {
// Check the program configuration version
if let Ok((_, pconfig)) = client.get_program_config(GetProgramConfigCommand) {
// Compare the program version with the client version
// If the program version is incompatible, return incompatible status
if client_version < pconfig.min_compatible_version {
return VersionStatus::Incompatible {
current_version: client_version.to_string(),
min_required_version: pconfig.min_compatible_version.to_string(),
message: format!(
"A new version of the client is available: {} → {}\nYour client version is no longer up to date. Please update it before continuing to use the client.",
client_version, pconfig.min_compatible_version
),
};
}
// Warn the user if their client version is older than the program version
if client_version < pconfig.version {
return VersionStatus::Outdated {
current_version: client_version.to_string(),
latest_version: pconfig.version.to_string(),
message: format!(
"A new version of the client is available: {} → {}\nWe recommend updating to the latest version for the best experience.",
client_version, pconfig.version
),
};
}
}

VersionStatus::Current
}

pub fn check_version<C: CliCommand, W: Write>(
client: &C,
out: &mut W,
Expand Down Expand Up @@ -33,6 +106,71 @@ mod tests {

use super::*;

fn setup_mock_client(
contract_version: ProgramVersion,
min_compatible_version: ProgramVersion,
) -> MockCliCommand {
let mut client = MockCliCommand::new();
client
.expect_get_program_config()
.with(predicate::eq(GetProgramConfigCommand))
.returning(move |_| {
let program_config = ProgramConfig {
account_type: AccountType::ProgramConfig,
bump_seed: 1,
version: contract_version.clone(),
min_compatible_version: min_compatible_version.clone(),
};
Ok((Pubkey::new_unique(), program_config))
});
client
}

#[test]
fn test_get_version_status_current() {
let client = setup_mock_client(ProgramVersion::new(1, 0, 0), ProgramVersion::new(0, 9, 0));
let status = get_version_status(&client, ProgramVersion::new(1, 0, 0));
assert_eq!(status, VersionStatus::Current);
}

#[test]
fn test_get_version_status_outdated() {
let client = setup_mock_client(ProgramVersion::new(1, 5, 10), ProgramVersion::new(1, 1, 0));
let status = get_version_status(&client, ProgramVersion::new(1, 2, 0));
match &status {
VersionStatus::Outdated {
current_version,
latest_version,
..
} => {
assert_eq!(current_version, "1.2.0");
assert_eq!(latest_version, "1.5.10");
}
_ => panic!("Expected Outdated status"),
}
assert!(!status.is_incompatible());
assert!(status.message().is_some());
}

#[test]
fn test_get_version_status_incompatible() {
let client = setup_mock_client(ProgramVersion::new(1, 5, 10), ProgramVersion::new(1, 1, 0));
let status = get_version_status(&client, ProgramVersion::new(1, 0, 0));
match &status {
VersionStatus::Incompatible {
current_version,
min_required_version,
..
} => {
assert_eq!(current_version, "1.0.0");
assert_eq!(min_required_version, "1.1.0");
}
_ => panic!("Expected Incompatible status"),
}
assert!(status.is_incompatible());
assert!(status.message().is_some());
}

pub fn test_check_version(
out: &mut Vec<u8>,
contract_version: ProgramVersion,
Expand Down
Loading