Skip to content

Commit 93ca1a5

Browse files
authored
feat: icp canister metadata (#269)
1 parent 5391e11 commit 93ca1a5

File tree

6 files changed

+223
-0
lines changed

6 files changed

+223
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Unreleased
22

3+
* feat: `icp canister metadata <canister> <metadata section>` now fetches metadata sections from specified canisters
34
* fix: Validate explicit canister paths and throw an error if `canister.yaml` is not found
45

56
# v0.1.0-beta.3
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use anyhow::bail;
2+
use clap::Args;
3+
use icp::context::Context;
4+
5+
use crate::{commands::args, operations::misc::fetch_canister_metadata};
6+
7+
#[derive(Debug, Args)]
8+
pub(crate) struct MetadataArgs {
9+
#[command(flatten)]
10+
pub(crate) common: args::CanisterCommandArgs,
11+
12+
/// The name of the metadata section to read
13+
pub(crate) metadata_name: String,
14+
}
15+
16+
pub(crate) async fn exec(ctx: &Context, args: &MetadataArgs) -> Result<(), anyhow::Error> {
17+
let selections = args.common.selections();
18+
19+
// Get the canister principal
20+
let canister_id = ctx
21+
.get_canister_id(
22+
&selections.canister,
23+
&selections.network,
24+
&selections.environment,
25+
)
26+
.await?;
27+
28+
// Get the agent
29+
let agent = ctx
30+
.get_agent(
31+
&selections.identity,
32+
&selections.network,
33+
&selections.environment,
34+
)
35+
.await?;
36+
37+
// Fetch the metadata
38+
let metadata = fetch_canister_metadata(&agent, canister_id, &args.metadata_name).await;
39+
40+
match metadata {
41+
Some(value) => {
42+
ctx.term.write_line(&value)?;
43+
Ok(())
44+
}
45+
None => bail!(
46+
"Metadata section '{}' not found in canister {}",
47+
args.metadata_name,
48+
canister_id
49+
),
50+
}
51+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ pub(crate) mod create;
55
pub(crate) mod delete;
66
pub(crate) mod install;
77
pub(crate) mod list;
8+
pub(crate) mod metadata;
89
pub(crate) mod settings;
910
pub(crate) mod start;
1011
pub(crate) mod status;
@@ -29,6 +30,9 @@ pub(crate) enum Command {
2930
/// List the canisters in an environment
3031
List(list::ListArgs),
3132

33+
/// Read a metadata section from a canister
34+
Metadata(metadata::MetadataArgs),
35+
3236
/// Commands to manage canister settings
3337
#[command(subcommand)]
3438
Settings(settings::Command),

crates/icp-cli/src/main.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,12 @@ async fn main() -> Result<(), Error> {
221221
.await?
222222
}
223223

224+
commands::canister::Command::Metadata(args) => {
225+
commands::canister::metadata::exec(&ctx, &args)
226+
.instrument(trace_span)
227+
.await?
228+
}
229+
224230
commands::canister::Command::Settings(cmd) => match cmd {
225231
commands::canister::settings::Command::Show(args) => {
226232
commands::canister::settings::show::exec(&ctx, &args)
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
use indoc::formatdoc;
2+
use predicates::str::contains;
3+
4+
use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients};
5+
use icp::{fs::write_string, prelude::*};
6+
7+
mod common;
8+
9+
#[tokio::test]
10+
async fn canister_metadata() {
11+
let ctx = TestContext::new();
12+
13+
// Setup project
14+
let project_dir = ctx.create_project_dir("icp");
15+
16+
// Use vendored WASM
17+
let wasm = ctx.make_asset("example_icp_mo.wasm");
18+
19+
// Project manifest
20+
let pm = formatdoc! {r#"
21+
canisters:
22+
- name: my-canister
23+
build:
24+
steps:
25+
- type: script
26+
command: cp {wasm} "$ICP_WASM_OUTPUT_PATH"
27+
28+
{NETWORK_RANDOM_PORT}
29+
{ENVIRONMENT_RANDOM_PORT}
30+
"#};
31+
32+
write_string(
33+
&project_dir.join("icp.yaml"), // path
34+
&pm, // contents
35+
)
36+
.expect("failed to write project manifest");
37+
38+
// Start network
39+
let _g = ctx.start_network_in(&project_dir, "random-network").await;
40+
ctx.ping_until_healthy(&project_dir, "random-network");
41+
42+
// Deploy project
43+
clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
44+
.mint_cycles(10 * TRILLION);
45+
46+
ctx.icp()
47+
.current_dir(&project_dir)
48+
.args([
49+
"deploy",
50+
"--subnet",
51+
common::SUBNET_ID,
52+
"--environment",
53+
"random-environment",
54+
])
55+
.assert()
56+
.success();
57+
58+
// Query metadata - try to read candid:service metadata
59+
ctx.icp()
60+
.current_dir(&project_dir)
61+
.args([
62+
"canister",
63+
"metadata",
64+
"my-canister",
65+
"candid:service",
66+
"--environment",
67+
"random-environment",
68+
])
69+
.assert()
70+
.success();
71+
}
72+
73+
#[tokio::test]
74+
async fn canister_metadata_not_found() {
75+
let ctx = TestContext::new();
76+
77+
// Setup project
78+
let project_dir = ctx.create_project_dir("icp");
79+
80+
// Use vendored WASM
81+
let wasm = ctx.make_asset("example_icp_mo.wasm");
82+
83+
// Project manifest
84+
let pm = formatdoc! {r#"
85+
canisters:
86+
- name: my-canister
87+
build:
88+
steps:
89+
- type: script
90+
command: cp {wasm} "$ICP_WASM_OUTPUT_PATH"
91+
92+
{NETWORK_RANDOM_PORT}
93+
{ENVIRONMENT_RANDOM_PORT}
94+
"#};
95+
96+
write_string(
97+
&project_dir.join("icp.yaml"), // path
98+
&pm, // contents
99+
)
100+
.expect("failed to write project manifest");
101+
102+
// Start network
103+
let _g = ctx.start_network_in(&project_dir, "random-network").await;
104+
ctx.ping_until_healthy(&project_dir, "random-network");
105+
106+
// Deploy project
107+
clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
108+
.mint_cycles(10 * TRILLION);
109+
110+
ctx.icp()
111+
.current_dir(&project_dir)
112+
.args([
113+
"deploy",
114+
"--subnet",
115+
common::SUBNET_ID,
116+
"--environment",
117+
"random-environment",
118+
])
119+
.assert()
120+
.success();
121+
122+
// Query non-existent metadata section - should fail
123+
ctx.icp()
124+
.current_dir(&project_dir)
125+
.args([
126+
"canister",
127+
"metadata",
128+
"my-canister",
129+
"nonexistent-metadata-section",
130+
"--environment",
131+
"random-environment",
132+
])
133+
.assert()
134+
.failure()
135+
.stderr(contains(
136+
"Metadata section 'nonexistent-metadata-section' not found",
137+
));
138+
}

docs/cli-reference.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ This document contains the help content for the `icp` command-line program.
1212
* [`icp canister delete`](#icp-canister-delete)
1313
* [`icp canister install`](#icp-canister-install)
1414
* [`icp canister list`](#icp-canister-list)
15+
* [`icp canister metadata`](#icp-canister-metadata)
1516
* [`icp canister settings`](#icp-canister-settings)
1617
* [`icp canister settings show`](#icp-canister-settings-show)
1718
* [`icp canister settings update`](#icp-canister-settings-update)
@@ -105,6 +106,7 @@ Perform canister operations against a network
105106
* `delete` — Delete a canister from a network
106107
* `install` — Install a built WASM to a canister on a network
107108
* `list` — List the canisters in an environment
109+
* `metadata` — Read a metadata section from a canister
108110
* `settings` — Commands to manage canister settings
109111
* `start` — Start a canister on a network
110112
* `status` — Show the status of canister(s)
@@ -227,6 +229,27 @@ List the canisters in an environment
227229

228230

229231

232+
## `icp canister metadata`
233+
234+
Read a metadata section from a canister
235+
236+
**Usage:** `icp canister metadata [OPTIONS] <CANISTER> <METADATA_NAME>`
237+
238+
###### **Arguments:**
239+
240+
* `<CANISTER>` — Name or principal of canister to target When using a name an environment must be specified
241+
* `<METADATA_NAME>` — The name of the metadata section to read
242+
243+
###### **Options:**
244+
245+
* `--network <NETWORK>` — Name of the network to target, conflicts with environment argument
246+
* `--mainnet` — Shorthand for --network=mainnet
247+
* `-e`, `--environment <ENVIRONMENT>` — Override the environment to connect to. By default, the local environment is used
248+
* `--ic` — Shorthand for --environment=ic
249+
* `--identity <IDENTITY>` — The user identity to run this command as
250+
251+
252+
230253
## `icp canister settings`
231254

232255
Commands to manage canister settings

0 commit comments

Comments
 (0)