Skip to content

Commit 1f74d9b

Browse files
authored
feat: Accept an environment as argument for network commands (#259)
1 parent e7098ca commit 1f74d9b

File tree

9 files changed

+341
-71
lines changed

9 files changed

+341
-71
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* This makes it more convenient to configure the default environment
1212
* feat: Validate call argument against candid interface
1313
* The interface is fetched from canister metadata onchain
14+
* feat: Accept an environment as argument for network commands
1415
* feat: call argument building interactively using candid assist
1516

1617
# v0.1.0-beta.2
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
use clap::Args;
2+
use icp::{context::NetworkOrEnvironmentSelection, project::DEFAULT_LOCAL_NETWORK_NAME};
3+
4+
#[derive(Args, Clone, Debug)]
5+
pub(crate) struct NetworkOrEnvironmentArgs {
6+
/// Name of the network to use
7+
#[arg(
8+
help = "Name of the network to use. Overrides ICP_ENVIRONMENT if set.",
9+
long_help = "Name of the network to use.\n\n\
10+
Takes precedence over -e/--environment and the ICP_ENVIRONMENT \
11+
environment variable when specified explicitly."
12+
)]
13+
pub(crate) name: Option<String>,
14+
15+
/// Use the network from the specified environment
16+
#[arg(
17+
short = 'e',
18+
long,
19+
help = "Use the network from the specified environment",
20+
long_help = "Use the network configured in the specified environment.\n\n\
21+
Cannot be used together with an explicit network name argument.\n\
22+
The ICP_ENVIRONMENT environment variable is also checked when \
23+
neither network name nor -e flag is specified."
24+
)]
25+
pub(crate) environment: Option<String>,
26+
}
27+
28+
impl From<NetworkOrEnvironmentArgs> for Result<NetworkOrEnvironmentSelection, anyhow::Error> {
29+
fn from(args: NetworkOrEnvironmentArgs) -> Self {
30+
// Check for mutual exclusivity (both explicit)
31+
if args.name.is_some() && args.environment.is_some() {
32+
return Err(anyhow::anyhow!(
33+
"Cannot specify both network name and environment. \
34+
Use either a network name or -e/--environment, not both."
35+
));
36+
}
37+
38+
// Precedence 1: Explicit network name (highest)
39+
if let Some(name) = args.name {
40+
return Ok(NetworkOrEnvironmentSelection::Network(name));
41+
}
42+
43+
// Precedence 2: Explicit environment flag
44+
if let Some(env_name) = args.environment {
45+
return Ok(NetworkOrEnvironmentSelection::Environment(env_name));
46+
}
47+
48+
// Precedence 3: ICP_ENVIRONMENT variable
49+
if let Ok(env_name) = std::env::var("ICP_ENVIRONMENT") {
50+
return Ok(NetworkOrEnvironmentSelection::Environment(env_name));
51+
}
52+
53+
// Precedence 4: Default to "local" network (lowest)
54+
Ok(NetworkOrEnvironmentSelection::Network(
55+
DEFAULT_LOCAL_NETWORK_NAME.to_string(),
56+
))
57+
}
58+
}

crates/icp-cli/src/commands/network/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use clap::Subcommand;
22

3+
mod args;
34
pub(crate) mod list;
45
pub(crate) mod ping;
56
pub(crate) mod start;

crates/icp-cli/src/commands/network/ping.rs

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,55 @@
1-
use anyhow::{anyhow, bail};
1+
use anyhow::bail;
22
use clap::Args;
33
use ic_agent::{Agent, agent::status::Status};
4-
use icp::{identity::IdentitySelection, project::DEFAULT_LOCAL_NETWORK_NAME};
4+
use icp::identity::IdentitySelection;
55
use std::time::Duration;
66
use tokio::time::sleep;
77

8+
use super::args::NetworkOrEnvironmentArgs;
89
use icp::context::Context;
910

1011
/// Try to connect to a network, and print out its status.
1112
#[derive(Args, Debug)]
13+
#[command(after_long_help = "\
14+
Examples:
15+
16+
# Ping default 'local' network
17+
icp network ping
18+
19+
# Ping explicit network
20+
icp network ping mynetwork
21+
22+
# Ping using environment flag
23+
icp network ping -e staging
24+
25+
# Ping using ICP_ENVIRONMENT variable
26+
ICP_ENVIRONMENT=staging icp network ping
27+
28+
# Name overrides ICP_ENVIRONMENT
29+
ICP_ENVIRONMENT=staging icp network ping local
30+
31+
# Wait until healthy
32+
icp network ping --wait-healthy
33+
")]
1234
pub(crate) struct PingArgs {
13-
/// The compute network to connect to. By default, ping the local network.
14-
#[arg(value_name = "NETWORK", default_value = DEFAULT_LOCAL_NETWORK_NAME)]
15-
network: String,
35+
#[clap(flatten)]
36+
network_selection: NetworkOrEnvironmentArgs,
1637

1738
/// Repeatedly ping until the replica is healthy or 1 minute has passed.
1839
#[arg(long)]
1940
wait_healthy: bool,
2041
}
2142

2243
pub(crate) async fn exec(ctx: &Context, args: &PingArgs) -> Result<(), anyhow::Error> {
23-
// Load Project
24-
let p = ctx.project.load().await?;
44+
// Load project
45+
let _ = ctx.project.load().await?;
2546

26-
// Obtain network configuration
27-
let network = p.networks.get(&args.network).ok_or_else(|| {
28-
anyhow!(
29-
"project does not contain a network named '{}'",
30-
args.network
31-
)
32-
})?;
47+
// Convert args to selection and get network
48+
let selection: Result<_, _> = args.network_selection.clone().into();
49+
let network = ctx.get_network_or_environment(&selection?).await?;
3350

3451
// NetworkAccess
35-
let access = ctx.network.access(network).await?;
52+
let access = ctx.network.access(&network).await?;
3653

3754
// Agent
3855
let agent = ctx

crates/icp-cli/src/commands/network/start.rs

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,40 @@
1-
use anyhow::{Context as _, anyhow, bail};
1+
use anyhow::{Context as _, bail};
22
use clap::Args;
33
use icp::{
44
identity::manifest::IdentityList,
55
network::{Configuration, run_network},
6-
project::DEFAULT_LOCAL_NETWORK_NAME,
76
};
87
use tracing::debug;
98

9+
use super::args::NetworkOrEnvironmentArgs;
1010
use icp::context::Context;
1111

1212
/// Run a given network
1313
#[derive(Args, Debug)]
14+
#[command(after_long_help = "\
15+
Examples:
16+
17+
# Use default 'local' network
18+
icp network start
19+
20+
# Use explicit network name
21+
icp network start mynetwork
22+
23+
# Use environment flag
24+
icp network start -e staging
25+
26+
# Use ICP_ENVIRONMENT variable
27+
ICP_ENVIRONMENT=staging icp network start
28+
29+
# Name overrides ICP_ENVIRONMENT
30+
ICP_ENVIRONMENT=staging icp network start local
31+
32+
# Background mode with environment
33+
icp network start -e staging -d
34+
")]
1435
pub(crate) struct StartArgs {
15-
/// Name of the network to start
16-
#[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)]
17-
name: String,
36+
#[clap(flatten)]
37+
network_selection: NetworkOrEnvironmentArgs,
1838

1939
/// Starts the network in a background process. This command will exit once the network is running.
2040
/// To stop the network, use 'icp network stop'.
@@ -26,36 +46,34 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::
2646
// Load project
2747
let p = ctx.project.load().await?;
2848

29-
// Obtain network configuration
30-
let network = p
31-
.networks
32-
.get(&args.name)
33-
.ok_or_else(|| anyhow!("project does not contain a network named '{}'", args.name))?;
49+
// Convert args to selection and get network
50+
let selection: Result<_, _> = args.network_selection.clone().into();
51+
let network = ctx.get_network_or_environment(&selection?).await?;
3452

3553
let cfg = match &network.configuration {
3654
// Locally-managed network
3755
Configuration::Managed { managed: cfg } => cfg,
3856

3957
// Non-managed networks cannot be started
4058
Configuration::Connected { connected: _ } => {
41-
bail!("network '{}' is not a managed network", args.name)
59+
bail!("network '{}' is not a managed network", network.name)
4260
}
4361
};
4462

4563
let pdir = &p.dir;
4664

4765
// Network directory
48-
let nd = ctx.network.get_network_directory(network)?;
66+
let nd = ctx.network.get_network_directory(&network)?;
4967
nd.ensure_exists()
5068
.context("failed to create network directory")?;
5169

5270
if nd.load_network_descriptor().await?.is_some() {
53-
bail!("network '{}' is already running", args.name);
71+
bail!("network '{}' is already running", network.name);
5472
}
5573

5674
// Clean up any existing canister ID mappings of which environment is on this network
5775
for env in p.environments.values() {
58-
if env.network == *network {
76+
if env.network == network {
5977
// It's been ensured that the network is managed, so is_cache is true.
6078
ctx.ids.cleanup(true, env.name.as_str())?;
6179
}

crates/icp-cli/src/commands/network/status.rs

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
1-
use anyhow::{anyhow, bail};
1+
use anyhow::bail;
22
use clap::Args;
3-
use icp::{context::Context, network::Configuration, project::DEFAULT_LOCAL_NETWORK_NAME};
3+
use icp::{context::Context, network::Configuration};
44
use serde::Serialize;
55

6+
use super::args::NetworkOrEnvironmentArgs;
7+
68
/// Get status information about a running network
79
#[derive(Args, Debug)]
10+
#[command(after_long_help = "\
11+
Examples:
12+
13+
# Get status of default 'local' network
14+
icp network status
15+
16+
# Get status of explicit network
17+
icp network status mynetwork
18+
19+
# Get status using environment flag
20+
icp network status -e staging
21+
22+
# Get status using ICP_ENVIRONMENT variable
23+
ICP_ENVIRONMENT=staging icp network status
24+
25+
# Name overrides ICP_ENVIRONMENT
26+
ICP_ENVIRONMENT=staging icp network status local
27+
28+
# JSON output
29+
icp network status --json
30+
")]
831
pub(crate) struct StatusArgs {
9-
/// Name of the network
10-
#[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)]
11-
name: String,
32+
#[clap(flatten)]
33+
network_selection: NetworkOrEnvironmentArgs,
1234

1335
/// Format output as JSON
1436
#[arg(long = "json")]
@@ -25,27 +47,25 @@ struct NetworkStatus {
2547

2648
pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow::Error> {
2749
// Load project
28-
let p = ctx.project.load().await?;
50+
let _ = ctx.project.load().await?;
2951

30-
// Obtain network configuration
31-
let network = p
32-
.networks
33-
.get(&args.name)
34-
.ok_or_else(|| anyhow!("project does not contain a network named '{}'", args.name))?;
52+
// Convert args to selection and get network
53+
let selection: Result<_, _> = args.network_selection.clone().into();
54+
let network = ctx.get_network_or_environment(&selection?).await?;
3555

3656
// Ensure it's a managed network
3757
if let Configuration::Connected { connected: _ } = &network.configuration {
38-
bail!("network '{}' is not a managed network", args.name)
58+
bail!("network '{}' is not a managed network", network.name)
3959
};
4060

4161
// Network directory
42-
let nd = ctx.network.get_network_directory(network)?;
62+
let nd = ctx.network.get_network_directory(&network)?;
4363

4464
// Load network descriptor
4565
let descriptor = nd
4666
.load_network_descriptor()
4767
.await?
48-
.ok_or_else(|| anyhow!("network '{}' is not running", args.name))?;
68+
.ok_or_else(|| anyhow::anyhow!("network '{}' is not running", network.name))?;
4969

5070
// Build status structure
5171
let status = NetworkStatus {

crates/icp-cli/src/commands/network/stop.rs

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,57 @@
1-
use anyhow::{anyhow, bail};
2-
use clap::Parser;
1+
use anyhow::bail;
2+
use clap::Args;
33
use icp::{
44
fs::remove_file,
55
network::{Configuration, config::ChildLocator, managed::run::stop_network},
6-
project::DEFAULT_LOCAL_NETWORK_NAME,
76
};
87

8+
use super::args::NetworkOrEnvironmentArgs;
99
use icp::context::Context;
1010

1111
/// Stop a background network
12-
#[derive(Parser, Debug)]
12+
#[derive(Args, Debug)]
13+
#[command(after_long_help = "\
14+
Examples:
15+
16+
# Stop default 'local' network
17+
icp network stop
18+
19+
# Stop explicit network
20+
icp network stop mynetwork
21+
22+
# Stop using environment flag
23+
icp network stop -e staging
24+
25+
# Stop using ICP_ENVIRONMENT variable
26+
ICP_ENVIRONMENT=staging icp network stop
27+
28+
# Name overrides ICP_ENVIRONMENT
29+
ICP_ENVIRONMENT=staging icp network stop local
30+
")]
1331
pub struct Cmd {
14-
/// Name of the network to stop
15-
#[arg(default_value = DEFAULT_LOCAL_NETWORK_NAME)]
16-
name: String,
32+
#[clap(flatten)]
33+
network_selection: NetworkOrEnvironmentArgs,
1734
}
1835

1936
pub async fn exec(ctx: &Context, cmd: &Cmd) -> Result<(), anyhow::Error> {
2037
// Load project
21-
let p = ctx.project.load().await?;
38+
let _ = ctx.project.load().await?;
2239

23-
// Obtain network configuration
24-
let network = p
25-
.networks
26-
.get(&cmd.name)
27-
.ok_or_else(|| anyhow!("project does not contain a network named '{}'", cmd.name))?;
40+
// Convert args to selection and get network
41+
let selection: Result<_, _> = cmd.network_selection.clone().into();
42+
let network = ctx.get_network_or_environment(&selection?).await?;
2843

2944
if let Configuration::Connected { connected: _ } = &network.configuration {
30-
bail!("network '{}' is not a managed network", cmd.name)
45+
bail!("network '{}' is not a managed network", network.name)
3146
};
3247

3348
// Network directory
34-
let nd = ctx.network.get_network_directory(network)?;
49+
let nd = ctx.network.get_network_directory(&network)?;
3550

3651
let descriptor = nd
3752
.load_network_descriptor()
3853
.await?
39-
.ok_or_else(|| anyhow!("network '{}' is not running", cmd.name))?;
54+
.ok_or_else(|| anyhow::anyhow!("network '{}' is not running", network.name))?;
4055

4156
match &descriptor.child_locator {
4257
ChildLocator::Pid { pid } => {

0 commit comments

Comments
 (0)