Skip to content

Commit b7b7fd7

Browse files
authored
feat: canister show works in global mode (#174)
1 parent 6b9b9f5 commit b7b7fd7

File tree

4 files changed

+178
-73
lines changed

4 files changed

+178
-73
lines changed

crates/icp-cli/src/commands/args.rs

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ pub(crate) enum ArgValidationError {
1818
))]
1919
AmbiguousCanisterName,
2020

21+
#[snafu(transparent)]
22+
EnvironmentError {
23+
source: crate::commands::GetEnvironmentError,
24+
},
25+
2126
#[snafu(transparent)]
2227
GetAgentForEnv {
2328
source: crate::commands::GetAgentForEnvError,
@@ -39,21 +44,62 @@ pub(crate) enum ArgValidationError {
3944
},
4045
}
4146

47+
#[derive(Args, Debug)]
48+
pub(crate) struct CanisterEnvironmentArgs {
49+
/// Name or principal of canister to target
50+
/// When using a name an environment must be specified
51+
pub(crate) canister: Canister,
52+
53+
/// Name of the target environment
54+
#[arg(long)]
55+
pub(crate) environment: Option<Environment>,
56+
}
57+
4258
#[derive(Args, Debug)]
4359
pub(crate) struct CanisterCommandArgs {
44-
/// Name of canister to target
60+
// Note: Could have flattened CanisterEnvironmentArg to avoid adding child field
61+
/// Name or principal of canister to target
62+
/// When using a name an environment must be specified
4563
pub(crate) canister: Canister,
4664

65+
/// Name of the network to target
4766
#[arg(long)]
4867
pub(crate) network: Option<Network>,
4968

69+
/// Name of the target environment
5070
#[arg(long)]
5171
pub(crate) environment: Option<Environment>,
5272

73+
/// The identity to use for this request
5374
#[command(flatten)]
5475
pub(crate) identity: IdentityOpt,
5576
}
5677

78+
impl CanisterEnvironmentArgs {
79+
pub async fn get_cid_for_environment(
80+
&self,
81+
ctx: &Context,
82+
) -> Result<Principal, ArgValidationError> {
83+
let arg_canister = self.canister.clone();
84+
let arg_environment = self.environment.clone().unwrap_or_default();
85+
let environment_name = arg_environment.name();
86+
87+
let principal = match arg_canister {
88+
Canister::Name(canister_name) => {
89+
ctx.get_canister_id_for_env(&canister_name, environment_name)
90+
.await?
91+
}
92+
Canister::Principal(principal) => {
93+
// Make sure a valid environment was requested
94+
let _ = ctx.get_environment(environment_name).await?;
95+
principal
96+
}
97+
};
98+
99+
Ok(principal)
100+
}
101+
}
102+
57103
impl CanisterCommandArgs {
58104
pub async fn get_cid_and_agent(
59105
&self,
@@ -123,6 +169,15 @@ impl From<&str> for Canister {
123169
}
124170
}
125171

172+
impl Display for Canister {
173+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
174+
match self {
175+
Canister::Name(n) => f.write_str(n),
176+
Canister::Principal(principal) => f.write_str(&principal.to_string()),
177+
}
178+
}
179+
}
180+
126181
#[derive(Clone, Debug, PartialEq)]
127182
pub(crate) enum Network {
128183
Name(String),
@@ -176,7 +231,11 @@ impl Display for Environment {
176231
mod tests {
177232
use candid::Principal;
178233

179-
use crate::commands::args::{Canister, Network};
234+
use super::*;
235+
use icp::MockProjectLoader;
236+
use std::sync::Arc;
237+
238+
use crate::{commands::args::Environment, store_id::MockInMemoryIdStore};
180239

181240
#[test]
182241
fn canister_by_name() {
@@ -213,4 +272,49 @@ mod tests {
213272
Network::Url("http://www.example.com".to_string()),
214273
);
215274
}
275+
276+
#[tokio::test]
277+
async fn test_get_cid_for_environment() {
278+
use crate::store_id::{Access as IdAccess, Key};
279+
use candid::Principal;
280+
281+
let ids_store = Arc::new(MockInMemoryIdStore::new());
282+
283+
// Register a canister ID for the dev environment
284+
let canister_id = Principal::from_text("rrkah-fqaaa-aaaaa-aaaaq-cai").unwrap();
285+
ids_store
286+
.register(
287+
&Key {
288+
network: "local".to_string(),
289+
environment: "dev".to_string(),
290+
canister: "backend".to_string(),
291+
},
292+
&canister_id,
293+
)
294+
.unwrap();
295+
296+
let ctx = Context {
297+
project: Arc::new(MockProjectLoader::complex()),
298+
ids: ids_store,
299+
..Context::mocked()
300+
};
301+
302+
let args = CanisterEnvironmentArgs {
303+
canister: Canister::Name("backend".to_string()),
304+
environment: Some(Environment::Name("dev".to_string())),
305+
};
306+
307+
assert!(matches!(args.get_cid_for_environment(&ctx).await, Ok(id) if id == canister_id));
308+
309+
let args = CanisterEnvironmentArgs {
310+
canister: Canister::Name("INVALID".to_string()),
311+
environment: Some(Environment::Name("dev".to_string())),
312+
};
313+
314+
let res = args.get_cid_for_environment(&ctx).await;
315+
assert!(
316+
res.is_err(),
317+
"An invalid canister name should result in an error"
318+
);
319+
}
216320
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,39 @@ pub(crate) mod top_up;
1616

1717
#[derive(Debug, Subcommand)]
1818
pub(crate) enum Command {
19+
/// Make a canister call
1920
Call(call::CallArgs),
21+
22+
/// Create a canister on a network
2023
Create(create::CreateArgs),
24+
25+
/// Delete a canister from a network
2126
Delete(delete::DeleteArgs),
27+
28+
/// Display a canister's information
2229
Info(info::InfoArgs),
30+
31+
/// Install a built WASM to a canister on a network
2332
Install(install::InstallArgs),
33+
34+
/// List the canisters in an environment
2435
List(list::ListArgs),
2536

2637
#[command(subcommand)]
2738
Settings(settings::Command),
2839

40+
/// Show a canister's details
2941
Show(show::ShowArgs),
42+
43+
/// Start a canister on a network
3044
Start(start::StartArgs),
45+
46+
/// Show the status of a canister
3147
Status(status::StatusArgs),
48+
49+
/// Stop a canister on a network
3250
Stop(stop::StopArgs),
51+
52+
/// Top up a canister with cycles
3353
TopUp(top_up::TopUpArgs),
3454
}

crates/icp-cli/src/commands/canister/show.rs

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,26 @@
11
use clap::Args;
22

3-
use crate::{
4-
commands::Context,
5-
options::EnvironmentOpt,
6-
store_id::{Key, LookupError as LookupIdError},
3+
use crate::commands::{
4+
Context,
5+
args::{ArgValidationError, CanisterEnvironmentArgs},
76
};
87

98
#[derive(Debug, Args)]
109
pub(crate) struct ShowArgs {
11-
/// The name of the canister within the current project
12-
pub(crate) name: String,
13-
1410
#[command(flatten)]
15-
environment: EnvironmentOpt,
11+
pub(crate) cmd_args: CanisterEnvironmentArgs,
1612
}
1713

1814
#[derive(Debug, thiserror::Error)]
1915
pub(crate) enum CommandError {
2016
#[error(transparent)]
21-
Project(#[from] icp::LoadError),
22-
23-
#[error("project does not contain an environment named '{name}'")]
24-
EnvironmentNotFound { name: String },
25-
26-
#[error("environment '{environment}' does not include canister '{canister}'")]
27-
EnvironmentCanister {
28-
environment: String,
29-
canister: String,
30-
},
31-
32-
#[error(transparent)]
33-
LookupCanisterId(#[from] LookupIdError),
17+
Shared(#[from] ArgValidationError),
3418
}
3519

3620
pub(crate) async fn exec(ctx: &Context, args: &ShowArgs) -> Result<(), CommandError> {
37-
// Load project
38-
let p = ctx.project.load().await?;
39-
40-
// Load target environment
41-
let env =
42-
p.environments
43-
.get(args.environment.name())
44-
.ok_or(CommandError::EnvironmentNotFound {
45-
name: args.environment.name().to_owned(),
46-
})?;
47-
48-
// Ensure canister is included in the environment
49-
if !env.canisters.contains_key(&args.name) {
50-
return Err(CommandError::EnvironmentCanister {
51-
environment: env.name.to_owned(),
52-
canister: args.name.to_owned(),
53-
});
54-
}
55-
56-
// Lookup the canister id
57-
let cid = ctx.ids.lookup(&Key {
58-
network: env.network.name.to_owned(),
59-
environment: env.name.to_owned(),
60-
canister: args.name.to_owned(),
61-
})?;
21+
let cid = args.cmd_args.get_cid_for_environment(ctx).await?;
6222

63-
println!("{cid} => {}", args.name);
23+
println!("{cid} => {}", args.cmd_args.canister);
6424

6525
// TODO(or.ricon): Show canister details
6626
// Things we might want to show (do we need to sub-command this?)

0 commit comments

Comments
 (0)