use crate::cli::{
clap::{required_string, value_arg},
common::{
COLLECTION_MODE_LIVE, CurrentUnixSecsError, SOURCE_ENDPOINT_ARG, collection_help,
current_unix_secs, json_arg, output_format, source_endpoint_arg, write_text_or_json,
},
};
use clap::{ArgMatches, Command as ClapCommand};
use ic_query::cloud_engine::{
CloudEngineHostError, CloudEngineSourceRequest, DEFAULT_CLOUD_ENGINE_SOURCE_ENDPOINT,
build_cloud_engine_operator_report, build_cloud_engine_prices_report,
cloud_engine_operator_report_text, cloud_engine_prices_report_text,
};
use std::io;
use thiserror::Error as ThisError;
const INFO_HELP_AFTER: &str = "\
Examples:
icq cloud-engine info 2nl67-oqoc5-cmocj-otlhq-kr2kr-53hov-drrds-7ihcs-fhomv-2eyvu-6qe
icq cloud-engine info 2nl67-oqoc5-cmocj-otlhq-kr2kr-53hov-drrds-7ihcs-fhomv-2eyvu-6qe --json
This command makes one control-plane query to resolve the Subnet and four
public operator queries when an operator is registered. The responses are not
certified and the sequential calls are not an exact point-in-time snapshot.";
const PRICES_HELP_AFTER: &str = "\
Examples:
icq cloud-engine prices
icq cloud-engine prices --json
This command makes exactly two control-plane queries: one for the network fee
and one for at most 1,000 public marketplace rows. The responses are not
certified or presented as an exact point-in-time snapshot.";
#[derive(Debug, ThisError)]
pub enum CloudEngineCommandError {
#[error(transparent)]
Host(#[from] CloudEngineHostError),
#[error(transparent)]
Clock(#[from] CurrentUnixSecsError),
#[error(transparent)]
Io(#[from] io::Error),
#[error(transparent)]
Json(#[from] serde_json::Error),
}
pub fn run_matches(matches: &ArgMatches, network: &str) -> Result<(), CloudEngineCommandError> {
match matches.subcommand() {
Some(("info", matches)) => run_info(matches, network),
Some(("prices", matches)) => run_prices(matches, network),
_ => unreachable!("clap requires a known cloud-engine subcommand"),
}
}
fn run_info(matches: &ArgMatches, network: &str) -> Result<(), CloudEngineCommandError> {
let request = source_request(matches, network)?;
let report =
build_cloud_engine_operator_report(&request, &required_string(matches, "subnet-id"))?;
write_text_or_json(
output_format(matches),
&report,
cloud_engine_operator_report_text,
)
}
fn run_prices(matches: &ArgMatches, network: &str) -> Result<(), CloudEngineCommandError> {
let request = source_request(matches, network)?;
let report = build_cloud_engine_prices_report(&request)?;
write_text_or_json(
output_format(matches),
&report,
cloud_engine_prices_report_text,
)
}
fn source_request(
matches: &ArgMatches,
network: &str,
) -> Result<CloudEngineSourceRequest, CurrentUnixSecsError> {
Ok(CloudEngineSourceRequest::from_unix_secs(
network,
required_string(matches, SOURCE_ENDPOINT_ARG),
current_unix_secs()?,
"ic-query",
))
}
pub fn command() -> ClapCommand {
ClapCommand::new("cloud-engine")
.bin_name("icq cloud-engine")
.about("Inspect public CloudEngine control-plane metadata")
.subcommand(info_command())
.subcommand(prices_command())
.after_help("Examples:\n icq cloud-engine info <subnet-id>\n icq cloud-engine prices")
}
fn info_command() -> ClapCommand {
report_args(
ClapCommand::new("info")
.bin_name("icq cloud-engine info")
.about("Show the operator binding and public settings for one CloudEngine Subnet")
.arg(
value_arg("subnet-id")
.required(true)
.value_name("subnet-id")
.help("CloudEngine Subnet principal to resolve"),
),
)
.after_help(collection_help(COLLECTION_MODE_LIVE, INFO_HELP_AFTER))
}
fn prices_command() -> ClapCommand {
report_args(
ClapCommand::new("prices")
.bin_name("icq cloud-engine prices")
.about("Show the public CloudEngine network fee and marketplace prices"),
)
.after_help(collection_help(COLLECTION_MODE_LIVE, PRICES_HELP_AFTER))
}
fn report_args(command: ClapCommand) -> ClapCommand {
command.arg(json_arg()).arg(
source_endpoint_arg(DEFAULT_CLOUD_ENGINE_SOURCE_ENDPOINT)
.help("IC API endpoint used for native CloudEngine queries"),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::{
clap::{parse_matches, render_help},
common::OutputFormat,
};
use std::ffi::OsString;
#[test]
fn usage_discloses_bounded_uncertified_reports() {
let usage = render_help(command());
assert!(usage.contains("Usage: icq cloud-engine [COMMAND]"));
assert!(usage.contains("info"));
assert!(usage.contains("prices"));
let info = render_help(info_command());
assert!(info.contains("<subnet-id>"));
assert!(info.contains("operator queries"));
assert!(info.contains("certified"));
let prices = render_help(prices_command());
assert!(prices.contains("exactly two control-plane queries"));
assert!(prices.contains("1,000"));
assert!(prices.contains(COLLECTION_MODE_LIVE));
}
#[test]
fn help_and_version_do_not_make_live_calls() {
for args in [
&["cloud-engine", "--help"][..],
&["cloud-engine", "info", "--help"],
&["cloud-engine", "prices", "--help"],
&["cloud-engine", "--version"],
] {
assert!(crate::run(args.iter().map(OsString::from)).is_ok());
}
}
#[test]
fn report_options_default_to_native_endpoint_and_text() {
let matches = parse_matches(prices_command(), Vec::<OsString>::new())
.expect("parse default CloudEngine options");
assert_eq!(
required_string(&matches, SOURCE_ENDPOINT_ARG),
DEFAULT_CLOUD_ENGINE_SOURCE_ENDPOINT
);
assert_eq!(output_format(&matches), OutputFormat::Text);
}
}