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
@@ -1,5 +1,6 @@
# Unreleased

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

# v0.1.0-beta.3
Expand Down
51 changes: 51 additions & 0 deletions crates/icp-cli/src/commands/canister/metadata.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use anyhow::bail;
use clap::Args;
use icp::context::Context;

use crate::{commands::args, operations::misc::fetch_canister_metadata};

#[derive(Debug, Args)]
pub(crate) struct MetadataArgs {
#[command(flatten)]
pub(crate) common: args::CanisterCommandArgs,

/// The name of the metadata section to read
pub(crate) metadata_name: String,
}

pub(crate) async fn exec(ctx: &Context, args: &MetadataArgs) -> Result<(), anyhow::Error> {
let selections = args.common.selections();

// Get the canister principal
let canister_id = ctx
.get_canister_id(
&selections.canister,
&selections.network,
&selections.environment,
)
.await?;

// Get the agent
let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;

// Fetch the metadata
let metadata = fetch_canister_metadata(&agent, canister_id, &args.metadata_name).await;

match metadata {
Some(value) => {
ctx.term.write_line(&value)?;
Ok(())
}
None => bail!(
"Metadata section '{}' not found in canister {}",
args.metadata_name,
canister_id
),
}
}
4 changes: 4 additions & 0 deletions crates/icp-cli/src/commands/canister/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub(crate) mod create;
pub(crate) mod delete;
pub(crate) mod install;
pub(crate) mod list;
pub(crate) mod metadata;
pub(crate) mod settings;
pub(crate) mod start;
pub(crate) mod status;
Expand All @@ -29,6 +30,9 @@ pub(crate) enum Command {
/// List the canisters in an environment
List(list::ListArgs),

/// Read a metadata section from a canister
Metadata(metadata::MetadataArgs),

/// Commands to manage canister settings
#[command(subcommand)]
Settings(settings::Command),
Expand Down
6 changes: 6 additions & 0 deletions crates/icp-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ async fn main() -> Result<(), Error> {
.await?
}

commands::canister::Command::Metadata(args) => {
commands::canister::metadata::exec(&ctx, &args)
.instrument(trace_span)
.await?
}

commands::canister::Command::Settings(cmd) => match cmd {
commands::canister::settings::Command::Show(args) => {
commands::canister::settings::show::exec(&ctx, &args)
Expand Down
138 changes: 138 additions & 0 deletions crates/icp-cli/tests/canister_metadata_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
use indoc::formatdoc;
use predicates::str::contains;

use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients};
use icp::{fs::write_string, prelude::*};

mod common;

#[tokio::test]
async fn canister_metadata() {
let ctx = TestContext::new();

// Setup project
let project_dir = ctx.create_project_dir("icp");

// Use vendored WASM
let wasm = ctx.make_asset("example_icp_mo.wasm");

// Project manifest
let pm = formatdoc! {r#"
canisters:
- name: my-canister
build:
steps:
- type: script
command: cp {wasm} "$ICP_WASM_OUTPUT_PATH"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};

write_string(
&project_dir.join("icp.yaml"), // path
&pm, // contents
)
.expect("failed to write project manifest");

// Start network
let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

// Deploy project
clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
.mint_cycles(10 * TRILLION);

ctx.icp()
.current_dir(&project_dir)
.args([
"deploy",
"--subnet",
common::SUBNET_ID,
"--environment",
"random-environment",
])
.assert()
.success();

// Query metadata - try to read candid:service metadata
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"metadata",
"my-canister",
"candid:service",
"--environment",
"random-environment",
])
.assert()
.success();
}

#[tokio::test]
async fn canister_metadata_not_found() {
let ctx = TestContext::new();

// Setup project
let project_dir = ctx.create_project_dir("icp");

// Use vendored WASM
let wasm = ctx.make_asset("example_icp_mo.wasm");

// Project manifest
let pm = formatdoc! {r#"
canisters:
- name: my-canister
build:
steps:
- type: script
command: cp {wasm} "$ICP_WASM_OUTPUT_PATH"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};

write_string(
&project_dir.join("icp.yaml"), // path
&pm, // contents
)
.expect("failed to write project manifest");

// Start network
let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

// Deploy project
clients::icp(&ctx, &project_dir, Some("random-environment".to_string()))
.mint_cycles(10 * TRILLION);

ctx.icp()
.current_dir(&project_dir)
.args([
"deploy",
"--subnet",
common::SUBNET_ID,
"--environment",
"random-environment",
])
.assert()
.success();

// Query non-existent metadata section - should fail
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"metadata",
"my-canister",
"nonexistent-metadata-section",
"--environment",
"random-environment",
])
.assert()
.failure()
.stderr(contains(
"Metadata section 'nonexistent-metadata-section' not found",
));
}
23 changes: 23 additions & 0 deletions docs/cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This document contains the help content for the `icp` command-line program.
* [`icp canister delete`↴](#icp-canister-delete)
* [`icp canister install`↴](#icp-canister-install)
* [`icp canister list`↴](#icp-canister-list)
* [`icp canister metadata`↴](#icp-canister-metadata)
* [`icp canister settings`↴](#icp-canister-settings)
* [`icp canister settings show`↴](#icp-canister-settings-show)
* [`icp canister settings update`↴](#icp-canister-settings-update)
Expand Down Expand Up @@ -105,6 +106,7 @@ Perform canister operations against a network
* `delete` — Delete a canister from a network
* `install` — Install a built WASM to a canister on a network
* `list` — List the canisters in an environment
* `metadata` — Read a metadata section from a canister
* `settings` — Commands to manage canister settings
* `start` — Start a canister on a network
* `status` — Show the status of canister(s)
Expand Down Expand Up @@ -227,6 +229,27 @@ List the canisters in an environment



## `icp canister metadata`

Read a metadata section from a canister

**Usage:** `icp canister metadata [OPTIONS] <CANISTER> <METADATA_NAME>`

###### **Arguments:**

* `<CANISTER>` — Name or principal of canister to target When using a name an environment must be specified
* `<METADATA_NAME>` — The name of the metadata section to read

###### **Options:**

* `--network <NETWORK>` — Name of the network to target, conflicts with environment argument
* `--mainnet` — Shorthand for --network=mainnet
* `-e`, `--environment <ENVIRONMENT>` — Override the environment to connect to. By default, the local environment is used
* `--ic` — Shorthand for --environment=ic
* `--identity <IDENTITY>` — The user identity to run this command as



## `icp canister settings`

Commands to manage canister settings
Expand Down