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 @@ -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
Expand Down
58 changes: 58 additions & 0 deletions crates/icp-cli/src/commands/network/args.rs
Original file line number Diff line number Diff line change
@@ -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<String>,

/// 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<String>,
}

impl From<NetworkOrEnvironmentArgs> for Result<NetworkOrEnvironmentSelection, anyhow::Error> {
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(),
))
}
}
1 change: 1 addition & 0 deletions crates/icp-cli/src/commands/network/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use clap::Subcommand;

mod args;
pub(crate) mod list;
pub(crate) mod ping;
pub(crate) mod start;
Expand Down
47 changes: 32 additions & 15 deletions crates/icp-cli/src/commands/network/ping.rs
Original file line number Diff line number Diff line change
@@ -1,38 +1,55 @@
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)]
wait_healthy: bool,
}

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
Expand Down
46 changes: 32 additions & 14 deletions crates/icp-cli/src/commands/network/start.rs
Original file line number Diff line number Diff line change
@@ -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'.
Expand All @@ -26,36 +46,34 @@ 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
Configuration::Managed { managed: cfg } => cfg,

// 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())?;
}
Expand Down
48 changes: 34 additions & 14 deletions crates/icp-cli/src/commands/network/status.rs
Original file line number Diff line number Diff line change
@@ -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")]
Expand All @@ -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 {
Expand Down
47 changes: 31 additions & 16 deletions crates/icp-cli/src/commands/network/stop.rs
Original file line number Diff line number Diff line change
@@ -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 } => {
Expand Down
Loading