Skip to content
1 change: 1 addition & 0 deletions crates/icp-cli/src/commands/canister/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),

Expand Down
5 changes: 5 additions & 0 deletions crates/icp-cli/src/commands/canister/settings/mod.rs
Original file line number Diff line number Diff line change
@@ -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),
}
75 changes: 75 additions & 0 deletions crates/icp-cli/src/commands/canister/settings/sync.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
use crate::{
commands::args::CanisterCommandArgs, operations::settings::SyncSettingsOperationError,
};

use clap::Args;
use ic_utils::interfaces::ManagementCanister;
use icp::{
LoadError,
context::{
AssertEnvContainsCanisterError, CanisterSelection, Context, GetCanisterIdAndAgentError,
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)]
LoadProject { source: LoadError },

#[snafu(display("project does not contain a canister named '{name}'"))]
CanisterNotFound { name: String },

#[snafu(transparent)]
EnvironmentCanisterNotFound {
source: AssertEnvContainsCanisterError,
},

#[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 p = ctx.project.load().await?;

let Some((_, canister)) = p.canisters.get(name) else {
return CanisterNotFoundSnafu { name }.fail();
};
ctx.assert_env_contains_canister(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(())
}
115 changes: 97 additions & 18 deletions crates/icp-cli/src/commands/canister/settings/update.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -104,31 +107,34 @@ pub(crate) struct UpdateArgs {
environment_variables: Option<EnvironmentVariableOpt>,
}

#[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> {
Expand All @@ -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);

Expand Down Expand Up @@ -169,6 +185,7 @@ pub(crate) async fn exec(ctx: &Context, args: &UpdateArgs) -> Result<(), Command
// Handle environment variables.
let mut environment_variables: Option<Vec<EnvironmentVariable>> = 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());
}
Expand All @@ -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 {
Expand Down Expand Up @@ -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(())
}
7 changes: 6 additions & 1 deletion crates/icp-cli/src/commands/deploy/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -227,13 +228,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:");

Expand Down
6 changes: 6 additions & 0 deletions crates/icp-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions crates/icp-cli/src/operations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Loading