diff --git a/crates/icp-cli/src/commands/canister/mod.rs b/crates/icp-cli/src/commands/canister/mod.rs index e85e6a468..0657d1edb 100644 --- a/crates/icp-cli/src/commands/canister/mod.rs +++ b/crates/icp-cli/src/commands/canister/mod.rs @@ -34,6 +34,7 @@ pub(crate) enum Command { /// List the canisters in an environment List(list::ListArgs), + /// Commands to manage canister settings #[command(subcommand)] Settings(settings::Command), diff --git a/crates/icp-cli/src/commands/canister/settings/mod.rs b/crates/icp-cli/src/commands/canister/settings/mod.rs index 54e07c6d4..d91b177ce 100644 --- a/crates/icp-cli/src/commands/canister/settings/mod.rs +++ b/crates/icp-cli/src/commands/canister/settings/mod.rs @@ -1,11 +1,16 @@ use clap::Subcommand; pub(crate) mod show; +pub(crate) mod sync; pub(crate) mod update; #[derive(Subcommand, Debug)] #[allow(clippy::large_enum_variant)] pub(crate) enum Command { + /// Display a canister's settings Show(show::ShowArgs), + /// Change a canister's settings to specified values Update(update::UpdateArgs), + /// Synchronize a canister's settings with those defined in the project + Sync(sync::SyncArgs), } diff --git a/crates/icp-cli/src/commands/canister/settings/sync.rs b/crates/icp-cli/src/commands/canister/settings/sync.rs new file mode 100644 index 000000000..4e1a79742 --- /dev/null +++ b/crates/icp-cli/src/commands/canister/settings/sync.rs @@ -0,0 +1,60 @@ +use crate::{ + commands::args::CanisterCommandArgs, operations::settings::SyncSettingsOperationError, +}; + +use clap::Args; +use ic_utils::interfaces::ManagementCanister; +use icp::context::{ + CanisterSelection, Context, GetCanisterIdAndAgentError, GetEnvCanisterError, + GetEnvironmentError, +}; +use snafu::Snafu; + +#[derive(Debug, Args)] +pub(crate) struct SyncArgs { + #[command(flatten)] + cmd_args: CanisterCommandArgs, +} + +#[derive(Debug, Snafu)] +pub(crate) enum CommandError { + #[snafu(transparent)] + GetIdAndAgent { source: GetCanisterIdAndAgentError }, + + #[snafu(transparent)] + GetEnvironment { source: GetEnvironmentError }, + + #[snafu(display("Canister name must be used for settings sync"))] + PrincipalCanister, + + #[snafu(transparent)] + GetEnvCanister { source: GetEnvCanisterError }, + + #[snafu(transparent)] + SyncSettingsError { source: SyncSettingsOperationError }, +} + +pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), CommandError> { + let selections = args.cmd_args.selections(); + let CanisterSelection::Named(name) = &selections.canister else { + return PrincipalCanisterSnafu.fail(); + }; + + let (_, canister) = ctx + .get_canister_and_path_for_env(name, &selections.environment) + .await?; + + let (cid, agent) = ctx + .get_canister_id_and_agent( + &selections.canister, + &selections.environment, + &selections.network, + &selections.identity, + ) + .await?; + + let mgmt = ManagementCanister::create(&agent); + + crate::operations::settings::sync_settings(&mgmt, &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 74e8dea1c..fa7555cce 100644 --- a/crates/icp-cli/src/commands/canister/settings/update.rs +++ b/crates/icp-cli/src/commands/canister/settings/update.rs @@ -1,12 +1,15 @@ use std::collections::{HashMap, HashSet}; +use std::io::Write; use byte_unit::{Byte, Unit}; use clap::{ArgAction, Args}; +use console::Term; use ic_agent::{AgentError, export::Principal}; use ic_management_canister_types::{CanisterStatusResult, EnvironmentVariable, LogVisibility}; -use icp::{agent, identity, network}; +use icp::{LoadError, agent, identity, network}; -use icp::context::{Context, GetCanisterIdAndAgentError}; +use icp::context::{CanisterSelection, Context, GetCanisterIdAndAgentError}; +use snafu::{ResultExt, Snafu}; use crate::commands::args; use icp::store_id::LookupIdError; @@ -104,31 +107,34 @@ pub(crate) struct UpdateArgs { environment_variables: Option, } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Snafu)] pub(crate) enum CommandError { - #[error(transparent)] - Project(#[from] icp::LoadError), + #[snafu(transparent)] + Project { source: icp::LoadError }, - #[error(transparent)] - Identity(#[from] identity::LoadError), + #[snafu(transparent)] + Identity { source: identity::LoadError }, - #[error(transparent)] - Access(#[from] network::AccessError), + #[snafu(transparent)] + Access { source: network::AccessError }, - #[error(transparent)] - Agent(#[from] agent::CreateAgentError), + #[snafu(transparent)] + Agent { source: agent::CreateAgentError }, - #[error("invalid environment variable '{variable}'")] + #[snafu(display("invalid environment variable '{variable}'"))] InvalidEnvironmentVariable { variable: String }, - #[error(transparent)] - Lookup(#[from] LookupIdError), + #[snafu(transparent)] + Lookup { source: LookupIdError }, - #[error(transparent)] - Update(#[from] AgentError), + #[snafu(transparent)] + Update { source: AgentError }, - #[error(transparent)] - GetCanisterIdAndAgent(#[from] GetCanisterIdAndAgentError), + #[snafu(transparent)] + GetCanisterIdAndAgent { source: GetCanisterIdAndAgentError }, + + #[snafu(display("failed to write to terminal"))] + WriteTerm { source: std::io::Error }, } pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), CommandError> { @@ -142,6 +148,16 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command ) .await?; + let configured_settings = if let CanisterSelection::Named(name) = &selections.canister { + match ctx.project.load().await { + Ok(p) => p.canisters[name].1.settings.clone(), + Err(LoadError::Locate) => <_>::default(), + Err(e) => return Err(CommandError::Project { source: e }), + } + } else { + <_>::default() + }; + // Management Interface let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent); @@ -169,6 +185,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command // Handle environment variables. let mut environment_variables: Option> = None; if let Some(environment_variables_opt) = &args.environment_variables { + maybe_warn_on_env_vars_change(&ctx.term, &configured_settings, environment_variables_opt)?; environment_variables = get_environment_variables(environment_variables_opt, current_status.as_ref()); } @@ -181,21 +198,51 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command } } if let Some(compute_allocation) = args.compute_allocation { + if configured_settings.compute_allocation.is_some() { + ctx.term.write_line( + "Warning: Compute allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_compute_allocation(compute_allocation); } if let Some(memory_allocation) = args.memory_allocation { + if configured_settings.memory_allocation.is_some() { + ctx.term.write_line( + "Warning: Memory allocation is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_memory_allocation(memory_allocation.as_u64()); } if let Some(freezing_threshold) = args.freezing_threshold { + if configured_settings.freezing_threshold.is_some() { + ctx.term.write_line( + "Warning: Freezing threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_freezing_threshold(freezing_threshold); } if let Some(reserved_cycles_limit) = args.reserved_cycles_limit { + if configured_settings.reserved_cycles_limit.is_some() { + ctx.term.write_line( + "Warning: Reserved cycles limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_reserved_cycles_limit(reserved_cycles_limit); } if let Some(wasm_memory_limit) = args.wasm_memory_limit { + if configured_settings.wasm_memory_limit.is_some() { + ctx.term.write_line( + "Warning: Wasm memory limit is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_wasm_memory_limit(wasm_memory_limit.as_u64()); } if let Some(wasm_memory_threshold) = args.wasm_memory_threshold { + if configured_settings.wasm_memory_threshold.is_some() { + ctx.term.write_line( + "Warning: Wasm memory threshold is already set in icp.yaml; this new value will be overridden on next settings sync" + ).context(WriteTermSnafu)? + } update = update.with_wasm_memory_threshold(wasm_memory_threshold.as_u64()); } if let Some(log_visibility) = log_visibility { @@ -395,3 +442,35 @@ fn get_environment_variables( None } + +fn maybe_warn_on_env_vars_change( + mut term: &Term, + configured_settings: &icp::canister::Settings, + environment_variables_opt: &EnvironmentVariableOpt, +) -> Result<(), CommandError> { + if let Some(configured_vars) = &configured_settings.environment_variables { + if let Some(to_add) = &environment_variables_opt.add_environment_variable { + for add_var in to_add { + if configured_vars.contains_key(&add_var.name) { + writeln!( + term, + "Warning: Environment variable '{}' is already set in icp.yaml; this new value will be overridden on next settings sync", + add_var.name + ).context(WriteTermSnafu)?; + } + } + } + if let Some(to_remove) = &environment_variables_opt.remove_environment_variable { + for remove_var in to_remove { + if configured_vars.contains_key(remove_var) { + writeln!( + term, + "Warning: Environment variable '{}' is already set in icp.yaml; removing it here will be overridden on next settings sync", + remove_var + ).context(WriteTermSnafu)?; + } + } + } + } + Ok(()) +} diff --git a/crates/icp-cli/src/commands/deploy/mod.rs b/crates/icp-cli/src/commands/deploy/mod.rs index 50c0fad5c..9f962fd61 100644 --- a/crates/icp-cli/src/commands/deploy/mod.rs +++ b/crates/icp-cli/src/commands/deploy/mod.rs @@ -15,6 +15,7 @@ use crate::{ build::build_many_with_progress_bar, create::CreateOperation, install::{InstallOperationError, install_many}, + settings::sync_settings_many, sync::{SyncOperationError, sync_many}, }, options::{EnvironmentOpt, IdentityOpt}, @@ -225,13 +226,17 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), Command set_binding_env_vars_many( agent.clone(), &env.name, - target_canisters, + target_canisters.clone(), canister_list, ctx.debug, ) .await .map_err(|e| anyhow!(e))?; + sync_settings_many(agent.clone(), target_canisters, ctx.debug) + .await + .map_err(|e| anyhow!(e))?; + // Install the selected canisters let _ = ctx.term.write_line("\n\nInstalling canisters:"); diff --git a/crates/icp-cli/src/main.rs b/crates/icp-cli/src/main.rs index 4bdf8a65b..813d2c2c1 100644 --- a/crates/icp-cli/src/main.rs +++ b/crates/icp-cli/src/main.rs @@ -182,6 +182,12 @@ async fn main() -> Result<(), Error> { .instrument(trace_span) .await? } + + commands::canister::settings::Command::Sync(args) => { + commands::canister::settings::sync::exec(&ctx, &args) + .instrument(trace_span) + .await? + } }, commands::canister::Command::Show(args) => { diff --git a/crates/icp-cli/src/operations/mod.rs b/crates/icp-cli/src/operations/mod.rs index 1f59b79eb..af173665f 100644 --- a/crates/icp-cli/src/operations/mod.rs +++ b/crates/icp-cli/src/operations/mod.rs @@ -2,4 +2,5 @@ pub(crate) mod binding_env_vars; pub(crate) mod build; pub(crate) mod create; pub(crate) mod install; +pub(crate) mod settings; pub(crate) mod sync; diff --git a/crates/icp-cli/src/operations/settings.rs b/crates/icp-cli/src/operations/settings.rs new file mode 100644 index 000000000..c9688ee18 --- /dev/null +++ b/crates/icp-cli/src/operations/settings.rs @@ -0,0 +1,138 @@ +use std::collections::HashMap; + +use candid::Principal; +use futures::{StreamExt, stream::FuturesOrdered}; +use ic_agent::{Agent, AgentError}; +use ic_management_canister_types::EnvironmentVariable; +use ic_utils::interfaces::ManagementCanister; +use icp::{Canister, canister::Settings}; +use itertools::Itertools; +use snafu::{ResultExt, Snafu}; + +use crate::progress::{ProgressManager, ProgressManagerSettings}; + +#[derive(Debug, Snafu)] +pub(crate) enum SyncSettingsOperationError { + #[snafu(display("failed to fetch current canister settings for canister {canister}"))] + FetchCurrentSettings { + source: AgentError, + 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, + canister: Principal, + }, +} + +pub(crate) async fn sync_settings( + mgmt: &ManagementCanister<'_>, + cid: &Principal, + canister: &Canister, +) -> Result<(), SyncSettingsOperationError> { + let (status,) = mgmt + .canister_status(cid) + .await + .context(FetchCurrentSettingsSnafu { canister: *cid })?; + let &Settings { + compute_allocation, + memory_allocation, + freezing_threshold, + reserved_cycles_limit, + wasm_memory_limit, + wasm_memory_threshold, + ref environment_variables, + } = &canister.settings; + let current_settings = status.settings; + + let environment_variable_setting = + if let Some(configured_environment_variables) = &environment_variables { + let mut merged_environment_variables: HashMap<_, _> = current_settings + .environment_variables + .clone() + .into_iter() + .map(|EnvironmentVariable { name, value }| (name, value)) + .collect(); + merged_environment_variables.extend(configured_environment_variables.clone()); + Some( + merged_environment_variables + .into_iter() + .map(|(name, value)| EnvironmentVariable { name, value }) + .collect_vec(), + ) + } else { + None + }; + if compute_allocation.is_none_or(|s| s == current_settings.compute_allocation) + && memory_allocation.is_none_or(|s| s == current_settings.memory_allocation) + && freezing_threshold.is_none_or(|s| s == current_settings.freezing_threshold) + && reserved_cycles_limit.is_none_or(|s| s == current_settings.reserved_cycles_limit) + && wasm_memory_limit.is_none_or(|s| s == current_settings.wasm_memory_limit) + && wasm_memory_threshold.is_none_or(|s| s == current_settings.wasm_memory_threshold) + && environment_variable_setting + .as_ref() + .is_none_or(|s| *s == current_settings.environment_variables) + { + // No changes needed + return Ok(()); + } + mgmt.update_settings(cid) + .with_optional_compute_allocation(compute_allocation) + .with_optional_memory_allocation(memory_allocation) + .with_optional_freezing_threshold(freezing_threshold) + .with_optional_reserved_cycles_limit(reserved_cycles_limit) + .with_optional_wasm_memory_limit(wasm_memory_limit) + .with_optional_wasm_memory_threshold(wasm_memory_threshold) + .with_optional_environment_variables(environment_variable_setting) + .build() + .context(ValidateSettingsSnafu { + name: &canister.name, + })? + .await + .context(UpdateSettingsSnafu { canister: *cid })?; + + Ok(()) +} + +pub(crate) async fn sync_settings_many( + agent: Agent, + target_canisters: Vec<(Principal, Canister)>, + debug: bool, +) -> Result<(), SyncSettingsOperationError> { + let mgmt = ManagementCanister::create(&agent); + + let mut futs = FuturesOrdered::new(); + let progress_manager = ProgressManager::new(ProgressManagerSettings { hidden: debug }); + + for (cid, info) in target_canisters { + let pb = progress_manager.create_progress_bar(&info.name); + + let settings_fn = { + let mgmt = mgmt.clone(); + let pb = pb.clone(); + + async move { + pb.set_message("Updating canister settings..."); + sync_settings(&mgmt, &cid, &info).await + } + }; + + futs.push_back(async move { + ProgressManager::execute_with_progress( + &pb, + settings_fn, + || "Canister settings updated successfully".to_string(), + |err| format!("Failed to update canister settings: {err}"), + ) + .await + }); + } + + while let Some(res) = futs.next().await { + res?; + } + + Ok(()) +} diff --git a/crates/icp-cli/tests/canister_settings_tests.rs b/crates/icp-cli/tests/canister_settings_tests.rs index eed270430..3d6f6410c 100644 --- a/crates/icp-cli/tests/canister_settings_tests.rs +++ b/crates/icp-cli/tests/canister_settings_tests.rs @@ -944,3 +944,187 @@ fn canister_settings_update_environment_variables() { .and(contains("Name: var3, Value: value3").not()), ); } + +#[test] +fn canister_settings_sync() { + let ctx = TestContext::new(); + + // Setup project + let project_dir = ctx.create_project_dir("icp"); + + // Use vendored WASM + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + // Project manifest + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + // Start network + let _g = ctx.start_network_in(&project_dir, "my-network"); + ctx.ping_until_healthy(&project_dir, "my-network"); + + // Deploy project + clients::icp(&ctx, &project_dir, Some("my-environment".to_string())) + .mint_cycles(200 * TRILLION); + + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--subnet", + common::SUBNET_ID, + "--environment", + "my-environment", + ]) + .assert() + .success(); + + // Test helpers for syncing settings and checking wasm memory limit + fn sync(ctx: &TestContext, project_dir: &Path) { + ctx.icp() + .current_dir(project_dir) + .args([ + "canister", + "settings", + "sync", + "my-canister", + "--environment", + "my-environment", + ]) + .assert() + .success(); + } + + fn confirm_wasm_memory_limit(ctx: &TestContext, project_dir: &Path, expected_limit: &str) { + ctx.icp() + .current_dir(project_dir) + .args([ + "canister", + "settings", + "show", + "my-canister", + "--environment", + "my-environment", + ]) + .assert() + .success() + .stderr(contains(format!("Wasm memory limit: {}", expected_limit))); + } + + // Initial value + confirm_wasm_memory_limit(&ctx, &project_dir, "3_221_225_472"); + + let pm_with_empty_settings = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + settings: + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + let pm_with_empty_wasm_limit = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + settings: + wasm_memory_limit: ~ + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + let pm_with_wasm_limit_4gb = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp {wasm} "$ICP_WASM_OUTPUT_PATH" + settings: + wasm_memory_limit: 4000000000 + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + // Syncing a nonexistent setting should not override the default + write_string(&project_dir.join("icp.yaml"), &pm_with_empty_settings) + .expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "3_221_225_472"); + write_string(&project_dir.join("icp.yaml"), &pm_with_empty_wasm_limit) + .expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "3_221_225_472"); + // Setting wasm memory limit in the manifest and syncing should update the canister settings + write_string(&project_dir.join("icp.yaml"), &pm_with_wasm_limit_4gb) + .expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "4_000_000_000"); + // Existing settings should be overridden on sync + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "my-environment", + "--wasm-memory-limit", + "5GiB", + ]) + .assert() + .success() + .stdout(contains("Wasm memory limit is already set in icp.yaml")); + confirm_wasm_memory_limit(&ctx, &project_dir, "5_368_709_120"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "4_000_000_000"); + // Syncing a nonexistent setting should not override a previously set setting + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "4_000_000_000"); + write_string(&project_dir.join("icp.yaml"), &pm_with_empty_settings) + .expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "4_000_000_000"); + write_string(&project_dir.join("icp.yaml"), &pm_with_empty_wasm_limit) + .expect("failed to write project manifest"); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "4_000_000_000"); + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "settings", + "update", + "my-canister", + "--environment", + "my-environment", + "--wasm-memory-limit", + "5GiB", + ]) + .assert() + .success(); + sync(&ctx, &project_dir); + confirm_wasm_memory_limit(&ctx, &project_dir, "5_368_709_120"); +} diff --git a/crates/icp/src/agent.rs b/crates/icp/src/agent.rs index cfb361e89..b8326b868 100644 --- a/crates/icp/src/agent.rs +++ b/crates/icp/src/agent.rs @@ -2,16 +2,20 @@ use std::{sync::Arc, time::Duration}; use async_trait::async_trait; use ic_agent::{Agent, AgentError, Identity}; +use snafu::Snafu; use crate::prelude::*; -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Snafu)] pub enum CreateAgentError { - #[error(transparent)] - Agent(#[from] AgentError), - - #[error(transparent)] - Unexpected(#[from] anyhow::Error), + #[snafu(transparent)] + Agent { + #[snafu(source(from(AgentError, Box::new)))] + source: Box, + }, + + #[snafu(transparent)] + Unexpected { source: anyhow::Error }, } #[async_trait] diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index 45dd9e8b4..b7a83b492 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -11,7 +11,7 @@ use crate::{ network::{Configuration as NetworkConfiguration, access::NetworkAccess}, prelude::*, project::DEFAULT_LOCAL_ENVIRONMENT_NAME, - store_id::IdMapping, + store_id::{IdMapping, LookupIdError}, }; use candid::Principal; use ic_agent::{Agent, Identity}; @@ -478,7 +478,8 @@ pub enum GetCanisterIdForEnvError { environment_name ))] CanisterIdLookup { - source: crate::store_id::LookupIdError, + #[snafu(source(from(LookupIdError, Box::new)))] + source: Box, canister_name: String, environment_name: String, }, diff --git a/crates/icp/src/identity/key.rs b/crates/icp/src/identity/key.rs index bd8f80e7c..ade67a65e 100644 --- a/crates/icp/src/identity/key.rs +++ b/crates/icp/src/identity/key.rs @@ -50,11 +50,16 @@ pub enum LoadIdentityError { #[snafu(display("failed to load PEM file `{path}`: failed to parse"))] ParsePemError { path: PathBuf, - source: pem::PemError, + #[snafu(source(from(pem::PemError, Box::new)))] + source: Box, }, #[snafu(display("failed to load PEM file `{path}`: failed to decipher key"))] - ParseKeyError { path: PathBuf, source: pkcs8::Error }, + ParseKeyError { + path: PathBuf, + #[snafu(source(from(pkcs8::Error, Box::new)))] + source: Box, + }, #[snafu(display("no identity found with name `{name}`"))] NoSuchIdentity { name: String }, diff --git a/docs/cli-reference.md b/docs/cli-reference.md index dc94e9a50..a0568dbea 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -16,6 +16,7 @@ This document contains the help content for the `icp-cli` command-line program. * [`icp-cli canister settings`↴](#icp-cli-canister-settings) * [`icp-cli canister settings show`↴](#icp-cli-canister-settings-show) * [`icp-cli canister settings update`↴](#icp-cli-canister-settings-update) +* [`icp-cli canister settings sync`↴](#icp-cli-canister-settings-sync) * [`icp-cli canister show`↴](#icp-cli-canister-show) * [`icp-cli canister start`↴](#icp-cli-canister-start) * [`icp-cli canister status`↴](#icp-cli-canister-status) @@ -99,7 +100,7 @@ Perform canister operations against a network * `info` — Display a canister's information * `install` — Install a built WASM to a canister on a network * `list` — List the canisters in an environment -* `settings` — +* `settings` — Commands to manage canister settings * `show` — Show a canister's details * `start` — Start a canister on a network * `status` — Show the status of a canister @@ -237,17 +238,22 @@ List the canisters in an environment ## `icp-cli canister settings` +Commands to manage canister settings + **Usage:** `icp-cli canister settings ` ###### **Subcommands:** -* `show` — -* `update` — +* `show` — Display a canister's settings +* `update` — Change a canister's settings to specified values +* `sync` — Synchronize a canister's settings with those defined in the project ## `icp-cli canister settings show` +Display a canister's settings + **Usage:** `icp-cli canister settings show [OPTIONS] ` ###### **Arguments:** @@ -265,6 +271,8 @@ List the canisters in an environment ## `icp-cli canister settings update` +Change a canister's settings to specified values + **Usage:** `icp-cli canister settings update [OPTIONS] ` ###### **Arguments:** @@ -295,6 +303,25 @@ List the canisters in an environment +## `icp-cli canister settings sync` + +Synchronize a canister's settings with those defined in the project + +**Usage:** `icp-cli canister settings sync [OPTIONS] ` + +###### **Arguments:** + +* `` — Name or principal of canister to target When using a name an environment must be specified + +###### **Options:** + +* `--network ` — Name of the network to target, conflicts with environment argument +* `--environment ` — Override the environment to connect to. By default, the local environment is used +* `--ic` — Shorthand for --environment=ic +* `--identity ` — The user identity to run this command as + + + ## `icp-cli canister show` Show a canister's details