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 Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ tracing.workspace = true
assert_cmd = "2"
camino-tempfile = "1"
indoc.workspace = true
icp = { workspace = true, features = ["test-features"] }
icp = { workspace = true }
nix = { version = "0.30.1", features = ["process", "signal"] }
pocket-ic.workspace = true
predicates = "3"
Expand Down
224 changes: 63 additions & 161 deletions crates/icp-cli/src/commands/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,47 +2,10 @@ use std::fmt::Display;

use candid::Principal;
use clap::Args;
use ic_agent::Agent;
use icp::context::{CanisterSelection, EnvironmentSelection, NetworkSelection};
use icp::identity::IdentitySelection;
use snafu::Snafu;

use crate::{commands::Context, options::IdentityOpt};

#[derive(Debug, Snafu)]
pub(crate) enum ArgValidationError {
#[snafu(display("You can't specify both an environment and a network"))]
EnvironmentAndNetworkSpecified,

#[snafu(display(
"Specifying a network is not supported if you are targeting a canister by name, specify an environment instead"
))]
AmbiguousCanisterName,

#[snafu(transparent)]
EnvironmentError {
source: crate::commands::GetEnvironmentError,
},

#[snafu(transparent)]
GetAgentForEnv {
source: crate::commands::GetAgentForEnvError,
},

#[snafu(transparent)]
GetCanisterIdForEnv {
source: crate::commands::GetCanisterIdForEnvError,
},

#[snafu(transparent)]
GetAgentForNetwork {
source: crate::commands::GetAgentForNetworkError,
},

#[snafu(transparent)]
GetAgentForUrl {
source: crate::commands::GetAgentForUrlError,
},
}

use crate::options::IdentityOpt;

#[derive(Args, Debug)]
pub(crate) struct CanisterEnvironmentArgs {
Expand All @@ -55,15 +18,25 @@ pub(crate) struct CanisterEnvironmentArgs {
pub(crate) environment: Option<Environment>,
}

impl CanisterEnvironmentArgs {
/// Convert arguments into selection enums for canister and environment
pub(crate) fn selections(&self) -> (CanisterSelection, EnvironmentSelection) {
let canister_selection: CanisterSelection = self.canister.clone().into();
let environment_selection: EnvironmentSelection =
self.environment.clone().unwrap_or_default().into();
(canister_selection, environment_selection)
}
}

#[derive(Args, Debug)]
pub(crate) struct CanisterCommandArgs {
// Note: Could have flattened CanisterEnvironmentArg to avoid adding child field
/// Name or principal of canister to target
/// When using a name an environment must be specified
pub(crate) canister: Canister,

/// Name of the network to target
#[arg(long)]
/// Name of the network to target, conflicts with environment argument
#[arg(long, conflicts_with = "environment")]
pub(crate) network: Option<Network>,

/// Name of the target environment
Expand All @@ -75,81 +48,32 @@ pub(crate) struct CanisterCommandArgs {
pub(crate) identity: IdentityOpt,
}

impl CanisterEnvironmentArgs {
pub async fn get_cid_for_environment(
&self,
ctx: &Context,
) -> Result<Principal, ArgValidationError> {
let arg_canister = self.canister.clone();
let arg_environment = self.environment.clone().unwrap_or_default();
let environment_name = arg_environment.name();

let principal = match arg_canister {
Canister::Name(canister_name) => {
ctx.get_canister_id_for_env(&canister_name, environment_name)
.await?
}
Canister::Principal(principal) => {
// Make sure a valid environment was requested
let _ = ctx.get_environment(environment_name).await?;
principal
}
};

Ok(principal)
}
/// Selections derived from CanisterCommandArgs
pub(crate) struct CommandSelections {
pub(crate) canister: CanisterSelection,
pub(crate) environment: EnvironmentSelection,
pub(crate) network: NetworkSelection,
pub(crate) identity: IdentitySelection,
}

impl CanisterCommandArgs {
pub async fn get_cid_and_agent(
&self,
ctx: &Context,
) -> Result<(Principal, Agent), ArgValidationError> {
let arg_canister = self.canister.clone();
let arg_environment = self.environment.clone().unwrap_or_default();
let env_name = arg_environment.name();
let arg_network = self.network.clone();
let identity_selection: IdentitySelection = self.identity.clone().into();

let (cid, agent) = match (arg_canister, &arg_environment, arg_network) {
(_, Environment::Name(_), Some(_)) => {
// Both an environment and a network are specified this is an error
return Err(ArgValidationError::EnvironmentAndNetworkSpecified);
}
(Canister::Name(_), Environment::Default(_), Some(_)) => {
// This is not allowed, we should not use name with an environment not a network
return Err(ArgValidationError::AmbiguousCanisterName);
}
(Canister::Name(cname), _, None) => {
// A canister name was specified so we must be in a project

let agent = ctx.get_agent_for_env(&identity_selection, env_name).await?;
let cid = ctx.get_canister_id_for_env(&cname, env_name).await?;

(cid, agent)
}
(Canister::Principal(principal), _, None) => {
// Call by canister_id to the environment specified

let agent = ctx.get_agent_for_env(&identity_selection, env_name).await?;

(principal, agent)
}
(Canister::Principal(principal), Environment::Default(_), Some(network)) => {
// Should handle known networks by name

let agent = match network {
Network::Name(net_name) => {
ctx.get_agent_for_network(&identity_selection, &net_name)
.await?
}
Network::Url(url) => ctx.get_agent_for_url(&identity_selection, &url).await?,
};
(principal, agent)
}
/// Convert command arguments into selection enums
pub(crate) fn selections(&self) -> CommandSelections {
let canister_selection: CanisterSelection = self.canister.clone().into();
let environment_selection: EnvironmentSelection =
self.environment.clone().unwrap_or_default().into();
let network_selection: NetworkSelection = match self.network.clone() {
Some(network) => network.into_selection(),
None => NetworkSelection::FromEnvironment,
};
let identity_selection: IdentitySelection = self.identity.clone().into();

Ok((cid, agent))
CommandSelections {
canister: canister_selection,
environment: environment_selection,
network: network_selection,
identity: identity_selection,
}
}
}

Expand All @@ -169,6 +93,15 @@ impl From<&str> for Canister {
}
}

impl From<Canister> for CanisterSelection {
fn from(v: Canister) -> Self {
match v {
Canister::Name(name) => CanisterSelection::Named(name),
Canister::Principal(principal) => CanisterSelection::Principal(principal),
}
}
}

impl Display for Canister {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Expand All @@ -194,6 +127,15 @@ impl From<&str> for Network {
}
}

impl Network {
pub(crate) fn into_selection(self) -> NetworkSelection {
match self {
Network::Name(name) => NetworkSelection::Named(name),
Network::Url(url) => NetworkSelection::Url(url),
}
}
}

#[derive(Clone, Debug, PartialEq)]
pub(crate) enum Environment {
Name(String),
Expand Down Expand Up @@ -221,6 +163,15 @@ impl From<&str> for Environment {
}
}

impl From<Environment> for EnvironmentSelection {
fn from(v: Environment) -> Self {
match v {
Environment::Name(name) => EnvironmentSelection::Named(name),
Environment::Default(_) => EnvironmentSelection::Default,
}
}
}

impl Display for Environment {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
Expand All @@ -232,10 +183,6 @@ mod tests {
use candid::Principal;

use super::*;
use icp::MockProjectLoader;
use std::sync::Arc;

use crate::{commands::args::Environment, store_id::MockInMemoryIdStore};

#[test]
fn canister_by_name() {
Expand Down Expand Up @@ -272,49 +219,4 @@ mod tests {
Network::Url("http://www.example.com".to_string()),
);
}

#[tokio::test]
async fn test_get_cid_for_environment() {
use crate::store_id::{Access as IdAccess, Key};
use candid::Principal;

let ids_store = Arc::new(MockInMemoryIdStore::new());

// Register a canister ID for the dev environment
let canister_id = Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap();
ids_store
.register(
&Key {
network: "local".to_string(),
environment: "dev".to_string(),
canister: "backend".to_string(),
},
&canister_id,
)
.unwrap();

let ctx = Context {
project: Arc::new(MockProjectLoader::complex()),
ids: ids_store,
..Context::mocked()
};

let args = CanisterEnvironmentArgs {
canister: Canister::Name("backend".to_string()),
environment: Some(Environment::Name("dev".to_string())),
};

assert!(matches!(args.get_cid_for_environment(&ctx).await, Ok(id) if id == canister_id));

let args = CanisterEnvironmentArgs {
canister: Canister::Name("INVALID".to_string()),
environment: Some(Environment::Name("dev".to_string())),
};

let res = args.get_cid_for_environment(&ctx).await;
assert!(
res.is_err(),
"An invalid canister name should result in an error"
);
}
}
6 changes: 2 additions & 4 deletions crates/icp-cli/src/commands/build/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,11 @@ use clap::Args;
use futures::{StreamExt, stream::FuturesOrdered};
use icp::{
canister::build::{BuildError, Params},
context::Context,
fs::read,
};

use crate::{
commands::Context,
progress::{ProgressManager, ProgressManagerSettings},
};
use crate::progress::{ProgressManager, ProgressManagerSettings};

#[derive(Args, Debug)]
pub(crate) struct BuildArgs {
Expand Down
7 changes: 4 additions & 3 deletions crates/icp-cli/src/commands/canister/binding_env_vars.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ use ic_utils::interfaces::management_canister::builders::EnvironmentVariable;
use icp::{agent, identity, network};
use tracing::debug;

use icp::context::Context;

use crate::{
commands::Context,
options::{EnvironmentOpt, IdentityOpt},
progress::{ProgressManager, ProgressManagerSettings},
store_artifact::LookupError as LookupArtifactError,
store_id::{Key, LookupError as LookupIdError},
};
use icp::store_artifact::LookupError as LookupArtifactError;
use icp::store_id::{Key, LookupError as LookupIdError};

#[derive(Clone, Debug, Args)]
pub(crate) struct BindingArgs {
Expand Down
20 changes: 14 additions & 6 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@ use candid::IDLArgs;
use clap::Args;
use dialoguer::console::Term;

use crate::commands::{
Context,
args::{self, ArgValidationError},
};
use icp::context::Context;

use crate::commands::args;

#[derive(Args, Debug)]
pub(crate) struct CallArgs {
Expand Down Expand Up @@ -36,11 +35,20 @@ pub(crate) enum CommandError {
Call(#[from] ic_agent::AgentError),

#[error(transparent)]
Shared(#[from] ArgValidationError),
GetCanisterIdAndAgent(#[from] icp::context::GetCanisterIdAndAgentError),
}

pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), CommandError> {
let (cid, agent) = args.cmd_args.get_cid_and_agent(ctx).await?;
let selections = args.cmd_args.selections();

let (cid, agent) = ctx
.get_canister_id_and_agent(
&selections.canister,
&selections.environment,
&selections.network,
&selections.identity,
)
.await?;

// Parse candid arguments
let cargs = candid_parser::parse_idl_args(&args.args)?;
Expand Down
5 changes: 3 additions & 2 deletions crates/icp-cli/src/commands/canister/create.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ use icp_canister_interfaces::{
};
use rand::seq::IndexedRandom;

use icp::context::Context;

use crate::{
commands::Context,
options::{EnvironmentOpt, IdentityOpt},
progress::{ProgressManager, ProgressManagerSettings},
store_id::{Key, LookupError, RegisterError},
};
use icp::store_id::{Key, LookupError, RegisterError};

pub(crate) const DEFAULT_CANISTER_CYCLES: u128 = 2 * TRILLION;

Expand Down
Loading