diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fc1436f..19f5ad1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ * This makes it more convenient to configure the default environment * feat: Validate call argument against candid interface * The interface is fetched from canister metadata onchain +* feat: Accept an environment as argument for network commands * feat: call argument building interactively using candid assist # v0.1.0-beta.2 diff --git a/crates/icp-cli/src/commands/network/args.rs b/crates/icp-cli/src/commands/network/args.rs new file mode 100644 index 00000000..d0162947 --- /dev/null +++ b/crates/icp-cli/src/commands/network/args.rs @@ -0,0 +1,58 @@ +use clap::Args; +use icp::{context::NetworkOrEnvironmentSelection, project::DEFAULT_LOCAL_NETWORK_NAME}; + +#[derive(Args, Clone, Debug)] +pub(crate) struct NetworkOrEnvironmentArgs { + /// Name of the network to use + #[arg( + help = "Name of the network to use. Overrides ICP_ENVIRONMENT if set.", + long_help = "Name of the network to use.\n\n\ + Takes precedence over -e/--environment and the ICP_ENVIRONMENT \ + environment variable when specified explicitly." + )] + pub(crate) name: Option, + + /// Use the network from the specified environment + #[arg( + short = 'e', + long, + help = "Use the network from the specified environment", + long_help = "Use the network configured in the specified environment.\n\n\ + Cannot be used together with an explicit network name argument.\n\ + The ICP_ENVIRONMENT environment variable is also checked when \ + neither network name nor -e flag is specified." + )] + pub(crate) environment: Option, +} + +impl From for Result { + fn from(args: NetworkOrEnvironmentArgs) -> Self { + // Check for mutual exclusivity (both explicit) + if args.name.is_some() && args.environment.is_some() { + return Err(anyhow::anyhow!( + "Cannot specify both network name and environment. \ + Use either a network name or -e/--environment, not both." + )); + } + + // Precedence 1: Explicit network name (highest) + if let Some(name) = args.name { + return Ok(NetworkOrEnvironmentSelection::Network(name)); + } + + // Precedence 2: Explicit environment flag + if let Some(env_name) = args.environment { + return Ok(NetworkOrEnvironmentSelection::Environment(env_name)); + } + + // Precedence 3: ICP_ENVIRONMENT variable + if let Ok(env_name) = std::env::var("ICP_ENVIRONMENT") { + return Ok(NetworkOrEnvironmentSelection::Environment(env_name)); + } + + // Precedence 4: Default to "local" network (lowest) + Ok(NetworkOrEnvironmentSelection::Network( + DEFAULT_LOCAL_NETWORK_NAME.to_string(), + )) + } +} diff --git a/crates/icp-cli/src/commands/network/mod.rs b/crates/icp-cli/src/commands/network/mod.rs index 04a149ba..b5355d53 100644 --- a/crates/icp-cli/src/commands/network/mod.rs +++ b/crates/icp-cli/src/commands/network/mod.rs @@ -1,5 +1,6 @@ use clap::Subcommand; +mod args; pub(crate) mod list; pub(crate) mod ping; pub(crate) mod start; diff --git a/crates/icp-cli/src/commands/network/ping.rs b/crates/icp-cli/src/commands/network/ping.rs index 4fe3063d..ce1b393e 100644 --- a/crates/icp-cli/src/commands/network/ping.rs +++ b/crates/icp-cli/src/commands/network/ping.rs @@ -1,18 +1,39 @@ -use anyhow::{anyhow, bail}; +use anyhow::bail; use clap::Args; use ic_agent::{Agent, agent::status::Status}; -use icp::{identity::IdentitySelection, project::DEFAULT_LOCAL_NETWORK_NAME}; +use icp::identity::IdentitySelection; use std::time::Duration; use tokio::time::sleep; +use super::args::NetworkOrEnvironmentArgs; use icp::context::Context; /// Try to connect to a network, and print out its status. #[derive(Args, Debug)] +#[command(after_long_help = "\ +Examples: + + # Ping default 'local' network + icp network ping + + # Ping explicit network + icp network ping mynetwork + + # Ping using environment flag + icp network ping -e staging + + # Ping using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network ping + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network ping local + + # Wait until healthy + icp network ping --wait-healthy +")] pub(crate) struct PingArgs { - /// The compute network to connect to. By default, ping the local network. - #[arg(value_name = "NETWORK", default_value = DEFAULT_LOCAL_NETWORK_NAME)] - network: String, + #[clap(flatten)] + network_selection: NetworkOrEnvironmentArgs, /// Repeatedly ping until the replica is healthy or 1 minute has passed. #[arg(long)] @@ -20,19 +41,15 @@ pub(crate) struct PingArgs { } pub(crate) async fn exec(ctx: &Context, args: &PingArgs) -> Result<(), anyhow::Error> { - // Load Project - let p = ctx.project.load().await?; + // Load project + let _ = ctx.project.load().await?; - // Obtain network configuration - let network = p.networks.get(&args.network).ok_or_else(|| { - anyhow!( - "project does not contain a network named '{}'", - args.network - ) - })?; + // Convert args to selection and get network + let selection: Result<_, _> = args.network_selection.clone().into(); + let network = ctx.get_network_or_environment(&selection?).await?; // NetworkAccess - let access = ctx.network.access(network).await?; + let access = ctx.network.access(&network).await?; // Agent let agent = ctx diff --git a/crates/icp-cli/src/commands/network/start.rs b/crates/icp-cli/src/commands/network/start.rs index 25bd66c8..40d92b93 100644 --- a/crates/icp-cli/src/commands/network/start.rs +++ b/crates/icp-cli/src/commands/network/start.rs @@ -1,20 +1,40 @@ -use anyhow::{Context as _, anyhow, bail}; +use anyhow::{Context as _, bail}; use clap::Args; use icp::{ identity::manifest::IdentityList, network::{Configuration, run_network}, - project::DEFAULT_LOCAL_NETWORK_NAME, }; use tracing::debug; +use super::args::NetworkOrEnvironmentArgs; use icp::context::Context; /// Run a given network #[derive(Args, Debug)] +#[command(after_long_help = "\ +Examples: + + # Use default 'local' network + icp network start + + # Use explicit network name + icp network start mynetwork + + # Use environment flag + icp network start -e staging + + # Use ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network start + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network start local + + # Background mode with environment + icp network start -e staging -d +")] pub(crate) struct StartArgs { - /// Name of the network to start - #[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)] - name: String, + #[clap(flatten)] + network_selection: NetworkOrEnvironmentArgs, /// Starts the network in a background process. This command will exit once the network is running. /// To stop the network, use 'icp network stop'. @@ -26,11 +46,9 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: // Load project let p = ctx.project.load().await?; - // Obtain network configuration - let network = p - .networks - .get(&args.name) - .ok_or_else(|| anyhow!("project does not contain a network named '{}'", args.name))?; + // Convert args to selection and get network + let selection: Result<_, _> = args.network_selection.clone().into(); + let network = ctx.get_network_or_environment(&selection?).await?; let cfg = match &network.configuration { // Locally-managed network @@ -38,24 +56,24 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: // Non-managed networks cannot be started Configuration::Connected { connected: _ } => { - bail!("network '{}' is not a managed network", args.name) + bail!("network '{}' is not a managed network", network.name) } }; let pdir = &p.dir; // Network directory - let nd = ctx.network.get_network_directory(network)?; + let nd = ctx.network.get_network_directory(&network)?; nd.ensure_exists() .context("failed to create network directory")?; if nd.load_network_descriptor().await?.is_some() { - bail!("network '{}' is already running", args.name); + bail!("network '{}' is already running", network.name); } // Clean up any existing canister ID mappings of which environment is on this network for env in p.environments.values() { - if env.network == *network { + if env.network == network { // It's been ensured that the network is managed, so is_cache is true. ctx.ids.cleanup(true, env.name.as_str())?; } diff --git a/crates/icp-cli/src/commands/network/status.rs b/crates/icp-cli/src/commands/network/status.rs index 85af56fb..0370b129 100644 --- a/crates/icp-cli/src/commands/network/status.rs +++ b/crates/icp-cli/src/commands/network/status.rs @@ -1,14 +1,36 @@ -use anyhow::{anyhow, bail}; +use anyhow::bail; use clap::Args; -use icp::{context::Context, network::Configuration, project::DEFAULT_LOCAL_NETWORK_NAME}; +use icp::{context::Context, network::Configuration}; use serde::Serialize; +use super::args::NetworkOrEnvironmentArgs; + /// Get status information about a running network #[derive(Args, Debug)] +#[command(after_long_help = "\ +Examples: + + # Get status of default 'local' network + icp network status + + # Get status of explicit network + icp network status mynetwork + + # Get status using environment flag + icp network status -e staging + + # Get status using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network status + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network status local + + # JSON output + icp network status --json +")] pub(crate) struct StatusArgs { - /// Name of the network - #[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)] - name: String, + #[clap(flatten)] + network_selection: NetworkOrEnvironmentArgs, /// Format output as JSON #[arg(long = "json")] @@ -25,27 +47,25 @@ struct NetworkStatus { pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow::Error> { // Load project - let p = ctx.project.load().await?; + let _ = ctx.project.load().await?; - // Obtain network configuration - let network = p - .networks - .get(&args.name) - .ok_or_else(|| anyhow!("project does not contain a network named '{}'", args.name))?; + // Convert args to selection and get network + let selection: Result<_, _> = args.network_selection.clone().into(); + let network = ctx.get_network_or_environment(&selection?).await?; // Ensure it's a managed network if let Configuration::Connected { connected: _ } = &network.configuration { - bail!("network '{}' is not a managed network", args.name) + bail!("network '{}' is not a managed network", network.name) }; // Network directory - let nd = ctx.network.get_network_directory(network)?; + let nd = ctx.network.get_network_directory(&network)?; // Load network descriptor let descriptor = nd .load_network_descriptor() .await? - .ok_or_else(|| anyhow!("network '{}' is not running", args.name))?; + .ok_or_else(|| anyhow::anyhow!("network '{}' is not running", network.name))?; // Build status structure let status = NetworkStatus { diff --git a/crates/icp-cli/src/commands/network/stop.rs b/crates/icp-cli/src/commands/network/stop.rs index be4e7b20..1fc74ffa 100644 --- a/crates/icp-cli/src/commands/network/stop.rs +++ b/crates/icp-cli/src/commands/network/stop.rs @@ -1,42 +1,57 @@ -use anyhow::{anyhow, bail}; -use clap::Parser; +use anyhow::bail; +use clap::Args; use icp::{ fs::remove_file, network::{Configuration, config::ChildLocator, managed::run::stop_network}, - project::DEFAULT_LOCAL_NETWORK_NAME, }; +use super::args::NetworkOrEnvironmentArgs; use icp::context::Context; /// Stop a background network -#[derive(Parser, Debug)] +#[derive(Args, Debug)] +#[command(after_long_help = "\ +Examples: + + # Stop default 'local' network + icp network stop + + # Stop explicit network + icp network stop mynetwork + + # Stop using environment flag + icp network stop -e staging + + # Stop using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network stop + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network stop local +")] pub struct Cmd { - /// Name of the network to stop - #[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)] - name: String, + #[clap(flatten)] + network_selection: NetworkOrEnvironmentArgs, } pub async fn exec(ctx: &Context, cmd: &Cmd) -> Result<(), anyhow::Error> { // Load project - let p = ctx.project.load().await?; + let _ = ctx.project.load().await?; - // Obtain network configuration - let network = p - .networks - .get(&cmd.name) - .ok_or_else(|| anyhow!("project does not contain a network named '{}'", cmd.name))?; + // Convert args to selection and get network + let selection: Result<_, _> = cmd.network_selection.clone().into(); + let network = ctx.get_network_or_environment(&selection?).await?; if let Configuration::Connected { connected: _ } = &network.configuration { - bail!("network '{}' is not a managed network", cmd.name) + bail!("network '{}' is not a managed network", network.name) }; // Network directory - let nd = ctx.network.get_network_directory(network)?; + let nd = ctx.network.get_network_directory(&network)?; let descriptor = nd .load_network_descriptor() .await? - .ok_or_else(|| anyhow!("network '{}' is not running", cmd.name))?; + .ok_or_else(|| anyhow::anyhow!("network '{}' is not running", network.name))?; match &descriptor.child_locator { ChildLocator::Pid { pid } => { diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index 760f7dbf..c954433a 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -52,6 +52,15 @@ impl EnvironmentSelection { } } +/// Selection type for network commands that accept either network name or environment +#[derive(Clone, Debug, PartialEq)] +pub enum NetworkOrEnvironmentSelection { + /// Use a network by name + Network(String), + /// Use the network from an environment by name + Environment(String), +} + /// Selection type for canisters - similar to IdentitySelection #[derive(Clone, Debug, PartialEq)] pub enum CanisterSelection { @@ -180,6 +189,28 @@ impl Context { } } + /// Gets a network from either a network name or environment name. + /// + /// # Errors + /// + /// Returns an error if the project cannot be loaded or if the network/environment is not found. + pub async fn get_network_or_environment( + &self, + selection: &NetworkOrEnvironmentSelection, + ) -> Result { + match selection { + NetworkOrEnvironmentSelection::Network(network_name) => { + let network_selection = NetworkSelection::Named(network_name.clone()); + Ok(self.get_network(&network_selection).await?) + } + NetworkOrEnvironmentSelection::Environment(env_name) => { + let env_selection = EnvironmentSelection::Named(env_name.clone()); + let env = self.get_environment(&env_selection).await?; + Ok(env.network) + } + } + } + pub async fn get_canister_and_path_for_env( &self, canister_name: &str, @@ -478,7 +509,7 @@ pub enum GetEnvironmentError { #[snafu(transparent)] ProjectLoad { source: crate::ProjectLoadError }, - #[snafu(display("environment '{}' not found in project", name))] + #[snafu(display("project does not contain an environment named '{}'", name))] EnvironmentNotFound { name: String }, } @@ -487,7 +518,7 @@ pub enum GetNetworkError { #[snafu(transparent)] ProjectLoad { source: crate::ProjectLoadError }, - #[snafu(display("network '{}' not found in project", name))] + #[snafu(display("project does not contain a network named '{}'", name))] NetworkNotFound { name: String }, #[snafu(display("cannot load URL-specified network"))] @@ -497,6 +528,15 @@ pub enum GetNetworkError { DefaultNetwork, } +#[derive(Debug, Snafu)] +pub enum GetNetworkOrEnvironmentError { + #[snafu(transparent)] + NetworkResolution { source: GetNetworkError }, + + #[snafu(transparent)] + EnvironmentResolution { source: GetEnvironmentError }, +} + #[derive(Debug, Snafu)] pub enum GetCanisterIdForEnvError { #[snafu(transparent)] diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 2f824156..17650599 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -626,16 +626,41 @@ Launch and manage local test networks Try to connect to a network, and print out its status -**Usage:** `icp network ping [OPTIONS] [NETWORK]` +**Usage:** `icp network ping [OPTIONS] [NAME]` + +Examples: + + # Ping default 'local' network + icp network ping + + # Ping explicit network + icp network ping mynetwork + + # Ping using environment flag + icp network ping -e staging + + # Ping using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network ping + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network ping local + + # Wait until healthy + icp network ping --wait-healthy + ###### **Arguments:** -* `` — The compute network to connect to. By default, ping the local network +* `` — Name of the network to use. - Default value: `local` + Takes precedence over -e/--environment and the ICP_ENVIRONMENT environment variable when specified explicitly. ###### **Options:** +* `-e`, `--environment ` — Use the network configured in the specified environment. + + Cannot be used together with an explicit network name argument. + The ICP_ENVIRONMENT environment variable is also checked when neither network name nor -e flag is specified. * `--wait-healthy` — Repeatedly ping until the replica is healthy or 1 minute has passed @@ -646,14 +671,39 @@ Run a given network **Usage:** `icp network start [OPTIONS] [NAME]` +Examples: + + # Use default 'local' network + icp network start + + # Use explicit network name + icp network start mynetwork + + # Use environment flag + icp network start -e staging + + # Use ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network start + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network start local + + # Background mode with environment + icp network start -e staging -d + + ###### **Arguments:** -* `` — Name of the network to start +* `` — Name of the network to use. - Default value: `local` + Takes precedence over -e/--environment and the ICP_ENVIRONMENT environment variable when specified explicitly. ###### **Options:** +* `-e`, `--environment ` — Use the network configured in the specified environment. + + Cannot be used together with an explicit network name argument. + The ICP_ENVIRONMENT environment variable is also checked when neither network name nor -e flag is specified. * `-d`, `--background` — Starts the network in a background process. This command will exit once the network is running. To stop the network, use 'icp network stop' @@ -664,14 +714,39 @@ Get status information about a running network **Usage:** `icp network status [OPTIONS] [NAME]` +Examples: + + # Get status of default 'local' network + icp network status + + # Get status of explicit network + icp network status mynetwork + + # Get status using environment flag + icp network status -e staging + + # Get status using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network status + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network status local + + # JSON output + icp network status --json + + ###### **Arguments:** -* `` — Name of the network +* `` — Name of the network to use. - Default value: `local` + Takes precedence over -e/--environment and the ICP_ENVIRONMENT environment variable when specified explicitly. ###### **Options:** +* `-e`, `--environment ` — Use the network configured in the specified environment. + + Cannot be used together with an explicit network name argument. + The ICP_ENVIRONMENT environment variable is also checked when neither network name nor -e flag is specified. * `--json` — Format output as JSON @@ -680,13 +755,38 @@ Get status information about a running network Stop a background network -**Usage:** `icp network stop [NAME]` +**Usage:** `icp network stop [OPTIONS] [NAME]` + +Examples: + + # Stop default 'local' network + icp network stop + + # Stop explicit network + icp network stop mynetwork + + # Stop using environment flag + icp network stop -e staging + + # Stop using ICP_ENVIRONMENT variable + ICP_ENVIRONMENT=staging icp network stop + + # Name overrides ICP_ENVIRONMENT + ICP_ENVIRONMENT=staging icp network stop local + ###### **Arguments:** -* `` — Name of the network to stop +* `` — Name of the network to use. + + Takes precedence over -e/--environment and the ICP_ENVIRONMENT environment variable when specified explicitly. + +###### **Options:** + +* `-e`, `--environment ` — Use the network configured in the specified environment. - Default value: `local` + Cannot be used together with an explicit network name argument. + The ICP_ENVIRONMENT environment variable is also checked when neither network name nor -e flag is specified.