From cd14b1f651cdf07e0130032ab37f007ed59f9131 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 13:39:35 -0400 Subject: [PATCH 01/13] refactor: add proxy_management module with create_canister wrapper Extract the proxied management canister create_canister call into a dedicated proxy_management module, keeping raw IC types as the interface. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/src/operations/create.rs | 9 +++---- crates/icp-cli/src/operations/mod.rs | 1 + .../src/operations/proxy_management.rs | 26 +++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) create mode 100644 crates/icp-cli/src/operations/proxy_management.rs diff --git a/crates/icp-cli/src/operations/create.rs b/crates/icp-cli/src/operations/create.rs index 6af1a5e5..e77be871 100644 --- a/crates/icp-cli/src/operations/create.rs +++ b/crates/icp-cli/src/operations/create.rs @@ -19,7 +19,8 @@ use rand::seq::IndexedRandom; use snafu::{OptionExt, ResultExt, Snafu}; use tokio::sync::OnceCell; -use super::proxy::{UpdateOrProxyError, update_or_proxy}; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; #[derive(Debug, Snafu)] pub enum CreateOperationError { @@ -220,13 +221,11 @@ impl CreateOperation { sender_canister_version: None, }; - let (result,): (CanisterIdRecord,) = update_or_proxy( + let result = proxy_management::create_canister( &self.inner.agent, - Principal::management_canister(), - "create_canister", - (args,), Some(proxy), self.inner.cycles, + args, ) .await?; diff --git a/crates/icp-cli/src/operations/mod.rs b/crates/icp-cli/src/operations/mod.rs index 643bdeb2..f1d8ba2b 100644 --- a/crates/icp-cli/src/operations/mod.rs +++ b/crates/icp-cli/src/operations/mod.rs @@ -5,6 +5,7 @@ pub(crate) mod canister_migration; pub(crate) mod create; pub(crate) mod install; pub(crate) mod proxy; +pub(crate) mod proxy_management; pub(crate) mod settings; pub(crate) mod snapshot_transfer; pub(crate) mod sync; diff --git a/crates/icp-cli/src/operations/proxy_management.rs b/crates/icp-cli/src/operations/proxy_management.rs new file mode 100644 index 00000000..d7dd28eb --- /dev/null +++ b/crates/icp-cli/src/operations/proxy_management.rs @@ -0,0 +1,26 @@ +use candid::Principal; +use ic_agent::Agent; +use ic_management_canister_types::{CanisterIdRecord, CreateCanisterArgs}; + +use super::proxy::{UpdateOrProxyError, update_or_proxy}; + +/// Calls `create_canister` on the management canister, optionally routing +/// through a proxy canister. +pub async fn create_canister( + agent: &Agent, + proxy: Option, + cycles: u128, + args: CreateCanisterArgs, +) -> Result { + let (result,): (CanisterIdRecord,) = update_or_proxy( + agent, + Principal::management_canister(), + "create_canister", + (args,), + proxy, + cycles, + ) + .await?; + + Ok(result) +} From 3cef5d41546a47cceacdc928ccd82abd4b78c8b5 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 14:18:45 -0400 Subject: [PATCH 02/13] refactor: route all management canister calls through proxy_management Replace every `ManagementCanister::create` / `ic_utils` builder call with the corresponding `proxy_management::*` wrapper that dispatches via `update_or_proxy`. Each call site now accepts an optional proxy principal, currently passed as `None`. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../icp-cli/src/commands/canister/delete.rs | 9 +- .../icp-cli/src/commands/canister/install.rs | 5 +- crates/icp-cli/src/commands/canister/logs.rs | 24 +- .../src/commands/canister/migrate_id.rs | 71 +++- .../src/commands/canister/settings/sync.rs | 5 +- .../src/commands/canister/settings/update.rs | 146 ++++----- .../src/commands/canister/snapshot/create.rs | 15 +- .../src/commands/canister/snapshot/delete.rs | 7 +- .../src/commands/canister/snapshot/list.rs | 13 +- .../src/commands/canister/snapshot/restore.rs | 15 +- crates/icp-cli/src/commands/canister/start.rs | 9 +- .../icp-cli/src/commands/canister/status.rs | 34 +- crates/icp-cli/src/commands/canister/stop.rs | 9 +- crates/icp-cli/src/commands/deploy.rs | 14 +- .../src/operations/binding_env_vars.rs | 41 ++- crates/icp-cli/src/operations/install.rs | 158 +++++---- .../src/operations/proxy_management.rs | 309 +++++++++++++++++- crates/icp-cli/src/operations/settings.rs | 96 +++--- .../src/operations/snapshot_transfer.rs | 117 ++++--- 19 files changed, 747 insertions(+), 350 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index a82baa54..f02ad6f1 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -1,7 +1,8 @@ use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::{CanisterSelection, Context}; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Delete a canister from a network #[derive(Debug, Args)] @@ -28,11 +29,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to delete canister - mgmt.delete_canister(&cid).await?; + proxy_management::delete_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; // Remove canister ID from the id store if it was referenced by name if let CanisterSelection::Named(canister_name) = &selections.canister { diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 7c80dcc4..9d9020e3 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -3,7 +3,7 @@ use std::io::IsTerminal; use anyhow::{Context as _, anyhow, bail}; use clap::Args; use dialoguer::Confirm; -use ic_utils::interfaces::management_canister::builders::CanisterInstallMode; +use ic_management_canister_types::CanisterInstallMode; use icp::context::{CanisterSelection, Context}; use icp::manifest::InitArgsFormat; use icp::prelude::*; @@ -122,7 +122,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow let canister_display = args.cmd_args.canister.to_string(); let (install_mode, status) = - resolve_install_mode_and_status(&agent, &canister_display, &canister_id, &args.mode) + resolve_install_mode_and_status(&agent, None, &canister_display, &canister_id, &args.mode) .await?; // Candid interface compatibility check for upgrades @@ -156,6 +156,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow install_canister( &agent, + None, &canister_id, &canister_display, &wasm, diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 8af176d9..c5290693 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -2,10 +2,8 @@ use std::io::stdout; use anyhow::{Context as _, anyhow}; use clap::Args; -use ic_utils::interfaces::ManagementCanister; -use ic_utils::interfaces::management_canister::{ - CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs, FetchCanisterLogsResult, -}; +use ic_agent::Agent; +use ic_management_canister_types::{CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs}; use icp::context::Context; use icp::signal::stop_signal; use itertools::Itertools; @@ -13,7 +11,7 @@ use serde::Serialize; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use tokio::select; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Fetch and display canister logs #[derive(Debug, Args)] @@ -103,14 +101,12 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - if args.follow { // Follow mode: continuously fetch and display new logs - follow_logs(args, &mgmt, &canister_id, args.interval).await + follow_logs(args, &agent, &canister_id, args.interval).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(args, &mgmt, &canister_id, build_filter(args)?).await + fetch_and_display_logs(args, &agent, &canister_id, build_filter(args)?).await } } @@ -161,7 +157,7 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er async fn fetch_and_display_logs( args: &LogsArgs, - mgmt: &ManagementCanister<'_>, + agent: &Agent, canister_id: &candid::Principal, filter: Option, ) -> Result<(), anyhow::Error> { @@ -169,8 +165,7 @@ async fn fetch_and_display_logs( canister_id: *canister_id, filter, }; - let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(&fetch_args) + let result = proxy_management::fetch_canister_logs(agent, None, fetch_args) .await .context("Failed to fetch canister logs")?; @@ -207,7 +202,7 @@ const FOLLOW_LOOKBACK_NANOS: u64 = 60 * 60 * 1_000_000_000; // 1 hour async fn follow_logs( args: &LogsArgs, - mgmt: &ManagementCanister<'_>, + agent: &Agent, canister_id: &candid::Principal, interval_seconds: u64, ) -> Result<(), anyhow::Error> { @@ -237,8 +232,7 @@ async fn follow_logs( canister_id: *canister_id, filter, }; - let (result,): (FetchCanisterLogsResult,) = mgmt - .fetch_canister_logs(&fetch_args) + let result = proxy_management::fetch_canister_logs(agent, None, fetch_args) .await .context("Failed to fetch canister logs")?; diff --git a/crates/icp-cli/src/commands/canister/migrate_id.rs b/crates/icp-cli/src/commands/canister/migrate_id.rs index 777cc5d3..81afa248 100644 --- a/crates/icp-cli/src/commands/canister/migrate_id.rs +++ b/crates/icp-cli/src/commands/canister/migrate_id.rs @@ -4,8 +4,9 @@ use std::time::{Duration, Instant}; use anyhow::bail; use clap::Args; use dialoguer::Confirm; -use ic_management_canister_types::CanisterStatusType; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, CanisterStatusType, UpdateSettingsArgs, +}; use icp::context::Context; use icp_canister_interfaces::nns_migration::{MigrationStatus, NNS_MIGRATION_PRINCIPAL}; use indicatif::{ProgressBar, ProgressStyle}; @@ -17,6 +18,7 @@ use crate::operations::canister_migration::{ get_subnet_for_canister, migrate_canister, migration_status, }; use crate::operations::misc::format_timestamp; +use crate::operations::proxy_management; use icp::context::CanisterSelection; /// Minimum cycles required for migration (10T). @@ -105,11 +107,23 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh } } - let mgmt = ManagementCanister::create(&agent); - // Fetch status of both canisters - let (source_status,) = mgmt.canister_status(&source_cid).await?; - let (target_status,) = mgmt.canister_status(&target_cid).await?; + let source_status = proxy_management::canister_status( + &agent, + None, + CanisterIdRecord { + canister_id: source_cid, + }, + ) + .await?; + let target_status = proxy_management::canister_status( + &agent, + None, + CanisterIdRecord { + canister_id: target_cid, + }, + ) + .await?; // Check both are stopped ensure_canister_stopped(source_status.status, &source_name)?; @@ -156,7 +170,14 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh } // Check target canister has no snapshots - let (snapshots,) = mgmt.list_canister_snapshots(&target_cid).await?; + let snapshots = proxy_management::list_canister_snapshots( + &agent, + None, + CanisterIdRecord { + canister_id: target_cid, + }, + ) + .await?; if !snapshots.is_empty() { bail!( "The target canister '{target_name}' ({target_cid}) has {} snapshot(s). \ @@ -188,11 +209,19 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh info!("Adding NNS migration canister as controller of '{source_name}'..."); let mut new_controllers = source_controllers; new_controllers.push(NNS_MIGRATION_PRINCIPAL); - let mut builder = mgmt.update_settings(&source_cid); - for controller in new_controllers { - builder = builder.with_controller(controller); - } - builder.await?; + proxy_management::update_settings( + &agent, + None, + UpdateSettingsArgs { + canister_id: source_cid, + settings: CanisterSettings { + controllers: Some(new_controllers), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; } let target_controllers = target_status.settings.controllers; @@ -200,11 +229,19 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh info!("Adding NNS migration canister as controller of '{target_name}'..."); let mut new_controllers = target_controllers; new_controllers.push(NNS_MIGRATION_PRINCIPAL); - let mut builder = mgmt.update_settings(&target_cid); - for controller in new_controllers { - builder = builder.with_controller(controller); - } - builder.await?; + proxy_management::update_settings( + &agent, + None, + UpdateSettingsArgs { + canister_id: target_cid, + settings: CanisterSettings { + controllers: Some(new_controllers), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; } // Initiate migration diff --git a/crates/icp-cli/src/commands/canister/settings/sync.rs b/crates/icp-cli/src/commands/canister/settings/sync.rs index 06ba3f06..474d2d42 100644 --- a/crates/icp-cli/src/commands/canister/settings/sync.rs +++ b/crates/icp-cli/src/commands/canister/settings/sync.rs @@ -1,6 +1,5 @@ use anyhow::bail; use clap::Args; -use ic_utils::interfaces::ManagementCanister; use icp::context::{CanisterSelection, Context}; use crate::commands::args::CanisterCommandArgs; @@ -37,8 +36,6 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - - crate::operations::settings::sync_settings(&mgmt, &cid, &canister).await?; + crate::operations::settings::sync_settings(&agent, None, &cid, &canister).await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 4c60f534..1f352378 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -1,16 +1,20 @@ use anyhow::bail; +use candid::Nat; use clap::{ArgAction, Args}; use dialoguer::Confirm; use ic_agent::Identity; use ic_agent::export::Principal; -use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, CanisterStatusResult, EnvironmentVariable, LogVisibility, + UpdateSettingsArgs, +}; use icp::ProjectLoadError; use icp::context::{CanisterSelection, Context}; use icp::parsers::{CyclesAmount, DurationAmount, MemoryAmount}; use std::collections::{HashMap, HashSet}; use tracing::warn; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; #[derive(Clone, Debug, Default, Args)] pub(crate) struct ControllerOpt { @@ -164,12 +168,12 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: <_>::default() }; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - let mut current_status: Option = None; if require_current_settings(args) { - current_status = Some(mgmt.canister_status(&cid).await?.0); + current_status = Some( + proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) + .await?, + ); } // TODO(VZ): Ask for consent if the freezing threshold is too long or too short. @@ -212,81 +216,77 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: get_environment_variables(environment_variables_opt, current_status.as_ref()); } - // Update settings. - let mut update = mgmt.update_settings(&cid); - if let Some(controllers) = controllers { - for controller in controllers { - update = update.with_controller(controller); - } - } - if let Some(compute_allocation) = args.compute_allocation { - if configured_settings.compute_allocation.is_some() { - warn!( - "Compute allocation is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_compute_allocation(compute_allocation); - } - if let Some(memory_allocation) = &args.memory_allocation { - if configured_settings.memory_allocation.is_some() { - warn!( - "Memory allocation is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_memory_allocation(memory_allocation.get()); + // Build settings with warnings for configured values + if args.compute_allocation.is_some() && configured_settings.compute_allocation.is_some() { + warn!( + "Compute allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(freezing_threshold) = &args.freezing_threshold { - if configured_settings.freezing_threshold.is_some() { - warn!( - "Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_freezing_threshold(freezing_threshold.get()); + if args.memory_allocation.is_some() && configured_settings.memory_allocation.is_some() { + warn!( + "Memory allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(reserved_cycles_limit) = &args.reserved_cycles_limit { - if configured_settings.reserved_cycles_limit.is_some() { - warn!( - "Reserved cycles limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_reserved_cycles_limit(reserved_cycles_limit.get()); + if args.freezing_threshold.is_some() && configured_settings.freezing_threshold.is_some() { + warn!( + "Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(wasm_memory_limit) = &args.wasm_memory_limit { - if configured_settings.wasm_memory_limit.is_some() { - warn!( - "Wasm memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_wasm_memory_limit(wasm_memory_limit.get()); + if args.reserved_cycles_limit.is_some() && configured_settings.reserved_cycles_limit.is_some() { + warn!( + "Reserved cycles limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(wasm_memory_threshold) = &args.wasm_memory_threshold { - if configured_settings.wasm_memory_threshold.is_some() { - warn!( - "Wasm memory threshold is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_wasm_memory_threshold(wasm_memory_threshold.get()); + if args.wasm_memory_limit.is_some() && configured_settings.wasm_memory_limit.is_some() { + warn!( + "Wasm memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(log_memory_limit) = &args.log_memory_limit { - if configured_settings.log_memory_limit.is_some() { - warn!( - "Log memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_log_memory_limit(log_memory_limit.get()); + if args.wasm_memory_threshold.is_some() && configured_settings.wasm_memory_threshold.is_some() { + warn!( + "Wasm memory threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(log_visibility) = log_visibility { - if configured_settings.log_visibility.is_some() { - warn!( - "Log visibility is already set in icp.yaml; this new value will be overridden on next settings sync" - ); - } - update = update.with_log_visibility(log_visibility); + if args.log_memory_limit.is_some() && configured_settings.log_memory_limit.is_some() { + warn!( + "Log memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - if let Some(environment_variables) = environment_variables { - update = update.with_environment_variables(environment_variables); + if log_visibility.is_some() && configured_settings.log_visibility.is_some() { + warn!( + "Log visibility is already set in icp.yaml; this new value will be overridden on next settings sync" + ); } - update.await?; + + let settings = CanisterSettings { + controllers, + compute_allocation: args.compute_allocation.map(|v| Nat::from(v as u64)), + memory_allocation: args.memory_allocation.as_ref().map(|m| Nat::from(m.get())), + freezing_threshold: args.freezing_threshold.as_ref().map(|d| Nat::from(d.get())), + reserved_cycles_limit: args + .reserved_cycles_limit + .as_ref() + .map(|r| Nat::from(r.get())), + wasm_memory_limit: args.wasm_memory_limit.as_ref().map(|m| Nat::from(m.get())), + wasm_memory_threshold: args + .wasm_memory_threshold + .as_ref() + .map(|m| Nat::from(m.get())), + log_memory_limit: args.log_memory_limit.as_ref().map(|m| Nat::from(m.get())), + log_visibility, + environment_variables, + }; + + proxy_management::update_settings( + &agent, + None, + UpdateSettingsArgs { + canister_id: cid, + settings, + sender_canister_version: None, + }, + ) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/snapshot/create.rs b/crates/icp-cli/src/commands/canister/snapshot/create.rs index b93bbc2d..7c4e2241 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/create.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/create.rs @@ -3,13 +3,14 @@ use std::io::stdout; use anyhow::bail; use byte_unit::{Byte, UnitType}; use clap::Args; -use ic_management_canister_types::{CanisterStatusType, TakeCanisterSnapshotArgs}; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusType, TakeCanisterSnapshotArgs, +}; use icp::context::Context; use serde::Serialize; use super::SnapshotId; -use crate::{commands::args, operations::misc::format_timestamp}; +use crate::{commands::args, operations::misc::format_timestamp, operations::proxy_management}; /// Create a snapshot of a canister's state #[derive(Debug, Args)] @@ -49,11 +50,11 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: ) .await?; - let mgmt = ManagementCanister::create(&agent); - // Check canister status - must be stopped to create a snapshot let name = &args.cmd_args.canister; - let (status,) = mgmt.canister_status(&cid).await?; + let status = + proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -73,7 +74,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: sender_canister_version: None, }; - let (snapshot,) = mgmt.take_canister_snapshot(&take_args).await?; + let snapshot = proxy_management::take_canister_snapshot(&agent, None, take_args).await?; if args.json { serde_json::to_writer( stdout(), diff --git a/crates/icp-cli/src/commands/canister/snapshot/delete.rs b/crates/icp-cli/src/commands/canister/snapshot/delete.rs index b11d4f54..07adfa38 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/delete.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/delete.rs @@ -1,11 +1,10 @@ use clap::Args; use ic_management_canister_types::DeleteCanisterSnapshotArgs; -use ic_utils::interfaces::ManagementCanister; use icp::context::Context; use tracing::info; use super::SnapshotId; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Delete a canister snapshot #[derive(Debug, Args)] @@ -35,14 +34,12 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: ) .await?; - let mgmt = ManagementCanister::create(&agent); - let delete_args = DeleteCanisterSnapshotArgs { canister_id: cid, snapshot_id: args.snapshot_id.0.clone(), }; - mgmt.delete_canister_snapshot(&delete_args).await?; + proxy_management::delete_canister_snapshot(&agent, None, delete_args).await?; let name = &args.cmd_args.canister; info!( diff --git a/crates/icp-cli/src/commands/canister/snapshot/list.rs b/crates/icp-cli/src/commands/canister/snapshot/list.rs index 4f76ca56..7daa71be 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/list.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/list.rs @@ -2,12 +2,12 @@ use std::io::stdout; use byte_unit::{Byte, UnitType}; use clap::Args; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; use itertools::Itertools; use serde::Serialize; -use crate::{commands::args, operations::misc::format_timestamp}; +use crate::{commands::args, operations::misc::format_timestamp, operations::proxy_management}; /// List all snapshots for a canister #[derive(Debug, Args)] @@ -42,9 +42,12 @@ pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::E ) .await?; - let mgmt = ManagementCanister::create(&agent); - - let (snapshots,) = mgmt.list_canister_snapshots(&cid).await?; + let snapshots = proxy_management::list_canister_snapshots( + &agent, + None, + CanisterIdRecord { canister_id: cid }, + ) + .await?; let name = &args.cmd_args.canister; if args.json { diff --git a/crates/icp-cli/src/commands/canister/snapshot/restore.rs b/crates/icp-cli/src/commands/canister/snapshot/restore.rs index 22fdec44..db6b51fd 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/restore.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/restore.rs @@ -1,12 +1,13 @@ use anyhow::bail; use clap::Args; -use ic_management_canister_types::{CanisterStatusType, LoadCanisterSnapshotArgs}; -use ic_utils::interfaces::ManagementCanister; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusType, LoadCanisterSnapshotArgs, +}; use icp::context::Context; use tracing::info; use super::SnapshotId; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Restore a canister from a snapshot #[derive(Debug, Args)] @@ -36,11 +37,11 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow ) .await?; - let mgmt = ManagementCanister::create(&agent); - // Check canister status - must be stopped to restore a snapshot let name = &args.cmd_args.canister; - let (status,) = mgmt.canister_status(&cid).await?; + let status = + proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -59,7 +60,7 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow sender_canister_version: None, }; - mgmt.load_canister_snapshot(&load_args).await?; + proxy_management::load_canister_snapshot(&agent, None, load_args).await?; info!( "Restored canister {name} ({cid}) from snapshot {id}", diff --git a/crates/icp-cli/src/commands/canister/start.rs b/crates/icp-cli/src/commands/canister/start.rs index 72d44070..98278f06 100644 --- a/crates/icp-cli/src/commands/canister/start.rs +++ b/crates/icp-cli/src/commands/canister/start.rs @@ -1,7 +1,8 @@ use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Start a canister on a network #[derive(Debug, Args)] @@ -27,11 +28,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to start canister - mgmt.start_canister(&cid).await?; + proxy_management::start_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index ca46d4c0..ebce9de4 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -1,7 +1,9 @@ use anyhow::{anyhow, bail}; use clap::Args; use ic_agent::{Agent, AgentError, export::Principal}; -use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusResult, EnvironmentVariable, LogVisibility, +}; use icp::{ context::{CanisterSelection, Context, EnvironmentSelection, NetworkSelection}, identity::IdentitySelection, @@ -10,7 +12,11 @@ use serde::Serialize; use std::fmt::Write; use tracing::debug; -use crate::{commands::args, options}; +use crate::{ + commands::args, + operations::{proxy::UpdateOrProxyError, proxy_management}, + options, +}; /// Error code returned by the replica if the target canister is not found const E_CANISTER_NOT_FOUND: &str = "IC0301"; @@ -203,9 +209,6 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - for (i, (maybe_name, cid)) in cids.iter().enumerate() { let output = match args.options.public { true => { @@ -222,8 +225,14 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: } false => { // Retrieve canister status from management canister - match mgmt.canister_status(cid).await { - Ok((result,)) => { + match proxy_management::canister_status( + &agent, + None, + CanisterIdRecord { canister_id: *cid }, + ) + .await + { + Ok(result) => { let status = SerializableCanisterStatusResult::from( cid.to_owned(), maybe_name.clone(), @@ -237,17 +246,18 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: .expect("Failed to build canister status output"), } } - Err(AgentError::UncertifiedReject { - reject, - operation: _, + Err(UpdateOrProxyError::DirectUpdateCall { + source: + AgentError::UncertifiedReject { + reject, + operation: _, + }, }) => { if reject.error_code.as_deref() == Some(E_CANISTER_NOT_FOUND) { - // The canister does not exist bail!("Canister {cid} was not found."); } if reject.error_code.as_deref() != Some(E_NOT_A_CONTROLLER) { - // We don't know this error code bail!( "Error looking up canister {cid}: {:?} - {}", reject.error_code, diff --git a/crates/icp-cli/src/commands/canister/stop.rs b/crates/icp-cli/src/commands/canister/stop.rs index b8822a99..c328ada8 100644 --- a/crates/icp-cli/src/commands/canister/stop.rs +++ b/crates/icp-cli/src/commands/canister/stop.rs @@ -1,7 +1,8 @@ use clap::Args; +use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; -use crate::commands::args; +use crate::{commands::args, operations::proxy_management}; /// Stop a canister on a network #[derive(Debug, Args)] @@ -27,11 +28,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), anyhow::E ) .await?; - // Management Interface - let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); - - // Instruct management canister to stop canister - mgmt.stop_canister(&cid).await?; + proxy_management::stop_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index 75275afa..d31f3915 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -217,6 +217,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: set_binding_env_vars_many( agent.clone(), + None, &env.name, target_canisters.clone(), canister_list, @@ -225,7 +226,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .await .map_err(|e| anyhow!(e))?; - sync_settings_many(agent.clone(), target_canisters, ctx.debug) + sync_settings_many(agent.clone(), None, target_canisters, ctx.debug) .await .map_err(|e| anyhow!(e))?; @@ -244,7 +245,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .map_err(|e| anyhow!(e))?; let (mode, status) = - resolve_install_mode_and_status(&agent, name, &cid, &args.mode).await?; + resolve_install_mode_and_status(&agent, None, name, &cid, &args.mode).await?; let env = ctx.get_environment(&environment_selection).await?; let (_canister_path, canister_info) = @@ -277,7 +278,14 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: info!("Installing canisters:"); - install_many(agent.clone(), canisters, ctx.artifacts.clone(), ctx.debug).await?; + install_many( + agent.clone(), + None, + canisters, + ctx.artifacts.clone(), + ctx.debug, + ) + .await?; // Sync the selected canisters diff --git a/crates/icp-cli/src/operations/binding_env_vars.rs b/crates/icp-cli/src/operations/binding_env_vars.rs index 668a17ee..88eff87f 100644 --- a/crates/icp-cli/src/operations/binding_env_vars.rs +++ b/crates/icp-cli/src/operations/binding_env_vars.rs @@ -1,16 +1,17 @@ use std::collections::{BTreeMap, HashSet}; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError, export::Principal}; -use ic_utils::interfaces::{ - ManagementCanister, management_canister::builders::EnvironmentVariable, -}; +use ic_agent::{Agent, export::Principal}; +use ic_management_canister_types::{CanisterSettings, EnvironmentVariable, UpdateSettingsArgs}; use icp::Canister; use snafu::Snafu; use tracing::error; use crate::progress::{ProgressManager, ProgressManagerSettings}; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; + #[derive(Debug, Snafu)] pub enum BindingEnvVarsOperationError { #[snafu(display("Could not find canister id(s) for {} in environment '{environment}'. Make sure they are created first", canister_names.join(", ")))] @@ -19,8 +20,8 @@ pub enum BindingEnvVarsOperationError { canister_names: Vec, }, - #[snafu(display("agent error: {source}"))] - Agent { source: AgentError }, + #[snafu(transparent)] + UpdateOrProxy { source: UpdateOrProxyError }, } #[derive(Debug, Snafu)] @@ -37,7 +38,8 @@ struct BindingEnvVarsFailure { } pub(crate) async fn set_env_vars_for_canister( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, canister_id: &Principal, canister_info: &Canister, binding_vars: &[(String, String)], @@ -57,10 +59,20 @@ pub(crate) async fn set_env_vars_for_canister( .into_iter() .map(|(name, value)| EnvironmentVariable { name, value }) .collect::>(); - mgmt.update_settings(canister_id) - .with_environment_variables(environment_variables) - .await - .map_err(|source| BindingEnvVarsOperationError::Agent { source })?; + + proxy_management::update_settings( + agent, + proxy, + UpdateSettingsArgs { + canister_id: *canister_id, + settings: CanisterSettings { + environment_variables: Some(environment_variables), + ..Default::default() + }, + sender_canister_version: None, + }, + ) + .await?; Ok(()) } @@ -68,13 +80,12 @@ pub(crate) async fn set_env_vars_for_canister( /// Orchestrates setting environment variables for multiple canisters with progress tracking pub(crate) async fn set_binding_env_vars_many( agent: Agent, + proxy: Option, environment_name: &str, target_canisters: Vec<(Principal, Canister)>, canister_list: BTreeMap, debug: bool, ) -> Result<(), SetBindingEnvVarsManyError> { - let mgmt = ManagementCanister::create(&agent); - // Check that all the canisters in this environment have an id // We need to have all the ids to generate environment variables // for the bindings @@ -118,13 +129,13 @@ pub(crate) async fn set_binding_env_vars_many( let canister_name = info.name.clone(); let settings_fn = { - let mgmt = mgmt.clone(); + let agent = agent.clone(); let pb = pb.clone(); let binding_vars = binding_vars.clone(); async move { pb.set_message("Updating environment variables..."); - set_env_vars_for_canister(&mgmt, &cid, &info, &binding_vars).await + set_env_vars_for_canister(&agent, proxy, &cid, &info, &binding_vars).await } }; diff --git a/crates/icp-cli/src/operations/install.rs b/crates/icp-cli/src/operations/install.rs index f66d80ee..7c69c452 100644 --- a/crates/icp-cli/src/operations/install.rs +++ b/crates/icp-cli/src/operations/install.rs @@ -1,10 +1,9 @@ use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError, export::Principal}; +use ic_agent::{Agent, export::Principal}; use ic_management_canister_types::{ - CanisterId, CanisterStatusType, ChunkHash, UpgradeFlags, UploadChunkArgs, WasmMemoryPersistence, -}; -use ic_utils::interfaces::{ - ManagementCanister, management_canister::builders::CanisterInstallMode, + CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterStatusType, ChunkHash, + ClearChunkStoreArgs, InstallChunkedCodeArgs, InstallCodeArgs, UpgradeFlags, UploadChunkArgs, + WasmMemoryPersistence, }; use sha2::{Digest, Sha256}; use snafu::{ResultExt, Snafu}; @@ -14,26 +13,28 @@ use tracing::{debug, error, warn}; use crate::progress::{ProgressManager, ProgressManagerSettings}; use super::misc::fetch_canister_metadata; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; #[derive(Debug, Snafu)] pub enum InstallOperationError { #[snafu(display("Could not find build artifact for canister '{canister_name}'"))] ArtifactNotFound { canister_name: String }, - #[snafu(display("agent error: {source}"))] - Agent { source: AgentError }, - #[snafu(display("Failed to stop canister '{canister_name}' before upgrade"))] StopCanister { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, }, #[snafu(display("Failed to start canister '{canister_name}' after upgrade"))] StartCanister { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, }, + + #[snafu(transparent)] + UpdateOrProxy { source: UpdateOrProxyError }, } #[derive(Debug, Snafu)] @@ -54,24 +55,30 @@ struct InstallFailure { /// determine whether the canister already has code installed. pub(crate) async fn resolve_install_mode_and_status( agent: &Agent, + proxy: Option, canister_name: &str, canister_id: &Principal, mode: &str, ) -> Result<(CanisterInstallMode, CanisterStatusType), ResolveInstallModeError> { - let mgmt = ManagementCanister::create(agent); - let (status,) = mgmt - .canister_status(canister_id) - .await - .context(ResolveInstallModeSnafu { canister_name })?; + let status = proxy_management::canister_status( + agent, + proxy, + CanisterIdRecord { + canister_id: CanisterId::from(*canister_id), + }, + ) + .await + .context(ResolveInstallModeSnafu { canister_name })?; + let canister_status = status.status; match mode { "auto" => Ok(if status.module_hash.is_some() { - (CanisterInstallMode::Upgrade(None), status.status) + (CanisterInstallMode::Upgrade(None), canister_status) } else { - (CanisterInstallMode::Install, status.status) + (CanisterInstallMode::Install, canister_status) }), - "install" => Ok((CanisterInstallMode::Install, status.status)), - "reinstall" => Ok((CanisterInstallMode::Reinstall, status.status)), - "upgrade" => Ok((CanisterInstallMode::Upgrade(None), status.status)), + "install" => Ok((CanisterInstallMode::Install, canister_status)), + "reinstall" => Ok((CanisterInstallMode::Reinstall, canister_status)), + "upgrade" => Ok((CanisterInstallMode::Upgrade(None), canister_status)), _ => panic!("invalid install mode: {mode}"), } } @@ -80,11 +87,12 @@ pub(crate) async fn resolve_install_mode_and_status( #[snafu(display("Failed to resolve install mode for canister {canister_name}"))] pub(crate) struct ResolveInstallModeError { canister_name: String, - source: AgentError, + source: UpdateOrProxyError, } pub(crate) async fn install_canister( agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, wasm: &[u8], @@ -118,6 +126,7 @@ pub(crate) async fn install_canister( do_install_operation( agent, + proxy, canister_id, canister_name, wasm, @@ -130,6 +139,7 @@ pub(crate) async fn install_canister( async fn do_install_operation( agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, wasm: &[u8], @@ -137,8 +147,6 @@ async fn do_install_operation( status: CanisterStatusType, init_args: Option<&[u8]>, ) -> Result<(), InstallOperationError> { - let mgmt = ManagementCanister::create(agent); - // Threshold for chunked installation: 2 MB // Raw install_code messages are limited to 2 MiB const CHUNK_THRESHOLD: usize = 2 * 1024 * 1024; @@ -153,30 +161,40 @@ async fn do_install_operation( let init_args_len = init_args.map_or(0, |args| args.len()); let total_install_size = wasm.len() + init_args_len + ENCODING_OVERHEAD; + let cid = CanisterId::from(*canister_id); + if total_install_size <= CHUNK_THRESHOLD { // Small wasm: use regular install_code debug!("Installing wasm for {canister_name} using install_code"); - let mut builder = mgmt.install_code(canister_id, wasm).with_mode(mode); - - if let Some(args) = init_args { - builder = builder.with_raw_arg(args.into()); - } + let install_args = InstallCodeArgs { + mode, + canister_id: cid, + wasm_module: wasm.to_vec(), + arg: init_args.map(|a| a.to_vec()).unwrap_or_default(), + sender_canister_version: None, + }; - stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async { - builder - .await - .map_err(|source| InstallOperationError::Agent { source }) - }) + stop_and_start_if_upgrade( + agent, + proxy, + canister_id, + canister_name, + mode, + status, + async { + proxy_management::install_code(agent, proxy, install_args).await?; + Ok(()) + }, + ) .await?; } else { // Large wasm: use chunked installation debug!("Installing wasm for {canister_name} using chunked installation"); // Clear any existing chunks to ensure a clean state - mgmt.clear_chunk_store(canister_id) - .await - .map_err(|source| InstallOperationError::Agent { source })?; + proxy_management::clear_chunk_store(agent, proxy, ClearChunkStoreArgs { canister_id: cid }) + .await?; // Split wasm into chunks and upload them let chunks: Vec<&[u8]> = wasm.chunks(CHUNK_SIZE).collect(); @@ -191,14 +209,11 @@ async fn do_install_operation( ); let upload_args = UploadChunkArgs { - canister_id: CanisterId::from(*canister_id), + canister_id: cid, chunk: chunk.to_vec(), }; - let (chunk_hash,) = mgmt - .upload_chunk(canister_id, &upload_args) - .await - .map_err(|source| InstallOperationError::Agent { source })?; + let chunk_hash = proxy_management::upload_chunk(agent, proxy, upload_args).await?; chunk_hashes.push(chunk_hash); } @@ -210,29 +225,38 @@ async fn do_install_operation( debug!("Installing chunked code with {} chunks", chunk_hashes.len()); - // Build and execute install_chunked_code - let mut builder = mgmt - .install_chunked_code(canister_id, &wasm_module_hash) - .with_chunk_hashes(chunk_hashes) - .with_install_mode(mode); - - if let Some(args) = init_args { - builder = builder.with_raw_arg(args.to_vec()); - } + let chunked_args = InstallChunkedCodeArgs { + mode, + target_canister: cid, + store_canister: None, + chunk_hashes_list: chunk_hashes, + wasm_module_hash, + arg: init_args.map(|a| a.to_vec()).unwrap_or_default(), + sender_canister_version: None, + }; - let install_res = - stop_and_start_if_upgrade(&mgmt, canister_id, canister_name, mode, status, async { - builder - .await - .map_err(|source| InstallOperationError::Agent { source }) - }) - .await; + let install_res = stop_and_start_if_upgrade( + agent, + proxy, + canister_id, + canister_name, + mode, + status, + async { + proxy_management::install_chunked_code(agent, proxy, chunked_args).await?; + Ok(()) + }, + ) + .await; // Clear chunk store after successful installation to free up storage - let clear_res = mgmt - .clear_chunk_store(canister_id) - .await - .map_err(|source| InstallOperationError::Agent { source }); + let clear_res = proxy_management::clear_chunk_store( + agent, + proxy, + ClearChunkStoreArgs { canister_id: cid }, + ) + .await + .map_err(InstallOperationError::from); if let Err(clear_error) = clear_res { if let Err(install_error) = install_res { @@ -249,7 +273,8 @@ async fn do_install_operation( } async fn stop_and_start_if_upgrade( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, canister_id: &Principal, canister_name: &str, mode: CanisterInstallMode, @@ -260,9 +285,12 @@ async fn stop_and_start_if_upgrade( mode, CanisterInstallMode::Upgrade(_) | CanisterInstallMode::Reinstall ) && matches!(status, CanisterStatusType::Running); + let cid_record = CanisterIdRecord { + canister_id: CanisterId::from(*canister_id), + }; // Stop the canister before proceeding if should_guard { - mgmt.stop_canister(canister_id) + proxy_management::stop_canister(agent, proxy, cid_record.clone()) .await .context(StopCanisterSnafu { canister_name })?; } @@ -270,7 +298,7 @@ async fn stop_and_start_if_upgrade( let install_result = f.await; // Restart the canister whether or not the installation succeeded if should_guard { - let start_result = mgmt.start_canister(canister_id).await; + let start_result = proxy_management::start_canister(agent, proxy, cid_record).await; if let Err(start_error) = start_result { // If both install and start failed, report the install error since it's more likely to be the root cause if let Err(install_error) = install_result { @@ -288,6 +316,7 @@ async fn stop_and_start_if_upgrade( /// Installs code to multiple canisters and displays progress bars. pub(crate) async fn install_many( agent: Agent, + proxy: Option, canisters: impl IntoIterator< Item = ( String, @@ -322,6 +351,7 @@ pub(crate) async fn install_many( install_canister( &agent, + proxy, &cid, &name, &wasm, diff --git a/crates/icp-cli/src/operations/proxy_management.rs b/crates/icp-cli/src/operations/proxy_management.rs index d7dd28eb..6b8f7a5d 100644 --- a/crates/icp-cli/src/operations/proxy_management.rs +++ b/crates/icp-cli/src/operations/proxy_management.rs @@ -1,11 +1,19 @@ use candid::Principal; use ic_agent::Agent; -use ic_management_canister_types::{CanisterIdRecord, CreateCanisterArgs}; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterStatusResult, ClearChunkStoreArgs, CreateCanisterArgs, + DeleteCanisterArgs, DeleteCanisterSnapshotArgs, FetchCanisterLogsArgs, FetchCanisterLogsResult, + InstallChunkedCodeArgs, InstallCodeArgs, ListCanisterSnapshotsArgs, + ListCanisterSnapshotsResult, LoadCanisterSnapshotArgs, ReadCanisterSnapshotDataArgs, + ReadCanisterSnapshotDataResult, ReadCanisterSnapshotMetadataArgs, + ReadCanisterSnapshotMetadataResult, StartCanisterArgs, StopCanisterArgs, + TakeCanisterSnapshotArgs, TakeCanisterSnapshotResult, UpdateSettingsArgs, + UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, + UploadCanisterSnapshotMetadataResult, UploadChunkArgs, UploadChunkResult, +}; use super::proxy::{UpdateOrProxyError, update_or_proxy}; -/// Calls `create_canister` on the management canister, optionally routing -/// through a proxy canister. pub async fn create_canister( agent: &Agent, proxy: Option, @@ -21,6 +29,301 @@ pub async fn create_canister( cycles, ) .await?; + Ok(result) +} +pub async fn canister_status( + agent: &Agent, + proxy: Option, + args: CanisterIdRecord, +) -> Result { + let (result,): (CanisterStatusResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "canister_status", + (args,), + proxy, + 0, + ) + .await?; Ok(result) } + +pub async fn stop_canister( + agent: &Agent, + proxy: Option, + args: StopCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "stop_canister", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn start_canister( + agent: &Agent, + proxy: Option, + args: StartCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "start_canister", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn delete_canister( + agent: &Agent, + proxy: Option, + args: DeleteCanisterArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "delete_canister", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn update_settings( + agent: &Agent, + proxy: Option, + args: UpdateSettingsArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "update_settings", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn install_code( + agent: &Agent, + proxy: Option, + args: InstallCodeArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "install_code", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn install_chunked_code( + agent: &Agent, + proxy: Option, + args: InstallChunkedCodeArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "install_chunked_code", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn upload_chunk( + agent: &Agent, + proxy: Option, + args: UploadChunkArgs, +) -> Result { + let (result,): (UploadChunkResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "upload_chunk", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn clear_chunk_store( + agent: &Agent, + proxy: Option, + args: ClearChunkStoreArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "clear_chunk_store", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn fetch_canister_logs( + agent: &Agent, + proxy: Option, + args: FetchCanisterLogsArgs, +) -> Result { + let (result,): (FetchCanisterLogsResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "fetch_canister_logs", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn take_canister_snapshot( + agent: &Agent, + proxy: Option, + args: TakeCanisterSnapshotArgs, +) -> Result { + let (result,): (TakeCanisterSnapshotResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "take_canister_snapshot", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn load_canister_snapshot( + agent: &Agent, + proxy: Option, + args: LoadCanisterSnapshotArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "load_canister_snapshot", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn list_canister_snapshots( + agent: &Agent, + proxy: Option, + args: ListCanisterSnapshotsArgs, +) -> Result { + let (result,): (ListCanisterSnapshotsResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "list_canister_snapshots", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn delete_canister_snapshot( + agent: &Agent, + proxy: Option, + args: DeleteCanisterSnapshotArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "delete_canister_snapshot", + (args,), + proxy, + 0, + ) + .await +} + +pub async fn read_canister_snapshot_metadata( + agent: &Agent, + proxy: Option, + args: ReadCanisterSnapshotMetadataArgs, +) -> Result { + let (result,): (ReadCanisterSnapshotMetadataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "read_canister_snapshot_metadata", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn upload_canister_snapshot_metadata( + agent: &Agent, + proxy: Option, + args: UploadCanisterSnapshotMetadataArgs, +) -> Result { + let (result,): (UploadCanisterSnapshotMetadataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "upload_canister_snapshot_metadata", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn read_canister_snapshot_data( + agent: &Agent, + proxy: Option, + args: ReadCanisterSnapshotDataArgs, +) -> Result { + let (result,): (ReadCanisterSnapshotDataResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "read_canister_snapshot_data", + (args,), + proxy, + 0, + ) + .await?; + Ok(result) +} + +pub async fn upload_canister_snapshot_data( + agent: &Agent, + proxy: Option, + args: UploadCanisterSnapshotDataArgs, +) -> Result<(), UpdateOrProxyError> { + update_or_proxy::<_, ()>( + agent, + Principal::management_canister(), + "upload_canister_snapshot_data", + (args,), + proxy, + 0, + ) + .await +} diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs index 1e7b1bc5..7de4999c 100644 --- a/crates/icp-cli/src/operations/settings.rs +++ b/crates/icp-cli/src/operations/settings.rs @@ -1,10 +1,11 @@ use std::collections::{HashMap, HashSet}; -use candid::Principal; +use candid::{Nat, Principal}; use futures::{StreamExt, stream::FuturesOrdered}; -use ic_agent::{Agent, AgentError}; -use ic_management_canister_types::{EnvironmentVariable, LogVisibility}; -use ic_utils::interfaces::ManagementCanister; +use ic_agent::Agent; +use ic_management_canister_types::{ + CanisterIdRecord, CanisterSettings, EnvironmentVariable, LogVisibility, UpdateSettingsArgs, +}; use icp::{Canister, canister::Settings}; use itertools::Itertools; use num_traits::ToPrimitive; @@ -13,19 +14,20 @@ use tracing::error; use crate::progress::{ProgressManager, ProgressManagerSettings}; +use super::proxy::UpdateOrProxyError; +use super::proxy_management; + #[derive(Debug, Snafu)] #[allow(clippy::enum_variant_names)] pub(crate) enum SyncSettingsOperationError { #[snafu(display("failed to fetch current canister settings for canister {canister}"))] FetchCurrentSettings { - source: AgentError, + source: UpdateOrProxyError, canister: Principal, }, - #[snafu(display("invalid canister settings in manifest for canister {name}"))] - ValidateSettings { source: AgentError, name: String }, #[snafu(display("failed to update canister settings for canister {canister}"))] UpdateSettings { - source: AgentError, + source: UpdateOrProxyError, canister: Principal, }, } @@ -67,14 +69,15 @@ fn environment_variables_eq(a: &[EnvironmentVariable], b: &[EnvironmentVariable] } pub(crate) async fn sync_settings( - mgmt: &ManagementCanister<'_>, + agent: &Agent, + proxy: Option, cid: &Principal, canister: &Canister, ) -> Result<(), SyncSettingsOperationError> { - let (status,) = mgmt - .canister_status(cid) - .await - .context(FetchCurrentSettingsSnafu { canister: *cid })?; + let status = + proxy_management::canister_status(agent, proxy, CanisterIdRecord { canister_id: *cid }) + .await + .context(FetchCurrentSettingsSnafu { canister: *cid })?; let &Settings { ref log_visibility, compute_allocation, @@ -144,52 +147,41 @@ pub(crate) async fn sync_settings( // No changes needed return Ok(()); } - let mut builder = mgmt.update_settings(cid); - if let Some(v) = log_visibility_setting { - builder = builder.with_log_visibility(v); - } - if let Some(v) = compute_allocation { - builder = builder.with_compute_allocation(v); - } - if let Some(v) = memory_allocation.as_ref().map(|m| m.get()) { - builder = builder.with_memory_allocation(v); - } - if let Some(v) = freezing_threshold.as_ref().map(|d| d.get()) { - builder = builder.with_freezing_threshold(v); - } - if let Some(v) = reserved_cycles_limit.as_ref().map(|r| r.get()) { - builder = builder.with_reserved_cycles_limit(v); - } - if let Some(v) = wasm_memory_limit.as_ref().map(|m| m.get()) { - builder = builder.with_wasm_memory_limit(v); - } - if let Some(v) = wasm_memory_threshold.as_ref().map(|m| m.get()) { - builder = builder.with_wasm_memory_threshold(v); - } - if let Some(v) = log_memory_limit.as_ref().map(|m| m.get()) { - builder = builder.with_log_memory_limit(v); - } - if let Some(v) = environment_variable_setting { - builder = builder.with_environment_variables(v); - } - builder - .build() - .context(ValidateSettingsSnafu { - name: &canister.name, - })? - .await - .context(UpdateSettingsSnafu { canister: *cid })?; + + let settings = CanisterSettings { + log_visibility: log_visibility_setting, + compute_allocation: compute_allocation.map(Nat::from), + memory_allocation: memory_allocation.as_ref().map(|m| Nat::from(m.get())), + freezing_threshold: freezing_threshold.as_ref().map(|d| Nat::from(d.get())), + reserved_cycles_limit: reserved_cycles_limit.as_ref().map(|r| Nat::from(r.get())), + wasm_memory_limit: wasm_memory_limit.as_ref().map(|m| Nat::from(m.get())), + wasm_memory_threshold: wasm_memory_threshold.as_ref().map(|m| Nat::from(m.get())), + log_memory_limit: log_memory_limit.as_ref().map(|m| Nat::from(m.get())), + environment_variables: environment_variable_setting, + ..Default::default() + }; + + proxy_management::update_settings( + agent, + proxy, + UpdateSettingsArgs { + canister_id: *cid, + settings, + sender_canister_version: None, + }, + ) + .await + .context(UpdateSettingsSnafu { canister: *cid })?; Ok(()) } pub(crate) async fn sync_settings_many( agent: Agent, + proxy: Option, target_canisters: Vec<(Principal, Canister)>, debug: bool, ) -> Result<(), SyncSettingsManyError> { - let mgmt = ManagementCanister::create(&agent); - let mut futs = FuturesOrdered::new(); let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: debug }); @@ -198,12 +190,12 @@ pub(crate) async fn sync_settings_many( let canister_name = info.name.clone(); let settings_fn = { - let mgmt = mgmt.clone(); + let agent = agent.clone(); let pb = pb.clone(); async move { pb.set_message("Updating canister settings..."); - sync_settings(&mgmt, &cid, &info).await + sync_settings(&agent, proxy, &cid, &info).await } }; diff --git a/crates/icp-cli/src/operations/snapshot_transfer.rs b/crates/icp-cli/src/operations/snapshot_transfer.rs index ddaf9c4a..44692769 100644 --- a/crates/icp-cli/src/operations/snapshot_transfer.rs +++ b/crates/icp-cli/src/operations/snapshot_transfer.rs @@ -12,7 +12,9 @@ use ic_management_canister_types::{ UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataArgs, UploadCanisterSnapshotMetadataResult, }; -use ic_utils::interfaces::ManagementCanister; + +use super::proxy::UpdateOrProxyError; +use super::proxy_management; use icp::{ fs::lock::{DirectoryStructureLock, LWrite, LockError, PathsAccess}, prelude::*, @@ -105,43 +107,43 @@ pub enum SnapshotTransferError { #[snafu(display("Failed to read snapshot metadata for canister {canister_id}"))] ReadMetadata { canister_id: Principal, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to read snapshot data chunk at offset {offset}"))] ReadDataChunk { offset: u64, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to read WASM chunk with hash {hash}"))] ReadWasmChunk { hash: String, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload snapshot metadata for canister {canister_id}"))] UploadMetadata { canister_id: Principal, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload snapshot data chunk at offset {offset}"))] UploadDataChunk { offset: u64, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(display("Failed to upload WASM chunk with hash {hash}"))] UploadWasmChunk { hash: String, - #[snafu(source(from(AgentError, Box::new)))] - source: Box, + #[snafu(source(from(UpdateOrProxyError, Box::new)))] + source: Box, }, #[snafu(transparent)] @@ -379,18 +381,27 @@ impl BlobType { } /// Check if an agent error is retryable. -fn is_retryable(error: &AgentError) -> bool { +fn is_retryable_agent_error(error: &AgentError) -> bool { matches!( error, AgentError::TimeoutWaitingForResponse() | AgentError::TransportError(_) ) } +/// Check if an `UpdateOrProxyError` is retryable (by inspecting the inner agent error). +fn is_retryable(error: &UpdateOrProxyError) -> bool { + match error { + UpdateOrProxyError::DirectUpdateCall { source } + | UpdateOrProxyError::ProxyUpdateCall { source } => is_retryable_agent_error(source), + _ => false, + } +} + /// Execute an async operation with exponential backoff retry. -async fn with_retry(operation: F) -> Result +async fn with_retry(operation: F) -> Result where F: Fn() -> Fut, - Fut: std::future::Future>, + Fut: std::future::Future>, { let mut backoff = ExponentialBackoff { max_elapsed_time: Some(std::time::Duration::from_secs(60)), @@ -432,16 +443,17 @@ pub async fn read_snapshot_metadata( canister_id: Principal, snapshot_id: &[u8], ) -> Result { - let mgmt = ManagementCanister::create(agent); - let args = ReadCanisterSnapshotMetadataArgs { canister_id, snapshot_id: snapshot_id.to_vec(), }; - let (metadata,) = with_retry(|| async { mgmt.read_canister_snapshot_metadata(&args).await }) - .await - .context(ReadMetadataSnafu { canister_id })?; + let metadata = with_retry(|| { + let args = args.clone(); + async move { proxy_management::read_canister_snapshot_metadata(agent, None, args).await } + }) + .await + .context(ReadMetadataSnafu { canister_id })?; Ok(metadata) } @@ -453,8 +465,6 @@ pub async fn upload_snapshot_metadata( metadata: &ReadCanisterSnapshotMetadataResult, replace_snapshot: Option<&[u8]>, ) -> Result { - let mgmt = ManagementCanister::create(agent); - // Convert Option to SnapshotMetadataGlobal, failing on None let globals = metadata .globals @@ -477,9 +487,12 @@ pub async fn upload_snapshot_metadata( on_low_wasm_memory_hook_status: metadata.on_low_wasm_memory_hook_status.clone(), }; - let (result,) = with_retry(|| async { mgmt.upload_canister_snapshot_metadata(&args).await }) - .await - .context(UploadMetadataSnafu { canister_id })?; + let result = with_retry(|| { + let args = args.clone(); + async move { proxy_management::upload_canister_snapshot_metadata(agent, None, args).await } + }) + .await + .context(UploadMetadataSnafu { canister_id })?; Ok(result) } @@ -511,8 +524,6 @@ pub async fn download_blob_to_file( return Ok(()); } - let mgmt = ManagementCanister::create(agent); - // Create or open file for random-access writing let file = if output_path.exists() { File::options() @@ -551,14 +562,19 @@ pub async fn download_blob_to_file( kind: blob_type.make_read_kind(chunk_offset, chunk_size), }; - let mgmt = mgmt.clone(); in_progress.push(async move { - let result = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) + let result = + with_retry(|| { + let args = args.clone(); + async move { + proxy_management::read_canister_snapshot_data(agent, None, args).await + } + }) .await .context(ReadDataChunkSnafu { offset: chunk_offset, })?; - Ok::<_, SnapshotTransferError>((chunk_offset, result.0.chunk)) + Ok::<_, SnapshotTransferError>((chunk_offset, result.chunk)) }); } @@ -608,8 +624,6 @@ pub async fn download_wasm_chunk( chunk_hash: &ChunkHash, paths: LWrite<&SnapshotPaths>, ) -> Result<(), SnapshotTransferError> { - let mgmt = ManagementCanister::create(agent); - let args = ReadCanisterSnapshotDataArgs { canister_id, snapshot_id: snapshot_id.to_vec(), @@ -621,9 +635,12 @@ pub async fn download_wasm_chunk( let hash_hex = hex::encode(&chunk_hash.hash); let output_path = paths.wasm_chunk_path(&chunk_hash.hash); - let (result,) = with_retry(|| async { mgmt.read_canister_snapshot_data(&args).await }) - .await - .context(ReadWasmChunkSnafu { hash: &hash_hex })?; + let result = with_retry(|| { + let args = args.clone(); + async move { proxy_management::read_canister_snapshot_data(agent, None, args).await } + }) + .await + .context(ReadWasmChunkSnafu { hash: &hash_hex })?; icp::fs::write(&output_path, &result.chunk)?; @@ -659,8 +676,6 @@ pub async fn upload_blob_from_file( BlobType::StableMemory => progress.stable_memory_offset, }; - let mgmt = ManagementCanister::create(agent); - let mut file = File::open(&input_path) .await .context(OpenBlobForUploadSnafu { path: &input_path })?; @@ -695,12 +710,17 @@ pub async fn upload_blob_from_file( chunk, }; - let mgmt = mgmt.clone(); + let chunk_len = args.chunk.len() as u64; in_progress.push(async move { - with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) - .await - .context(UploadDataChunkSnafu { offset })?; - Ok::<_, SnapshotTransferError>((offset, args.chunk.len() as u64)) + with_retry(|| { + let args = args.clone(); + async move { + proxy_management::upload_canister_snapshot_data(agent, None, args).await + } + }) + .await + .context(UploadDataChunkSnafu { offset })?; + Ok::<_, SnapshotTransferError>((offset, chunk_len)) }); } @@ -756,8 +776,6 @@ pub async fn upload_wasm_chunk( chunk_hash: &[u8], paths: LWrite<&SnapshotPaths>, ) -> Result<(), SnapshotTransferError> { - let mgmt = ManagementCanister::create(agent); - let chunk_path = paths.wasm_chunk_path(chunk_hash); let chunk = icp::fs::read(&chunk_path)?; @@ -770,9 +788,12 @@ pub async fn upload_wasm_chunk( let hash_hex = hex::encode(chunk_hash); - with_retry(|| async { mgmt.upload_canister_snapshot_data(&args).await }) - .await - .context(UploadWasmChunkSnafu { hash: hash_hex })?; + with_retry(|| { + let args = args.clone(); + async move { proxy_management::upload_canister_snapshot_data(agent, None, args).await } + }) + .await + .context(UploadWasmChunkSnafu { hash: hash_hex })?; Ok(()) } From f5c92a7772065f3ce850d738770dc06a935f3307 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 14:43:41 -0400 Subject: [PATCH 03/13] fix: set effective_canister_id for direct management canister calls Management canister calls routed through `update_or_proxy` were missing the effective_canister_id, causing the IC HTTP endpoint to fail with "canister_not_found" when routing the request. The old `ic_utils::ManagementCanister` set this automatically; the new raw `agent.update()` path needs it explicitly. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/src/commands/canister/call.rs | 1 + crates/icp-cli/src/operations/proxy.rs | 29 ++++++++++++--- .../src/operations/proxy_management.rs | 37 +++++++++++++++++++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 8ef699c9..50df8d83 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -230,6 +230,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E &method, arg_bytes, args.proxy, + None, args.cycles.get(), ) .await? diff --git a/crates/icp-cli/src/operations/proxy.rs b/crates/icp-cli/src/operations/proxy.rs index 51e0b8b9..a50f4947 100644 --- a/crates/icp-cli/src/operations/proxy.rs +++ b/crates/icp-cli/src/operations/proxy.rs @@ -33,6 +33,12 @@ pub enum UpdateOrProxyError { /// If `proxy` is `None`, makes a direct update call to the target canister. /// If `proxy` is `Some`, wraps the call in [`ProxyArgs`] and sends it to the /// proxy canister's `proxy` method, which forwards it to the target. +/// +/// `effective_canister_id` overrides the effective canister ID used for HTTP +/// routing in direct calls. This is required when calling the management +/// canister, where the effective canister ID must be the target canister +/// rather than `aaaaa-aa`. When `None`, defaults to `canister_id`. +/// /// The `cycles` parameter is only used for proxied calls. pub async fn update_or_proxy_raw( agent: &Agent, @@ -40,6 +46,7 @@ pub async fn update_or_proxy_raw( method: &str, arg: Vec, proxy: Option, + effective_canister_id: Option, cycles: u128, ) -> Result, UpdateOrProxyError> { if let Some(proxy_cid) = proxy { @@ -68,11 +75,11 @@ pub async fn update_or_proxy_raw( .fail(), } } else { - let res = agent - .update(&canister_id, method) - .with_arg(arg) - .await - .context(DirectUpdateCallSnafu)?; + let mut builder = agent.update(&canister_id, method).with_arg(arg); + if let Some(eid) = effective_canister_id { + builder = builder.with_effective_canister_id(eid); + } + let res = builder.await.context(DirectUpdateCallSnafu)?; Ok(res) } } @@ -84,6 +91,7 @@ pub async fn update_or_proxy( method: &str, args: A, proxy: Option, + effective_canister_id: Option, cycles: u128, ) -> Result where @@ -91,7 +99,16 @@ where R: for<'a> ArgumentDecoder<'a>, { let arg = candid::encode_args(args).context(CandidEncodeSnafu)?; - let res = update_or_proxy_raw(agent, canister_id, method, arg, proxy, cycles).await?; + let res = update_or_proxy_raw( + agent, + canister_id, + method, + arg, + proxy, + effective_canister_id, + cycles, + ) + .await?; let decoded: R = candid::decode_args(&res).context(CandidDecodeSnafu)?; Ok(decoded) } diff --git a/crates/icp-cli/src/operations/proxy_management.rs b/crates/icp-cli/src/operations/proxy_management.rs index 6b8f7a5d..27b18102 100644 --- a/crates/icp-cli/src/operations/proxy_management.rs +++ b/crates/icp-cli/src/operations/proxy_management.rs @@ -26,6 +26,7 @@ pub async fn create_canister( "create_canister", (args,), proxy, + None, cycles, ) .await?; @@ -37,12 +38,14 @@ pub async fn canister_status( proxy: Option, args: CanisterIdRecord, ) -> Result { + let effective = args.canister_id; let (result,): (CanisterStatusResult,) = update_or_proxy( agent, Principal::management_canister(), "canister_status", (args,), proxy, + Some(effective), 0, ) .await?; @@ -54,12 +57,14 @@ pub async fn stop_canister( proxy: Option, args: StopCanisterArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "stop_canister", (args,), proxy, + Some(effective), 0, ) .await @@ -70,12 +75,14 @@ pub async fn start_canister( proxy: Option, args: StartCanisterArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "start_canister", (args,), proxy, + Some(effective), 0, ) .await @@ -86,12 +93,14 @@ pub async fn delete_canister( proxy: Option, args: DeleteCanisterArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "delete_canister", (args,), proxy, + Some(effective), 0, ) .await @@ -102,12 +111,14 @@ pub async fn update_settings( proxy: Option, args: UpdateSettingsArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "update_settings", (args,), proxy, + Some(effective), 0, ) .await @@ -118,12 +129,14 @@ pub async fn install_code( proxy: Option, args: InstallCodeArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "install_code", (args,), proxy, + Some(effective), 0, ) .await @@ -134,12 +147,14 @@ pub async fn install_chunked_code( proxy: Option, args: InstallChunkedCodeArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.target_canister; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "install_chunked_code", (args,), proxy, + Some(effective), 0, ) .await @@ -150,12 +165,14 @@ pub async fn upload_chunk( proxy: Option, args: UploadChunkArgs, ) -> Result { + let effective = args.canister_id; let (result,): (UploadChunkResult,) = update_or_proxy( agent, Principal::management_canister(), "upload_chunk", (args,), proxy, + Some(effective), 0, ) .await?; @@ -167,12 +184,14 @@ pub async fn clear_chunk_store( proxy: Option, args: ClearChunkStoreArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "clear_chunk_store", (args,), proxy, + Some(effective), 0, ) .await @@ -183,12 +202,14 @@ pub async fn fetch_canister_logs( proxy: Option, args: FetchCanisterLogsArgs, ) -> Result { + let effective = args.canister_id; let (result,): (FetchCanisterLogsResult,) = update_or_proxy( agent, Principal::management_canister(), "fetch_canister_logs", (args,), proxy, + Some(effective), 0, ) .await?; @@ -200,12 +221,14 @@ pub async fn take_canister_snapshot( proxy: Option, args: TakeCanisterSnapshotArgs, ) -> Result { + let effective = args.canister_id; let (result,): (TakeCanisterSnapshotResult,) = update_or_proxy( agent, Principal::management_canister(), "take_canister_snapshot", (args,), proxy, + Some(effective), 0, ) .await?; @@ -217,12 +240,14 @@ pub async fn load_canister_snapshot( proxy: Option, args: LoadCanisterSnapshotArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "load_canister_snapshot", (args,), proxy, + Some(effective), 0, ) .await @@ -233,12 +258,14 @@ pub async fn list_canister_snapshots( proxy: Option, args: ListCanisterSnapshotsArgs, ) -> Result { + let effective = args.canister_id; let (result,): (ListCanisterSnapshotsResult,) = update_or_proxy( agent, Principal::management_canister(), "list_canister_snapshots", (args,), proxy, + Some(effective), 0, ) .await?; @@ -250,12 +277,14 @@ pub async fn delete_canister_snapshot( proxy: Option, args: DeleteCanisterSnapshotArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "delete_canister_snapshot", (args,), proxy, + Some(effective), 0, ) .await @@ -266,12 +295,14 @@ pub async fn read_canister_snapshot_metadata( proxy: Option, args: ReadCanisterSnapshotMetadataArgs, ) -> Result { + let effective = args.canister_id; let (result,): (ReadCanisterSnapshotMetadataResult,) = update_or_proxy( agent, Principal::management_canister(), "read_canister_snapshot_metadata", (args,), proxy, + Some(effective), 0, ) .await?; @@ -283,12 +314,14 @@ pub async fn upload_canister_snapshot_metadata( proxy: Option, args: UploadCanisterSnapshotMetadataArgs, ) -> Result { + let effective = args.canister_id; let (result,): (UploadCanisterSnapshotMetadataResult,) = update_or_proxy( agent, Principal::management_canister(), "upload_canister_snapshot_metadata", (args,), proxy, + Some(effective), 0, ) .await?; @@ -300,12 +333,14 @@ pub async fn read_canister_snapshot_data( proxy: Option, args: ReadCanisterSnapshotDataArgs, ) -> Result { + let effective = args.canister_id; let (result,): (ReadCanisterSnapshotDataResult,) = update_or_proxy( agent, Principal::management_canister(), "read_canister_snapshot_data", (args,), proxy, + Some(effective), 0, ) .await?; @@ -317,12 +352,14 @@ pub async fn upload_canister_snapshot_data( proxy: Option, args: UploadCanisterSnapshotDataArgs, ) -> Result<(), UpdateOrProxyError> { + let effective = args.canister_id; update_or_proxy::<_, ()>( agent, Principal::management_canister(), "upload_canister_snapshot_data", (args,), proxy, + Some(effective), 0, ) .await From 19b26e8b50ca6140bc8899a32e5dda7342a79d4c Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 14:48:34 -0400 Subject: [PATCH 04/13] fix: use Candid-encoded empty args for install_code default When no init args are provided, the old ic_utils builder serialized an empty Candid message (DIDL\x00\x00) via `Encode!()`. The refactored code used `unwrap_or_default()` producing raw empty bytes, which the management canister cannot parse as valid Candid. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/src/operations/install.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/icp-cli/src/operations/install.rs b/crates/icp-cli/src/operations/install.rs index 7c69c452..0564f3ed 100644 --- a/crates/icp-cli/src/operations/install.rs +++ b/crates/icp-cli/src/operations/install.rs @@ -1,3 +1,4 @@ +use candid::Encode; use futures::{StreamExt, stream::FuturesOrdered}; use ic_agent::{Agent, export::Principal}; use ic_management_canister_types::{ @@ -157,11 +158,13 @@ async fn do_install_operation( // Generous overhead for encoding, target canister ID, install mode, etc. const ENCODING_OVERHEAD: usize = 500; - // Calculate total install message size - let init_args_len = init_args.map_or(0, |args| args.len()); - let total_install_size = wasm.len() + init_args_len + ENCODING_OVERHEAD; - let cid = CanisterId::from(*canister_id); + let arg = init_args + .map(|a| a.to_vec()) + .unwrap_or_else(|| Encode!().unwrap()); + + // Calculate total install message size + let total_install_size = wasm.len() + arg.len() + ENCODING_OVERHEAD; if total_install_size <= CHUNK_THRESHOLD { // Small wasm: use regular install_code @@ -171,7 +174,7 @@ async fn do_install_operation( mode, canister_id: cid, wasm_module: wasm.to_vec(), - arg: init_args.map(|a| a.to_vec()).unwrap_or_default(), + arg, sender_canister_version: None, }; @@ -231,7 +234,7 @@ async fn do_install_operation( store_canister: None, chunk_hashes_list: chunk_hashes, wasm_module_hash, - arg: init_args.map(|a| a.to_vec()).unwrap_or_default(), + arg, sender_canister_version: None, }; From 3b3f1286071b0e4346a3684fed3bc457c3730033 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 15:22:05 -0400 Subject: [PATCH 05/13] fix: use query call for direct fetch_canister_logs fetch_canister_logs is a query method on the management canister, not an update. When no proxy is provided, make a direct query call instead of routing through update_or_proxy. Introduce FetchCanisterLogsError to model the distinct direct-query and proxied-update error paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../src/operations/proxy_management.rs | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/crates/icp-cli/src/operations/proxy_management.rs b/crates/icp-cli/src/operations/proxy_management.rs index 27b18102..ffc7cac7 100644 --- a/crates/icp-cli/src/operations/proxy_management.rs +++ b/crates/icp-cli/src/operations/proxy_management.rs @@ -12,6 +12,8 @@ use ic_management_canister_types::{ UploadCanisterSnapshotMetadataResult, UploadChunkArgs, UploadChunkResult, }; +use snafu::{ResultExt, Snafu}; + use super::proxy::{UpdateOrProxyError, update_or_proxy}; pub async fn create_canister( @@ -197,23 +199,57 @@ pub async fn clear_chunk_store( .await } +#[derive(Debug, Snafu)] +pub enum FetchCanisterLogsError { + #[snafu(display("failed to encode call arguments: {source}"))] + CandidEncode { source: candid::Error }, + + #[snafu(display("failed to decode call response: {source}"))] + CandidDecode { source: candid::Error }, + + #[snafu(display("direct query call failed: {source}"))] + DirectQueryCall { source: ic_agent::AgentError }, + + #[snafu(display("proxied call failed: {source}"))] + ProxiedCall { source: UpdateOrProxyError }, +} + +/// Fetches canister logs from the management canister. +/// +/// Unlike other management canister methods, `fetch_canister_logs` is a +/// **query** call when made directly. When a proxy is provided, the call is +/// routed through the proxy canister as an update call instead. pub async fn fetch_canister_logs( agent: &Agent, proxy: Option, args: FetchCanisterLogsArgs, -) -> Result { +) -> Result { let effective = args.canister_id; - let (result,): (FetchCanisterLogsResult,) = update_or_proxy( - agent, - Principal::management_canister(), - "fetch_canister_logs", - (args,), - proxy, - Some(effective), - 0, - ) - .await?; - Ok(result) + if proxy.is_some() { + let (result,): (FetchCanisterLogsResult,) = update_or_proxy( + agent, + Principal::management_canister(), + "fetch_canister_logs", + (args,), + proxy, + Some(effective), + 0, + ) + .await + .context(ProxiedCallSnafu)?; + Ok(result) + } else { + let arg = candid::encode_args((args,)).context(CandidEncodeSnafu)?; + let res = agent + .query(&Principal::management_canister(), "fetch_canister_logs") + .with_arg(arg) + .with_effective_canister_id(effective) + .await + .context(DirectQueryCallSnafu)?; + let (result,): (FetchCanisterLogsResult,) = + candid::decode_args(&res).context(CandidDecodeSnafu)?; + Ok(result) + } } pub async fn take_canister_snapshot( From a5db0c2445e2b0d82935778d2ab2319dab66d509 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 15:39:18 -0400 Subject: [PATCH 06/13] feat: add --proxy flag to canister subcommands Add a --proxy CLI flag to all `icp canister` subcommands that involve management canister calls via proxy_management, allowing users to route these calls through a proxy canister. Commands updated: start, stop, delete, install, status, logs, migrate-id, settings update, settings sync, snapshot create/restore/ list/delete. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/src/commands/canister/delete.rs | 8 +++++++- .../icp-cli/src/commands/canister/install.rs | 18 ++++++++++++++---- crates/icp-cli/src/commands/canister/logs.rs | 16 ++++++++++++---- .../src/commands/canister/migrate_id.rs | 14 +++++++++----- .../src/commands/canister/settings/sync.rs | 7 ++++++- .../src/commands/canister/settings/update.rs | 14 +++++++++++--- .../src/commands/canister/snapshot/create.rs | 16 ++++++++++++---- .../src/commands/canister/snapshot/delete.rs | 7 ++++++- .../src/commands/canister/snapshot/list.rs | 7 ++++++- .../src/commands/canister/snapshot/restore.rs | 16 ++++++++++++---- crates/icp-cli/src/commands/canister/start.rs | 8 +++++++- crates/icp-cli/src/commands/canister/status.rs | 6 +++++- crates/icp-cli/src/commands/canister/stop.rs | 8 +++++++- 13 files changed, 114 insertions(+), 31 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index f02ad6f1..de32f003 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -1,3 +1,4 @@ +use candid::Principal; use clap::Args; use ic_management_canister_types::CanisterIdRecord; use icp::context::{CanisterSelection, Context}; @@ -9,6 +10,10 @@ use crate::{commands::args, operations::proxy_management}; pub(crate) struct DeleteArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow::Error> { @@ -29,7 +34,8 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: ) .await?; - proxy_management::delete_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; + proxy_management::delete_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; // Remove canister ID from the id store if it was referenced by name if let CanisterSelection::Named(canister_name) = &selections.canister { diff --git a/crates/icp-cli/src/commands/canister/install.rs b/crates/icp-cli/src/commands/canister/install.rs index 9d9020e3..2b45f713 100644 --- a/crates/icp-cli/src/commands/canister/install.rs +++ b/crates/icp-cli/src/commands/canister/install.rs @@ -1,6 +1,7 @@ use std::io::IsTerminal; use anyhow::{Context as _, anyhow, bail}; +use candid::Principal; use clap::Args; use dialoguer::Confirm; use ic_management_canister_types::CanisterInstallMode; @@ -47,6 +48,10 @@ pub(crate) struct InstallArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow::Error> { @@ -121,9 +126,14 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow .transpose()?; let canister_display = args.cmd_args.canister.to_string(); - let (install_mode, status) = - resolve_install_mode_and_status(&agent, None, &canister_display, &canister_id, &args.mode) - .await?; + let (install_mode, status) = resolve_install_mode_and_status( + &agent, + args.proxy, + &canister_display, + &canister_id, + &args.mode, + ) + .await?; // Candid interface compatibility check for upgrades if !args.yes && matches!(install_mode, CanisterInstallMode::Upgrade(_)) { @@ -156,7 +166,7 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow install_canister( &agent, - None, + args.proxy, &canister_id, &canister_display, &wasm, diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index c5290693..7d32cb96 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -1,6 +1,7 @@ use std::io::stdout; use anyhow::{Context as _, anyhow}; +use candid::Principal; use clap::Args; use ic_agent::Agent; use ic_management_canister_types::{CanisterLogFilter, CanisterLogRecord, FetchCanisterLogsArgs}; @@ -48,6 +49,10 @@ pub(crate) struct LogsArgs { /// Output command results as JSON #[arg(long)] pub(crate) json: bool, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } fn parse_timestamp(s: &str) -> Result { @@ -103,10 +108,10 @@ pub(crate) async fn exec(ctx: &Context, args: &LogsArgs) -> Result<(), anyhow::E if args.follow { // Follow mode: continuously fetch and display new logs - follow_logs(args, &agent, &canister_id, args.interval).await + follow_logs(args, &agent, args.proxy, &canister_id, args.interval).await } else { // Single fetch mode: fetch all logs once - fetch_and_display_logs(args, &agent, &canister_id, build_filter(args)?).await + fetch_and_display_logs(args, &agent, args.proxy, &canister_id, build_filter(args)?).await } } @@ -158,6 +163,7 @@ fn build_filter(args: &LogsArgs) -> Result, anyhow::Er async fn fetch_and_display_logs( args: &LogsArgs, agent: &Agent, + proxy: Option, canister_id: &candid::Principal, filter: Option, ) -> Result<(), anyhow::Error> { @@ -165,7 +171,7 @@ async fn fetch_and_display_logs( canister_id: *canister_id, filter, }; - let result = proxy_management::fetch_canister_logs(agent, None, fetch_args) + let result = proxy_management::fetch_canister_logs(agent, proxy, fetch_args) .await .context("Failed to fetch canister logs")?; @@ -203,6 +209,7 @@ const FOLLOW_LOOKBACK_NANOS: u64 = 60 * 60 * 1_000_000_000; // 1 hour async fn follow_logs( args: &LogsArgs, agent: &Agent, + proxy: Option, canister_id: &candid::Principal, interval_seconds: u64, ) -> Result<(), anyhow::Error> { @@ -232,7 +239,7 @@ async fn follow_logs( canister_id: *canister_id, filter, }; - let result = proxy_management::fetch_canister_logs(agent, None, fetch_args) + let result = proxy_management::fetch_canister_logs(agent, proxy, fetch_args) .await .context("Failed to fetch canister logs")?; @@ -434,6 +441,7 @@ mod tests { since_index, until_index, json: false, + proxy: None, } } diff --git a/crates/icp-cli/src/commands/canister/migrate_id.rs b/crates/icp-cli/src/commands/canister/migrate_id.rs index 81afa248..01ab97a7 100644 --- a/crates/icp-cli/src/commands/canister/migrate_id.rs +++ b/crates/icp-cli/src/commands/canister/migrate_id.rs @@ -47,6 +47,10 @@ pub(crate) struct MigrateIdArgs { /// Exit as soon as the migrated canister is deleted (don't wait for full completion) #[arg(long)] skip_watch: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyhow::Error> { @@ -110,7 +114,7 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh // Fetch status of both canisters let source_status = proxy_management::canister_status( &agent, - None, + args.proxy, CanisterIdRecord { canister_id: source_cid, }, @@ -118,7 +122,7 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh .await?; let target_status = proxy_management::canister_status( &agent, - None, + args.proxy, CanisterIdRecord { canister_id: target_cid, }, @@ -172,7 +176,7 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh // Check target canister has no snapshots let snapshots = proxy_management::list_canister_snapshots( &agent, - None, + args.proxy, CanisterIdRecord { canister_id: target_cid, }, @@ -211,7 +215,7 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh new_controllers.push(NNS_MIGRATION_PRINCIPAL); proxy_management::update_settings( &agent, - None, + args.proxy, UpdateSettingsArgs { canister_id: source_cid, settings: CanisterSettings { @@ -231,7 +235,7 @@ pub(crate) async fn exec(ctx: &Context, args: &MigrateIdArgs) -> Result<(), anyh new_controllers.push(NNS_MIGRATION_PRINCIPAL); proxy_management::update_settings( &agent, - None, + args.proxy, UpdateSettingsArgs { canister_id: target_cid, settings: CanisterSettings { diff --git a/crates/icp-cli/src/commands/canister/settings/sync.rs b/crates/icp-cli/src/commands/canister/settings/sync.rs index 474d2d42..7de66c89 100644 --- a/crates/icp-cli/src/commands/canister/settings/sync.rs +++ b/crates/icp-cli/src/commands/canister/settings/sync.rs @@ -1,4 +1,5 @@ use anyhow::bail; +use candid::Principal; use clap::Args; use icp::context::{CanisterSelection, Context}; @@ -9,6 +10,10 @@ use crate::commands::args::CanisterCommandArgs; pub(crate) struct SyncArgs { #[command(flatten)] cmd_args: CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::Error> { @@ -36,6 +41,6 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E ) .await?; - crate::operations::settings::sync_settings(&agent, None, &cid, &canister).await?; + crate::operations::settings::sync_settings(&agent, args.proxy, &cid, &canister).await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 1f352378..56eaac51 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -134,6 +134,10 @@ pub(crate) struct UpdateArgs { #[command(flatten)] environment_variables: Option, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow::Error> { @@ -171,8 +175,12 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: let mut current_status: Option = None; if require_current_settings(args) { current_status = Some( - proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) - .await?, + proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?, ); } @@ -279,7 +287,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: proxy_management::update_settings( &agent, - None, + args.proxy, UpdateSettingsArgs { canister_id: cid, settings, diff --git a/crates/icp-cli/src/commands/canister/snapshot/create.rs b/crates/icp-cli/src/commands/canister/snapshot/create.rs index 7c4e2241..3ea74fa1 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/create.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/create.rs @@ -2,6 +2,7 @@ use std::io::stdout; use anyhow::bail; use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use ic_management_canister_types::{ CanisterIdRecord, CanisterStatusType, TakeCanisterSnapshotArgs, @@ -30,6 +31,10 @@ pub(crate) struct CreateArgs { /// Suppress human-readable output; print only snapshot ID #[arg(long, short)] quiet: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> { @@ -52,9 +57,12 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: // Check canister status - must be stopped to create a snapshot let name = &args.cmd_args.canister; - let status = - proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) - .await?; + let status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -74,7 +82,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow: sender_canister_version: None, }; - let snapshot = proxy_management::take_canister_snapshot(&agent, None, take_args).await?; + let snapshot = proxy_management::take_canister_snapshot(&agent, args.proxy, take_args).await?; if args.json { serde_json::to_writer( stdout(), diff --git a/crates/icp-cli/src/commands/canister/snapshot/delete.rs b/crates/icp-cli/src/commands/canister/snapshot/delete.rs index 07adfa38..e31ce320 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/delete.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/delete.rs @@ -1,3 +1,4 @@ +use candid::Principal; use clap::Args; use ic_management_canister_types::DeleteCanisterSnapshotArgs; use icp::context::Context; @@ -14,6 +15,10 @@ pub(crate) struct DeleteArgs { /// The snapshot ID to delete (hex-encoded) snapshot_id: SnapshotId, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow::Error> { @@ -39,7 +44,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: snapshot_id: args.snapshot_id.0.clone(), }; - proxy_management::delete_canister_snapshot(&agent, None, delete_args).await?; + proxy_management::delete_canister_snapshot(&agent, args.proxy, delete_args).await?; let name = &args.cmd_args.canister; info!( diff --git a/crates/icp-cli/src/commands/canister/snapshot/list.rs b/crates/icp-cli/src/commands/canister/snapshot/list.rs index 7daa71be..01c1bad6 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/list.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/list.rs @@ -1,6 +1,7 @@ use std::io::stdout; use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; @@ -22,6 +23,10 @@ pub(crate) struct ListArgs { /// Suppress human-readable output; print only snapshot IDs #[arg(long, short)] pub(crate) quiet: bool, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::Error> { @@ -44,7 +49,7 @@ pub(crate) async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::E let snapshots = proxy_management::list_canister_snapshots( &agent, - None, + args.proxy, CanisterIdRecord { canister_id: cid }, ) .await?; diff --git a/crates/icp-cli/src/commands/canister/snapshot/restore.rs b/crates/icp-cli/src/commands/canister/snapshot/restore.rs index db6b51fd..bf6ebb8c 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/restore.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/restore.rs @@ -1,4 +1,5 @@ use anyhow::bail; +use candid::Principal; use clap::Args; use ic_management_canister_types::{ CanisterIdRecord, CanisterStatusType, LoadCanisterSnapshotArgs, @@ -17,6 +18,10 @@ pub(crate) struct RestoreArgs { /// The snapshot ID to restore (hex-encoded) snapshot_id: SnapshotId, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow::Error> { @@ -39,9 +44,12 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow // Check canister status - must be stopped to restore a snapshot let name = &args.cmd_args.canister; - let status = - proxy_management::canister_status(&agent, None, CanisterIdRecord { canister_id: cid }) - .await?; + let status = proxy_management::canister_status( + &agent, + args.proxy, + CanisterIdRecord { canister_id: cid }, + ) + .await?; match status.status { CanisterStatusType::Running => { bail!( @@ -60,7 +68,7 @@ pub(crate) async fn exec(ctx: &Context, args: &RestoreArgs) -> Result<(), anyhow sender_canister_version: None, }; - proxy_management::load_canister_snapshot(&agent, None, load_args).await?; + proxy_management::load_canister_snapshot(&agent, args.proxy, load_args).await?; info!( "Restored canister {name} ({cid}) from snapshot {id}", diff --git a/crates/icp-cli/src/commands/canister/start.rs b/crates/icp-cli/src/commands/canister/start.rs index 98278f06..4278cc7f 100644 --- a/crates/icp-cli/src/commands/canister/start.rs +++ b/crates/icp-cli/src/commands/canister/start.rs @@ -1,3 +1,4 @@ +use candid::Principal; use clap::Args; use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; @@ -9,6 +10,10 @@ use crate::{commands::args, operations::proxy_management}; pub(crate) struct StartArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::Error> { @@ -28,7 +33,8 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow:: ) .await?; - proxy_management::start_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; + proxy_management::start_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; Ok(()) } diff --git a/crates/icp-cli/src/commands/canister/status.rs b/crates/icp-cli/src/commands/canister/status.rs index ebce9de4..18412e1c 100644 --- a/crates/icp-cli/src/commands/canister/status.rs +++ b/crates/icp-cli/src/commands/canister/status.rs @@ -62,6 +62,10 @@ pub(crate) struct StatusArgsOptions { /// looks up public information from the state tree. #[arg(short, long)] pub public: bool, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub proxy: Option, } /// Fetch the list of canister ids from the id_store @@ -227,7 +231,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StatusArgs) -> Result<(), anyhow: // Retrieve canister status from management canister match proxy_management::canister_status( &agent, - None, + args.options.proxy, CanisterIdRecord { canister_id: *cid }, ) .await diff --git a/crates/icp-cli/src/commands/canister/stop.rs b/crates/icp-cli/src/commands/canister/stop.rs index c328ada8..0a10bb0f 100644 --- a/crates/icp-cli/src/commands/canister/stop.rs +++ b/crates/icp-cli/src/commands/canister/stop.rs @@ -1,3 +1,4 @@ +use candid::Principal; use clap::Args; use ic_management_canister_types::CanisterIdRecord; use icp::context::Context; @@ -9,6 +10,10 @@ use crate::{commands::args, operations::proxy_management}; pub(crate) struct StopArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, + + /// Principal of a proxy canister to route the management canister call through. + #[arg(long)] + pub(crate) proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), anyhow::Error> { @@ -28,7 +33,8 @@ pub(crate) async fn exec(ctx: &Context, args: &StopArgs) -> Result<(), anyhow::E ) .await?; - proxy_management::stop_canister(&agent, None, CanisterIdRecord { canister_id: cid }).await?; + proxy_management::stop_canister(&agent, args.proxy, CanisterIdRecord { canister_id: cid }) + .await?; Ok(()) } From d078248bcf12996bfc18a5d88510676e9cd5bfd2 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 15:41:59 -0400 Subject: [PATCH 07/13] docs: update changelog and CLI reference for --proxy flag Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- docs/reference/cli.md | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 38a96e7a..2ff8ae4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -* feat: `icp canister create --proxy` to create canisters via a proxy canister +* feat: Add `--proxy` to `icp canister` subcommands that call the management canister # v0.2.2 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9a729145..6141b26c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -266,6 +266,7 @@ Delete a canister from a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -307,6 +308,7 @@ Install a built WASM to a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -348,6 +350,7 @@ Fetch and display canister logs * `--since-index ` — Show logs at or after this log index (inclusive). Cannot be used with --follow * `--until-index ` — Show logs before this log index (exclusive). Cannot be used with --follow * `--json` — Output command results as JSON +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -392,6 +395,7 @@ Migrate a canister ID from one subnet to another * `-y`, `--yes` — Skip confirmation prompts * `--resume-watch` — Resume watching an already-initiated migration (skips validation and initiation) * `--skip-watch` — Exit as soon as the migrated canister is deleted (don't wait for full completion) +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -430,6 +434,7 @@ By default this queries the status endpoint of the management canister. If the c * `-i`, `--id-only` — Only print the canister ids * `--json` — Format output in json * `-p`, `--public` — Show the only the public information. Skips trying to get the status from the management canister and looks up public information from the state tree +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -470,6 +475,7 @@ Change a canister's settings to specified values * `--set-log-viewer ` * `--add-environment-variable ` * `--remove-environment-variable ` +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -489,6 +495,7 @@ Synchronize a canister's settings with those defined in the project * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -528,6 +535,7 @@ Create a snapshot of a canister's state * `--replace ` — Replace an existing snapshot instead of creating a new one. The old snapshot will be deleted once the new one is successfully created * `--json` — Output command results as JSON * `-q`, `--quiet` — Suppress human-readable output; print only snapshot ID +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -548,6 +556,7 @@ Delete a canister snapshot * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -591,6 +600,7 @@ List all snapshots for a canister * `--identity ` — The user identity to run this command as * `--json` — Output command results as JSON * `-q`, `--quiet` — Suppress human-readable output; print only snapshot IDs +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -611,6 +621,7 @@ Restore a canister from a snapshot * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -654,6 +665,7 @@ Start a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -678,6 +690,7 @@ By default this queries the status endpoint of the management canister. If the c * `-i`, `--id-only` — Only print the canister ids * `--json` — Format output in json * `-p`, `--public` — Show the only the public information. Skips trying to get the status from the management canister and looks up public information from the state tree +* `--proxy ` — Principal of a proxy canister to route the management canister call through @@ -697,6 +710,7 @@ Stop a canister on a network * `-k`, `--root-key ` — The root key to use if connecting to a network by URL. Required when using `--network ` * `-e`, `--environment ` — Override the environment to connect to. By default, the local environment is used * `--identity ` — The user identity to run this command as +* `--proxy ` — Principal of a proxy canister to route the management canister call through From b3c9961319604266cf2c8dad16cb1627b0c5964e Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 16:15:32 -0400 Subject: [PATCH 08/13] feat: add --proxy flag to icp deploy command Route management canister calls through a proxy canister during deploy. Includes a TODO for the known limitation where sync steps (asset uploads) fail for newly created frontend canisters when using --proxy. Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 2 +- crates/icp-cli/src/commands/deploy.rs | 26 ++++++++++++++++++-------- docs/reference/cli.md | 1 + 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ff8ae4e..742ed55d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Unreleased -* feat: Add `--proxy` to `icp canister` subcommands that call the management canister +* feat: Add `--proxy` to `icp canister` subcommands and `icp deploy` to route management canister calls through a proxy canister # v0.2.2 diff --git a/crates/icp-cli/src/commands/deploy.rs b/crates/icp-cli/src/commands/deploy.rs index d31f3915..443724b0 100644 --- a/crates/icp-cli/src/commands/deploy.rs +++ b/crates/icp-cli/src/commands/deploy.rs @@ -39,9 +39,13 @@ pub(crate) struct DeployArgs { pub(crate) mode: String, /// The subnet to use for the canisters being deployed. - #[clap(long)] + #[clap(long, conflicts_with = "proxy")] pub(crate) subnet: Option, + /// Principal of a proxy canister to route management canister calls through. + #[arg(long, conflicts_with = "subnet")] + pub(crate) proxy: Option, + /// One or more controllers for the canisters being deployed. Repeat `--controller` to specify multiple. #[arg(long)] pub(crate) controller: Vec, @@ -127,9 +131,10 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: if canisters_to_create.is_empty() { info!("All canisters already exist"); } else { - let target = match args.subnet { - Some(subnet) => CreateTarget::Subnet(subnet), - None => CreateTarget::None, + let target = match (args.subnet, args.proxy) { + (Some(subnet), _) => CreateTarget::Subnet(subnet), + (_, Some(proxy)) => CreateTarget::Proxy(proxy), + _ => CreateTarget::None, }; let create_operation = CreateOperation::new( agent.clone(), @@ -217,7 +222,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: set_binding_env_vars_many( agent.clone(), - None, + args.proxy, &env.name, target_canisters.clone(), canister_list, @@ -226,7 +231,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .await .map_err(|e| anyhow!(e))?; - sync_settings_many(agent.clone(), None, target_canisters, ctx.debug) + sync_settings_many(agent.clone(), args.proxy, target_canisters, ctx.debug) .await .map_err(|e| anyhow!(e))?; @@ -245,7 +250,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: .map_err(|e| anyhow!(e))?; let (mode, status) = - resolve_install_mode_and_status(&agent, None, name, &cid, &args.mode).await?; + resolve_install_mode_and_status(&agent, args.proxy, name, &cid, &args.mode).await?; let env = ctx.get_environment(&environment_selection).await?; let (_canister_path, canister_info) = @@ -280,7 +285,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: install_many( agent.clone(), - None, + args.proxy, canisters, ctx.artifacts.clone(), ctx.debug, @@ -323,6 +328,11 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: if sync_canisters.is_empty() { info!("No canisters have sync steps configured"); } else { + // TODO: When `--proxy` is used and the canister was newly created, the proxy + // canister is its only controller. Sync steps (e.g. asset uploads to a frontend + // canister) will fail because the user's identity lacks the required permissions. + // The fix is to make a proxy call to the frontend canister's `grant_permission` + // method to permit the user identity to upload assets directly before syncing. info!("Syncing canisters:"); sync_many(ctx.syncer.clone(), agent.clone(), sync_canisters, ctx.debug).await?; diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 6141b26c..2ca9c8b1 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -829,6 +829,7 @@ Deploy a project to an environment Possible values: `auto`, `install`, `reinstall`, `upgrade` * `--subnet ` — The subnet to use for the canisters being deployed +* `--proxy ` — Principal of a proxy canister to route management canister calls through * `--controller ` — One or more controllers for the canisters being deployed. Repeat `--controller` to specify multiple * `--cycles ` — Cycles to fund canister creation. Supports suffixes: k (thousand), m (million), b (billion), t (trillion) From 7a30ebf29a64e0e37431c835478d2b6260fe4995 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 18:18:34 -0400 Subject: [PATCH 09/13] test: add integration tests for --proxy flag across canister commands Cover deploy, install, delete, status, stop, start, settings update, settings sync, and the full snapshot workflow through a proxy canister. Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/tests/canister_delete_tests.rs | 92 ++++++ .../icp-cli/tests/canister_install_tests.rs | 83 ++++++ .../icp-cli/tests/canister_settings_tests.rs | 171 +++++++++++ .../icp-cli/tests/canister_snapshot_tests.rs | 278 ++++++++++++++++++ crates/icp-cli/tests/canister_start_tests.rs | 103 +++++++ crates/icp-cli/tests/canister_status_tests.rs | 61 ++++ crates/icp-cli/tests/canister_stop_tests.rs | 76 +++++ crates/icp-cli/tests/common/context.rs | 17 ++ crates/icp-cli/tests/deploy_tests.rs | 76 +++++ 9 files changed, 957 insertions(+) diff --git a/crates/icp-cli/tests/canister_delete_tests.rs b/crates/icp-cli/tests/canister_delete_tests.rs index c6c79133..6a6f9da2 100644 --- a/crates/icp-cli/tests/canister_delete_tests.rs +++ b/crates/icp-cli/tests/canister_delete_tests.rs @@ -116,3 +116,95 @@ async fn canister_delete() { .failure() .stderr(contains("could not find ID for canister")); } + +#[tokio::test] +async fn canister_delete_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify canister ID exists in id store + let id_mapping_path = project_dir + .join(".icp") + .join("cache") + .join("mappings") + .join("random-environment.ids.json"); + let id_mapping_before = + std::fs::read_to_string(&id_mapping_path).expect("ID mapping file should exist"); + assert!( + id_mapping_before.contains("my-canister"), + "ID mapping should contain my-canister before deletion" + ); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Delete canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "delete", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister ID is removed from the id store + let id_mapping_after = + std::fs::read_to_string(&id_mapping_path).expect("ID mapping file should still exist"); + assert!( + !id_mapping_after.contains("my-canister"), + "ID mapping should NOT contain my-canister after deletion" + ); +} diff --git a/crates/icp-cli/tests/canister_install_tests.rs b/crates/icp-cli/tests/canister_install_tests.rs index 479dc870..9bed9d1c 100644 --- a/crates/icp-cli/tests/canister_install_tests.rs +++ b/crates/icp-cli/tests/canister_install_tests.rs @@ -1116,3 +1116,86 @@ async fn canister_install_upgrade_rejects_incompatible_candid() { .success() .stdout(eq("(\"Hello, 42!\")").trim()); } + +#[tokio::test] +async fn canister_install_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Build canister + ctx.icp() + .current_dir(&project_dir) + .args(["build", "my-canister"]) + .assert() + .success(); + + // Create canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "create", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Install canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "install", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister works + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "greet", + "(\"test\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(eq("(\"Hello, test!\")").trim()); +} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 16300351..5f79cef4 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -318,6 +318,87 @@ fn get_principal(client: &icp_cli::Client<'_>, identity: &str) -> String { client.get_principal(identity).to_string() } +#[tokio::test] +async fn canister_settings_update_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let client = clients::icp(&ctx, &project_dir, None); + let principal_alice = get_principal(&client, "alice"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Add controller through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + "--add-controller", + principal_alice.as_str(), + ]) + .assert() + .success(); + + // Verify the controller was added + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains(&proxy_cid)) + .and(contains(principal_alice.as_str())), + ); +} + #[tokio::test] async fn canister_settings_update_log_visibility() { let ctx = TestContext::new(); @@ -1293,3 +1374,93 @@ async fn canister_settings_sync_log_visibility() { sync(&ctx, &project_dir); confirm_log_visibility(&ctx, &project_dir, "Allowed viewers: 2vxsx-fae, aaaaa-aa"); } + +#[tokio::test] +async fn canister_settings_sync_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Update manifest with memory_allocation setting + let pm_with_settings = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + settings: + memory_allocation: 10485760 + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm_with_settings) + .expect("failed to write project manifest"); + + // Sync settings through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "sync", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify the setting was applied + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Memory allocation: 10_485_760")); +} diff --git a/crates/icp-cli/tests/canister_snapshot_tests.rs b/crates/icp-cli/tests/canister_snapshot_tests.rs index 0cb69e2d..04f97f6e 100644 --- a/crates/icp-cli/tests/canister_snapshot_tests.rs +++ b/crates/icp-cli/tests/canister_snapshot_tests.rs @@ -1495,3 +1495,281 @@ async fn canister_migrate_id() { .assert() .failure(); } + +/// Tests the full snapshot workflow through a proxy: create -> list -> restore -> delete +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_snapshot_workflow_through_proxy() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("icp"); + + ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + init_args: "(opt 1 : opt nat8)" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy with initial value 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "my-canister", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify initial value is 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"1\"")); + + // Stop canister before creating snapshot + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Create a snapshot through proxy + let create_output = ctx + .icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "create", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let create_output_str = String::from_utf8_lossy(&create_output); + assert!( + create_output_str.contains("Created snapshot"), + "Expected 'Created snapshot' in output, got: {}", + create_output_str + ); + + let snapshot_id = create_output_str + .lines() + .find(|line| line.contains("Created snapshot")) + .and_then(|line| line.split_whitespace().nth(2)) + .expect("Could not extract snapshot ID from output"); + + // List snapshots through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "list", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains(snapshot_id)); + + // Start, reinstall with new value, then restore from snapshot + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "install", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + "--mode", + "reinstall", + "--args", + "(opt 99 : opt nat8)", + ]) + .assert() + .success(); + + // Verify value is now 99 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"99\"")); + + // Stop and restore from snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "restore", + "my-canister", + snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Restored canister")); + + // Start and verify value is back to 1 + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "get", + "()", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("\"1\"")); + + // Delete the snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "delete", + "my-canister", + snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Deleted snapshot")); + + // List snapshots - should be empty + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "list", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("No snapshots found")); +} diff --git a/crates/icp-cli/tests/canister_start_tests.rs b/crates/icp-cli/tests/canister_start_tests.rs index 48861772..e73a8ce5 100644 --- a/crates/icp-cli/tests/canister_start_tests.rs +++ b/crates/icp-cli/tests/canister_start_tests.rs @@ -120,3 +120,106 @@ async fn canister_start() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_start_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify stopped + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Stopped")); + + // Start canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "start", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify running + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Running")); +} diff --git a/crates/icp-cli/tests/canister_status_tests.rs b/crates/icp-cli/tests/canister_status_tests.rs index 71437a35..c6dabfb2 100644 --- a/crates/icp-cli/tests/canister_status_tests.rs +++ b/crates/icp-cli/tests/canister_status_tests.rs @@ -76,3 +76,64 @@ async fn canister_status() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_status_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Query status through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains("Status: Running")) + .and(contains(&proxy_cid)), + ); +} diff --git a/crates/icp-cli/tests/canister_stop_tests.rs b/crates/icp-cli/tests/canister_stop_tests.rs index 70d30074..3f4a0330 100644 --- a/crates/icp-cli/tests/canister_stop_tests.rs +++ b/crates/icp-cli/tests/canister_stop_tests.rs @@ -88,3 +88,79 @@ async fn canister_stop() { .and(contains("Controllers: 2vxsx-fae")), ); } + +#[tokio::test] +async fn canister_stop_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Stop canister through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "stop", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Verify canister is stopped via status through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout( + starts_with("Canister Id:") + .and(contains("Status: Stopped")) + .and(contains(&proxy_cid)), + ); +} diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index 3f5a7f36..1b641f8c 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -444,6 +444,23 @@ impl TestContext { .as_ref() } + /// Get the proxy canister principal from network status JSON output. + pub(crate) fn get_proxy_cid(&self, project_dir: &Path, network: &str) -> String { + let output = self + .icp() + .current_dir(project_dir) + .args(["network", "status", network, "--json"]) + .output() + .expect("failed to get network status"); + let status_json: serde_json::Value = + serde_json::from_slice(&output.stdout).expect("failed to parse network status JSON"); + status_json + .get("proxy_canister_principal") + .and_then(|v| v.as_str()) + .expect("proxy canister principal not found in network status") + .to_string() + } + pub(crate) fn docker_pull_network(&self) { self.docker_pull_image("ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0"); } diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 37e05271..327b1cc4 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -1,6 +1,7 @@ use indoc::{formatdoc, indoc}; use predicates::{ ord::eq, + prelude::PredicateBooleanExt, str::{PredicateStrExt, contains}, }; use test_tag::tag; @@ -683,3 +684,78 @@ async fn deploy_cloud_engine() { .success() .stdout(eq("(\"Hello, test!\")").trim()); } + +#[tokio::test] +async fn deploy_through_proxy() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + 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"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Verify canister works by calling it through proxy (proxy is the controller) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "my-canister", + "greet", + "(\"proxy\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(eq("(\"Hello, proxy!\")").trim()); + + // Verify canister status through proxy shows the proxy as controller + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "status", + "my-canister", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Status: Running").and(contains(&proxy_cid))); +} From 8f17032e02ea3323141f04626ae89966b1fd4820 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 18:18:39 -0400 Subject: [PATCH 10/13] test: add ignored integration test for canister logs --proxy fetch_canister_logs is not yet available in replicated mode, so hide the --proxy flag from --help and mark the test as #[ignore] until the IC spec change lands (https://github.com/dfinity/portal/pull/6106). Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/icp-cli/src/commands/canister/logs.rs | 5 +- crates/icp-cli/tests/canister_logs_tests.rs | 79 ++++++++++++++++++++ docs/reference/cli.md | 1 - 3 files changed, 83 insertions(+), 2 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/logs.rs b/crates/icp-cli/src/commands/canister/logs.rs index 7d32cb96..78c66ac4 100644 --- a/crates/icp-cli/src/commands/canister/logs.rs +++ b/crates/icp-cli/src/commands/canister/logs.rs @@ -51,7 +51,10 @@ pub(crate) struct LogsArgs { pub(crate) json: bool, /// Principal of a proxy canister to route the management canister call through. - #[arg(long)] + /// + /// Hidden until the IC supports fetch_canister_logs in replicated mode. + /// Tracking: https://github.com/dfinity/portal/pull/6106 + #[arg(long, hide = true)] pub(crate) proxy: Option, } diff --git a/crates/icp-cli/tests/canister_logs_tests.rs b/crates/icp-cli/tests/canister_logs_tests.rs index b84484b7..49484f91 100644 --- a/crates/icp-cli/tests/canister_logs_tests.rs +++ b/crates/icp-cli/tests/canister_logs_tests.rs @@ -365,3 +365,82 @@ async fn canister_logs_filter_by_timestamp() { .success() .stdout(contains("Timestamped message")); } + +// Ignored: fetch_canister_logs is not yet available in replicated mode. +// Tracking: https://github.com/dfinity/portal/pull/6106 +#[ignore] +#[cfg(unix)] // moc +#[tokio::test] +async fn canister_logs_through_proxy() { + let ctx = TestContext::new(); + let project_dir = ctx.create_project_dir("canister_logs"); + + ctx.copy_asset_dir("canister_logs", &project_dir); + + let pm = formatdoc! {r#" + canisters: + - name: logger + recipe: + type: "@dfinity/motoko@v4.0.0" + configuration: + main: main.mo + args: "" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + let proxy_cid = ctx.get_proxy_cid(&project_dir, "random-network"); + + // Deploy logger through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "logger", + "--proxy", + &proxy_cid, + "--environment", + "random-environment", + ]) + .assert() + .success(); + + // Create some logs by calling through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "logger", + "log", + "(\"Proxy log message\")", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + + // Fetch logs through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "logs", + "logger", + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stdout(contains("Proxy log message")); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2ca9c8b1..bc35d4f6 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -350,7 +350,6 @@ Fetch and display canister logs * `--since-index ` — Show logs at or after this log index (inclusive). Cannot be used with --follow * `--until-index ` — Show logs before this log index (exclusive). Cannot be used with --follow * `--json` — Output command results as JSON -* `--proxy ` — Principal of a proxy canister to route the management canister call through From a9716f1d5072758450766922cc1b7a0f18248a29 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 19:13:50 -0400 Subject: [PATCH 11/13] fix: warn on effective controller removal in canister settings update When --proxy is set, the proxy canister is the effective controller making management calls. The self-removal warning now checks the proxy principal instead of the caller's identity, preventing false prompts (and "not a terminal" errors in tests) when adding controllers to proxy-deployed canisters. Warning messages are also updated to be accurate for each case. Co-Authored-By: Claude Sonnet 4.6 --- .../src/commands/canister/settings/update.rs | 20 +++++++++++++++---- .../icp-cli/tests/canister_settings_tests.rs | 4 ++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/settings/update.rs b/crates/icp-cli/src/commands/canister/settings/update.rs index 56eaac51..46c8a5f5 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -191,13 +191,25 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), anyhow: if let Some(controllers_opt) = &args.controllers { controllers = get_controllers(controllers_opt, current_status.as_ref()); - // Check if the caller is being removed from controllers + // Check if the effective controller is being removed from the controller list. + // When --proxy is set, the proxy canister is the one making management calls and + // is the effective controller. Without --proxy, it's the caller's identity. + let effective_controller = args.proxy.unwrap_or(caller_principal); if let Some(new_controllers) = &controllers - && !new_controllers.contains(&caller_principal) + && !new_controllers.contains(&effective_controller) && !args.force { - warn!("You are about to remove yourself from the controllers list."); - warn!("This will cause you to lose control of the canister and cannot be undone."); + if args.proxy.is_some() { + warn!( + "You are about to remove the proxy canister ({effective_controller}) from the controllers list." + ); + warn!( + "This will prevent further management calls through this proxy and cannot be undone." + ); + } else { + warn!("You are about to remove yourself from the controllers list."); + warn!("This will cause you to lose control of the canister and cannot be undone."); + } let confirmed = Confirm::new() .with_prompt("Do you want to proceed?") diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index 5f79cef4..d01d9fe1 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -389,6 +389,8 @@ async fn canister_settings_update_through_proxy() { "my-canister", "--environment", "random-environment", + "--proxy", + &proxy_cid, ]) .assert() .success() @@ -1459,6 +1461,8 @@ async fn canister_settings_sync_through_proxy() { "my-canister", "--environment", "random-environment", + "--proxy", + &proxy_cid, ]) .assert() .success() From 7afcedb9d75348af8378732b753830f0fafc4574 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 19:38:25 -0400 Subject: [PATCH 12/13] feat: add --proxy flag to snapshot download and upload commands Thread proxy through all snapshot transfer operations so that snapshot download/upload can be routed through a proxy canister. Also extend the proxy workflow integration test to cover these commands. Co-Authored-By: Claude Sonnet 4.6 --- .../commands/canister/snapshot/download.rs | 20 ++++- .../src/commands/canister/snapshot/upload.rs | 22 +++++- .../src/operations/snapshot_transfer.rs | 37 ++++++---- .../icp-cli/tests/canister_snapshot_tests.rs | 74 ++++++++++++++++++- 4 files changed, 131 insertions(+), 22 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/snapshot/download.rs b/crates/icp-cli/src/commands/canister/snapshot/download.rs index 35b52940..471a3455 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/download.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/download.rs @@ -1,4 +1,5 @@ use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use icp::context::Context; use icp::prelude::*; @@ -29,6 +30,10 @@ pub(crate) struct DownloadArgs { /// Resume a previously interrupted download #[arg(long)] resume: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyhow::Error> { @@ -84,7 +89,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho id = hex::encode(snapshot_id), ); - let metadata = read_snapshot_metadata(&agent, cid, snapshot_id).await?; + let metadata = read_snapshot_metadata(&agent, args.proxy, cid, snapshot_id).await?; info!( " Timestamp: {}", @@ -119,6 +124,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho let pb = create_transfer_progress_bar(metadata.wasm_module_size, "WASM module"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::WasmModule, @@ -140,6 +146,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho let pb = create_transfer_progress_bar(metadata.wasm_memory_size, "WASM memory"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::WasmMemory, @@ -165,6 +172,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho create_transfer_progress_bar(metadata.stable_memory_size, "Stable memory"); download_blob_to_file( &agent, + args.proxy, cid, snapshot_id, BlobType::StableMemory, @@ -193,7 +201,15 @@ pub(crate) async fn exec(ctx: &Context, args: &DownloadArgs) -> Result<(), anyho for chunk_hash in &metadata.wasm_chunk_store { let chunk_path = paths.wasm_chunk_path(&chunk_hash.hash); if !chunk_path.exists() { - download_wasm_chunk(&agent, cid, snapshot_id, chunk_hash, paths).await?; + download_wasm_chunk( + &agent, + args.proxy, + cid, + snapshot_id, + chunk_hash, + paths, + ) + .await?; } } info!("WASM chunks: done"); diff --git a/crates/icp-cli/src/commands/canister/snapshot/upload.rs b/crates/icp-cli/src/commands/canister/snapshot/upload.rs index bb93e263..81bdda76 100644 --- a/crates/icp-cli/src/commands/canister/snapshot/upload.rs +++ b/crates/icp-cli/src/commands/canister/snapshot/upload.rs @@ -1,6 +1,7 @@ use std::io::stdout; use byte_unit::{Byte, UnitType}; +use candid::Principal; use clap::Args; use icp::context::Context; use icp::prelude::*; @@ -41,6 +42,10 @@ pub(crate) struct UploadArgs { /// Suppress human-readable output; print only snapshot ID #[arg(long, short, conflicts_with = "json")] quiet: bool, + + /// Principal of a proxy canister to route the management canister calls through. + #[arg(long)] + proxy: Option, } pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow::Error> { @@ -103,7 +108,8 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: // Upload metadata to create a new snapshot let replace_snapshot = args.replace.as_ref().map(|s| s.0.as_slice()); let result = - upload_snapshot_metadata(&agent, cid, &metadata, replace_snapshot).await?; + upload_snapshot_metadata(&agent, args.proxy, cid, &metadata, replace_snapshot) + .await?; let snapshot_id_hex = hex::encode(&result.snapshot_id); info!("Created snapshot {snapshot_id_hex} for upload"); @@ -123,6 +129,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: let pb = create_transfer_progress_bar(metadata.wasm_module_size, "WASM module"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::WasmModule, @@ -143,6 +150,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: let pb = create_transfer_progress_bar(metadata.wasm_memory_size, "WASM memory"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::WasmMemory, @@ -164,6 +172,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: create_transfer_progress_bar(metadata.stable_memory_size, "Stable memory"); upload_blob_from_file( &agent, + args.proxy, cid, &snapshot_id_bytes, BlobType::StableMemory, @@ -188,8 +197,15 @@ pub(crate) async fn exec(ctx: &Context, args: &UploadArgs) -> Result<(), anyhow: for chunk_hash in &metadata.wasm_chunk_store { let hash_hex = hex::encode(&chunk_hash.hash); if !progress.wasm_chunks_uploaded.contains(&hash_hex) { - upload_wasm_chunk(&agent, cid, &snapshot_id_bytes, &chunk_hash.hash, paths) - .await?; + upload_wasm_chunk( + &agent, + args.proxy, + cid, + &snapshot_id_bytes, + &chunk_hash.hash, + paths, + ) + .await?; progress.wasm_chunks_uploaded.insert(hash_hex); save_upload_progress(&progress, paths)?; } diff --git a/crates/icp-cli/src/operations/snapshot_transfer.rs b/crates/icp-cli/src/operations/snapshot_transfer.rs index 44692769..24a6388d 100644 --- a/crates/icp-cli/src/operations/snapshot_transfer.rs +++ b/crates/icp-cli/src/operations/snapshot_transfer.rs @@ -440,6 +440,7 @@ pub fn create_transfer_progress_bar(total_bytes: u64, label: &str) -> ProgressBa /// Read snapshot metadata from a canister. pub async fn read_snapshot_metadata( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], ) -> Result { @@ -450,7 +451,7 @@ pub async fn read_snapshot_metadata( let metadata = with_retry(|| { let args = args.clone(); - async move { proxy_management::read_canister_snapshot_metadata(agent, None, args).await } + async move { proxy_management::read_canister_snapshot_metadata(agent, proxy, args).await } }) .await .context(ReadMetadataSnafu { canister_id })?; @@ -461,6 +462,7 @@ pub async fn read_snapshot_metadata( /// Upload snapshot metadata to create a new snapshot. pub async fn upload_snapshot_metadata( agent: &Agent, + proxy: Option, canister_id: Principal, metadata: &ReadCanisterSnapshotMetadataResult, replace_snapshot: Option<&[u8]>, @@ -489,7 +491,7 @@ pub async fn upload_snapshot_metadata( let result = with_retry(|| { let args = args.clone(); - async move { proxy_management::upload_canister_snapshot_metadata(agent, None, args).await } + async move { proxy_management::upload_canister_snapshot_metadata(agent, proxy, args).await } }) .await .context(UploadMetadataSnafu { canister_id })?; @@ -504,6 +506,7 @@ pub async fn upload_snapshot_metadata( /// The agent handles rate limiting and semaphoring internally. pub async fn download_blob_to_file( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], blob_type: BlobType, @@ -563,17 +566,16 @@ pub async fn download_blob_to_file( }; in_progress.push(async move { - let result = - with_retry(|| { - let args = args.clone(); - async move { - proxy_management::read_canister_snapshot_data(agent, None, args).await - } - }) - .await - .context(ReadDataChunkSnafu { - offset: chunk_offset, - })?; + let result = with_retry(|| { + let args = args.clone(); + async move { + proxy_management::read_canister_snapshot_data(agent, proxy, args).await + } + }) + .await + .context(ReadDataChunkSnafu { + offset: chunk_offset, + })?; Ok::<_, SnapshotTransferError>((chunk_offset, result.chunk)) }); } @@ -619,6 +621,7 @@ pub async fn download_blob_to_file( /// Download a single WASM chunk by hash. pub async fn download_wasm_chunk( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], chunk_hash: &ChunkHash, @@ -637,7 +640,7 @@ pub async fn download_wasm_chunk( let result = with_retry(|| { let args = args.clone(); - async move { proxy_management::read_canister_snapshot_data(agent, None, args).await } + async move { proxy_management::read_canister_snapshot_data(agent, proxy, args).await } }) .await .context(ReadWasmChunkSnafu { hash: &hash_hex })?; @@ -654,6 +657,7 @@ pub async fn download_wasm_chunk( /// Returns the final byte offset after all uploads complete. pub async fn upload_blob_from_file( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], blob_type: BlobType, @@ -715,7 +719,7 @@ pub async fn upload_blob_from_file( with_retry(|| { let args = args.clone(); async move { - proxy_management::upload_canister_snapshot_data(agent, None, args).await + proxy_management::upload_canister_snapshot_data(agent, proxy, args).await } }) .await @@ -771,6 +775,7 @@ pub async fn upload_blob_from_file( /// Upload a single WASM chunk. pub async fn upload_wasm_chunk( agent: &Agent, + proxy: Option, canister_id: Principal, snapshot_id: &[u8], chunk_hash: &[u8], @@ -790,7 +795,7 @@ pub async fn upload_wasm_chunk( with_retry(|| { let args = args.clone(); - async move { proxy_management::upload_canister_snapshot_data(agent, None, args).await } + async move { proxy_management::upload_canister_snapshot_data(agent, proxy, args).await } }) .await .context(UploadWasmChunkSnafu { hash: hash_hex })?; diff --git a/crates/icp-cli/tests/canister_snapshot_tests.rs b/crates/icp-cli/tests/canister_snapshot_tests.rs index 04f97f6e..49c18cb6 100644 --- a/crates/icp-cli/tests/canister_snapshot_tests.rs +++ b/crates/icp-cli/tests/canister_snapshot_tests.rs @@ -1496,12 +1496,13 @@ async fn canister_migrate_id() { .failure(); } -/// Tests the full snapshot workflow through a proxy: create -> list -> restore -> delete +/// Tests the full snapshot workflow through a proxy: create -> list -> download -> upload -> restore -> delete #[cfg(unix)] // moc #[tokio::test] async fn canister_snapshot_workflow_through_proxy() { let ctx = TestContext::new(); let project_dir = ctx.create_project_dir("icp"); + let snapshot_dir = ctx.create_project_dir("snapshot"); ctx.copy_asset_dir("echo_init_arg_canister", &project_dir); @@ -1623,6 +1624,77 @@ async fn canister_snapshot_workflow_through_proxy() { .success() .stdout(contains(snapshot_id)); + // Download the snapshot through proxy + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "download", + "my-canister", + snapshot_id, + "--output", + snapshot_dir.as_str(), + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .stderr(contains("Snapshot downloaded")); + + assert!( + snapshot_dir.join("metadata.json").exists(), + "metadata.json should exist after download" + ); + + // Upload the snapshot back through proxy + let upload_output = ctx + .icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "upload", + "my-canister", + "--input", + snapshot_dir.as_str(), + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success() + .get_output() + .stdout + .clone(); + + let uploaded_snapshot_id = String::from_utf8_lossy(&upload_output) + .lines() + .find(|line| line.contains("uploaded successfully")) + .and_then(|line| line.split_whitespace().nth(1)) + .expect("Could not extract uploaded snapshot ID") + .to_string(); + + // Delete the uploaded snapshot (cleanup) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "snapshot", + "delete", + "my-canister", + &uploaded_snapshot_id, + "--environment", + "random-environment", + "--proxy", + &proxy_cid, + ]) + .assert() + .success(); + // Start, reinstall with new value, then restore from snapshot ctx.icp() .current_dir(&project_dir) From ac87d25b6e165d14c9803515d5d18ecb479fb3a1 Mon Sep 17 00:00:00 2001 From: Linwei Shang Date: Thu, 2 Apr 2026 19:42:22 -0400 Subject: [PATCH 13/13] docs: update CLI reference for snapshot download/upload --proxy flag Co-Authored-By: Claude Sonnet 4.6 --- docs/reference/cli.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/reference/cli.md b/docs/reference/cli.md index bc35d4f6..62fe873e 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -578,6 +578,7 @@ Download a snapshot to local disk * `--identity ` — The user identity to run this command as * `-o`, `--output ` — Output directory for the snapshot files * `--resume` — Resume a previously interrupted download +* `--proxy ` — Principal of a proxy canister to route the management canister calls through @@ -645,6 +646,7 @@ Upload a snapshot from local disk * `--resume` — Resume a previously interrupted upload * `--json` — Output command results as JSON * `-q`, `--quiet` — Suppress human-readable output; print only snapshot ID +* `--proxy ` — Principal of a proxy canister to route the management canister calls through