diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d244980b3..d9b5850a3e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,17 @@ # UNRELEASED +### feat: expose canister upgrade options in CLI + +`dfx canister install` and `dfx deploy` takes options `--skip-pre-upgrade` and `--wasm-memory-persistence`. + +`dfx deploy --mode` now takes the same possible values as `dfx canister install --mode`: "install", "reinstall", "upgrade" and "auto". + +In "auto" mode, the upgrade options are hints which only take effects when the actual install mode is "upgrade". + +To maintain backward compatibility, a minor difference between the two commands remains. +If the `--mode` is not set, `dfx deploy` defaults to "auto", while `dfx canister install` defaults to "install". + ### feat: Also report Motoko stable compatibility warnings Report upgrade compatibility warnings for Motoko, such as deleted stable variables, in addition to compatibility errors. diff --git a/docs/cli-reference/dfx-canister.mdx b/docs/cli-reference/dfx-canister.mdx index 93016472fa..39e606227a 100644 --- a/docs/cli-reference/dfx-canister.mdx +++ b/docs/cli-reference/dfx-canister.mdx @@ -534,17 +534,19 @@ dfx canister install [option] [--all | canister_name] You can use the following options with the `dfx canister install` command. -| Option | Description | -|-----------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--argument ` | Specifies an argument to pass to the canister during installation. | -| `--argument-type ` | Specifies the data type for the argument when making the call using an argument [possible values: idl, raw] | -| `--argument-file ` | Specifies the file from which to read the argument to pass to the init method. Stdin may be referred to as `-`. | -| `--async-call` | Enables you to continue without waiting for the result of the installation to be returned by polling the Internet Computer or the local canister execution environment. | -| `-m`, `--mode ` | Specifies whether you want to `install`, `reinstall`, or `upgrade` canisters. Defaults to `install`. For more information about installation modes and canister management, see [managing canisters](/docs/current/developer-docs/smart-contracts/maintain/settings). | -| `--no-wallet` | Performs the call with the user Identity as the Sender of messages. Bypasses the Wallet canister. Enabled by default. | -| `--no-asset-upgrade` | Skips upgrading the asset canister, to only install the assets themselves. | -| `--upgrade-unchanged` | Upgrade the canister even if the .wasm did not change. | -| `--wasm ` | Specifies a particular Wasm file to install, bypassing the dfx.json project settings. | +| Option | Description | +|------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--argument ` | Specifies an argument to pass to the canister during installation. | +| `--argument-type ` | Specifies the data type for the argument when making the call using an argument [possible values: idl, raw] | +| `--argument-file ` | Specifies the file from which to read the argument to pass to the init method. Stdin may be referred to as `-`. | +| `--async-call` | Enables you to continue without waiting for the result of the installation to be returned by polling the Internet Computer or the local canister execution environment. | +| `-m`, `--mode ` | Specifies whether you want to `install`, `reinstall`, or `upgrade` canisters. Defaults to `install`. For more information about installation modes and canister management, see [managing canisters](/docs/current/developer-docs/smart-contracts/maintain/settings). | +| `--no-wallet` | Performs the call with the user Identity as the Sender of messages. Bypasses the Wallet canister. Enabled by default. | +| `--no-asset-upgrade` | Skips upgrading the asset canister, to only install the assets themselves. | +| `--upgrade-unchanged` | Upgrade the canister even if the .wasm did not change. | +| `--wasm ` | Specifies a particular Wasm file to install, bypassing the dfx.json project settings. | +| `--skip-pre-upgrade` | Skip the pre_upgrade hook on upgrade. This requires the upgrade/auto mode. | +| `--wasm-memory-persistence ` | Keep or replace the Wasm main memory on upgrade. Possible values: keep, replace. This requires the upgrade/auto mode. | #### Specifies the argument to pass to the init entrypoint diff --git a/docs/cli-reference/dfx-deploy.mdx b/docs/cli-reference/dfx-deploy.mdx index 72573176c5..24ad93411a 100644 --- a/docs/cli-reference/dfx-deploy.mdx +++ b/docs/cli-reference/dfx-deploy.mdx @@ -26,23 +26,26 @@ dfx deploy [options] [canister_name] You can use the following options with the `dfx deploy` command. -| Option | Description | -|------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `--network ` | Overrides the environment to connect to. By default, the local canister execution environment is used. | -| `--playground ` | Alias for `--network playground`. By default, canisters on this network are borrowed from the Motoko Playground. | -| `--ic ` | Alias for `--network ic`. | -| `--argument ` | Specifies an argument to pass to the canister during installation. | -| `--argument-type ` | Specifies the data type for the argument when making the call using an argument [possible values: idl, raw] | -| `--argument-file ` | Specifies the file from which to read the argument to pass to the init method. Stdin may be referred to as `-`. | -| `--created-at-time ` | Transaction timestamp, in nanoseconds, for use in controlling transaction deduplication, default is system time. https://internetcomputer.org/docs/current/developer-docs/integrations/icrc-1/#transaction-deduplication- | -| `--from-subaccount ` | Subaccount of the selected identity to spend cycles from. | -| `--with-cycles ` | Enables you to specify the initial number of cycles for a canister in a project. | -| `--specified-id ` | Attempts to create the canister with this Canister ID | -| `--by-proposal` | Upload proposed changed assets, but do not commit them. Follow up by calling either commit_proposed_batch() or delete_batch(). | -| `--compute-evidence` | Build a frontend canister, determine batch operations required to synchronize asset canister contents, and compute a hash over those operations. Displays this hash ("evidence"), which should match the evidence displayed by `dfx deploy --by-proposal`. | -| `--subnet-type ` | Specify the subnet type to create the canister on. If no subnet type is provided, the canister will be created on a random default application subnet. | -| `--subnet ` | Specify the subnet to create the canister on. If no subnet is provided, the canister will be created on a random default application subnet. | -| `--next-to ` | Create canisters on the same subnet as this canister. | +| Option | Description | +|------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--network ` | Overrides the environment to connect to. By default, the local canister execution environment is used. | +| `--playground ` | Alias for `--network playground`. By default, canisters on this network are borrowed from the Motoko Playground. | +| `--ic ` | Alias for `--network ic`. | +| `--argument ` | Specifies an argument to pass to the canister during installation. | +| `--argument-type ` | Specifies the data type for the argument when making the call using an argument [possible values: idl, raw] | +| `--argument-file ` | Specifies the file from which to read the argument to pass to the init method. Stdin may be referred to as `-`. | +| `--created-at-time ` | Transaction timestamp, in nanoseconds, for use in controlling transaction deduplication, default is system time. https://internetcomputer.org/docs/current/developer-docs/integrations/icrc-1/#transaction-deduplication- | +| `--from-subaccount ` | Subaccount of the selected identity to spend cycles from. | +| `-m`, `--mode ` | Specifies whether you want to `install`, `reinstall`, or `upgrade` canisters. Defaults to `auto`. For more information about installation modes and canister management, see [managing canisters](/docs/current/developer-docs/smart-contracts/maintain/settings). | +| `--with-cycles ` | Enables you to specify the initial number of cycles for a canister in a project. | +| `--specified-id ` | Attempts to create the canister with this Canister ID | +| `--by-proposal` | Upload proposed changed assets, but do not commit them. Follow up by calling either commit_proposed_batch() or delete_batch(). | +| `--compute-evidence` | Build a frontend canister, determine batch operations required to synchronize asset canister contents, and compute a hash over those operations. Displays this hash ("evidence"), which should match the evidence displayed by `dfx deploy --by-proposal`. | +| `--subnet-type ` | Specify the subnet type to create the canister on. If no subnet type is provided, the canister will be created on a random default application subnet. | +| `--subnet ` | Specify the subnet to create the canister on. If no subnet is provided, the canister will be created on a random default application subnet. | +| `--next-to ` | Create canisters on the same subnet as this canister. | +| `--skip-pre-upgrade` | Skip the pre_upgrade hook on upgrade. This requires the upgrade/auto mode. | +| `--wasm-memory-persistence ` | Keep or replace the Wasm main memory on upgrade. Possible values: keep, replace. This requires the upgrade/auto mode. | ### Specifies the argument to pass to the init entrypoint diff --git a/e2e/tests-dfx/deploy.bash b/e2e/tests-dfx/deploy.bash index 80462bb707..709f912f8e 100644 --- a/e2e/tests-dfx/deploy.bash +++ b/e2e/tests-dfx/deploy.bash @@ -252,3 +252,31 @@ teardown() { assert_command dfx canister call fake-cmc last_create_canister_args assert_contains 'subnet_type = opt "custom_subnet_type"' } + +@test "specify install mode" { + dfx_start + assert_command dfx deploy --mode install + assert_command dfx deploy hello_backend --mode reinstall --yes + assert_command dfx deploy --mode upgrade + assert_command dfx deploy --mode auto +} + +@test "specify upgrade options (skip_pre_upgrade, wasm_memory_persistence)" { + dfx_start + + # The default mode in deploy is 'auto'. + # When the canister is not deployed yet, the actual InstallMode is 'install'. + # In this case, the provided upgrade options are just hint which doesn't take effect. + assert_command dfx deploy --skip-pre-upgrade + + assert_command dfx deploy --wasm-memory-persistence keep + assert_command dfx deploy --wasm-memory-persistence replace + assert_command dfx deploy --skip-pre-upgrade --wasm-memory-persistence keep + assert_command dfx deploy --mode auto --skip-pre-upgrade + assert_command dfx deploy --mode upgrade --skip-pre-upgrade + + assert_command_fail dfx deploy --mode install --skip-pre-upgrade + assert_contains "--skip-pre-upgrade and --wasm-memory-persistence can only be used with mode 'upgrade' or 'auto'." + assert_command_fail dfx deploy --mode reinstall --wasm-memory-persistence keep + assert_contains "--skip-pre-upgrade and --wasm-memory-persistence can only be used with mode 'upgrade' or 'auto'." +} diff --git a/e2e/tests-dfx/install.bash b/e2e/tests-dfx/install.bash index f5a4131e08..e947e8e22d 100644 --- a/e2e/tests-dfx/install.bash +++ b/e2e/tests-dfx/install.bash @@ -339,3 +339,28 @@ Please remove one of them or leave both undefined." echo yes | dfx canister install --mode=reinstall custom done } + +@test "specify upgrade options (skip_pre_upgrade, wasm_memory_persistence)" { + dfx_start + dfx canister create e2e_project_backend + dfx build e2e_project_backend + + # The canister is not installed yet and the mode is 'auto'. + # The actual InstallMode will be 'install'. + # In this case, the provided upgrade options are just hint which doesn't take effect. + assert_command dfx canister install e2e_project_backend --mode auto --skip-pre-upgrade + + assert_command dfx canister install e2e_project_backend --mode auto --wasm-memory-persistence keep + assert_command dfx canister install e2e_project_backend --mode auto --wasm-memory-persistence replace + assert_command dfx canister install e2e_project_backend --mode auto --skip-pre-upgrade --wasm-memory-persistence keep + + assert_command dfx canister install e2e_project_backend --mode upgrade --skip-pre-upgrade + assert_command dfx canister install e2e_project_backend --mode upgrade --wasm-memory-persistence keep + assert_command dfx canister install e2e_project_backend --mode upgrade --wasm-memory-persistence replace + assert_command dfx canister install e2e_project_backend --mode upgrade --skip-pre-upgrade --wasm-memory-persistence keep + + assert_command_fail dfx canister install e2e_project_backend --mode install --skip-pre-upgrade + assert_contains "--skip-pre-upgrade and --wasm-memory-persistence can only be used with mode 'upgrade' or 'auto'." + assert_command_fail dfx canister install e2e_project_backend --mode reinstall --wasm-memory-persistence keep + assert_contains "--skip-pre-upgrade and --wasm-memory-persistence can only be used with mode 'upgrade' or 'auto'." +} diff --git a/src/dfx/src/commands/canister/install.rs b/src/dfx/src/commands/canister/install.rs index 8da0b3640d..d992ca2d9e 100644 --- a/src/dfx/src/commands/canister/install.rs +++ b/src/dfx/src/commands/canister/install.rs @@ -6,16 +6,15 @@ use crate::lib::operations::canister::install_canister::install_canister; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::blob_from_arguments; use crate::util::clap::argument_from_cli::ArgumentFromCliLongOpt; +use crate::util::clap::install_mode::{InstallModeHint, InstallModeOpt}; use dfx_core::canister::{install_canister_wasm, install_mode_to_prompt}; use dfx_core::identity::CallSender; -use anyhow::{anyhow, bail, Context}; +use anyhow::bail; use candid::Principal; use clap::Parser; -use ic_utils::interfaces::management_canister::builders::InstallMode; use slog::info; use std::path::PathBuf; -use std::str::FromStr; /// Installs compiled code in a canister. #[derive(Parser, Clone)] @@ -36,11 +35,8 @@ pub struct CanisterInstallOpts { #[arg(long)] async_call: bool, - /// Specifies the type of deployment. You can set the canister deployment modes to install, reinstall, or upgrade. - /// If auto is selected, either install or upgrade will be used depending on if the canister has already been installed. - #[arg(long, short, default_value("install"), - value_parser = ["install", "reinstall", "upgrade", "auto"])] - mode: String, + #[command(flatten)] + install_mode: InstallModeOpt, /// Upgrade the canister even if the .wasm did not change. #[arg(long)] @@ -82,15 +78,11 @@ pub async fn exec( ) -> DfxResult { fetch_root_key_if_needed(env).await?; - let mode = if opts.mode == "auto" { - None - } else { - Some(InstallMode::from_str(&opts.mode).map_err(|err| anyhow!(err))?) - }; + let mode_hint = opts.install_mode.mode_for_canister_install()?; let mut canister_id_store = env.get_canister_id_store()?; let network = env.get_network_descriptor(); - if mode == Some(InstallMode::Reinstall) && (opts.canister.is_none() || opts.all) { + if mode_hint == InstallModeHint::Reinstall && (opts.canister.is_none() || opts.all) { bail!("The --mode=reinstall is only valid when specifying a single canister, because reinstallation destroys all data in the canister."); } @@ -109,7 +101,7 @@ pub async fn exec( opts.always_assist, )?; let wasm_module = dfx_core::fs::read(wasm_path)?; - let mode = mode.context("The install mode cannot be auto when using --wasm")?; + let mode = mode_hint.to_install_mode_with_wasm_path()?; info!( env.get_logger(), "{} code for canister {}", @@ -151,7 +143,6 @@ pub async fn exec( let canister_info = CanisterInfo::load(&config, canister, Some(canister_id))?; if let Some(wasm_path) = opts.wasm { // streamlined version, we can ignore most of the environment - let mode = mode.context("The install mode cannot be auto when using --wasm")?; install_canister( env, &mut canister_id_store, @@ -160,7 +151,7 @@ pub async fn exec( Some(&wasm_path), argument_from_cli.as_deref(), argument_type.as_deref(), - Some(mode), + &mode_hint, call_sender, opts.upgrade_unchanged, None, @@ -180,7 +171,7 @@ pub async fn exec( None, argument_from_cli.as_deref(), argument_type.as_deref(), - mode, + &mode_hint, call_sender, opts.upgrade_unchanged, None, @@ -224,7 +215,7 @@ pub async fn exec( None, None, None, - mode, + &mode_hint, call_sender, opts.upgrade_unchanged, None, diff --git a/src/dfx/src/commands/deploy.rs b/src/dfx/src/commands/deploy.rs index 0c25831384..78751a62de 100644 --- a/src/dfx/src/commands/deploy.rs +++ b/src/dfx/src/commands/deploy.rs @@ -9,21 +9,20 @@ use crate::lib::operations::canister::deploy_canisters::DeployMode::{ }; use crate::lib::root_key::fetch_root_key_if_needed; use crate::util::clap::argument_from_cli::ArgumentFromCliLongOpt; +use crate::util::clap::install_mode::{InstallModeHint, InstallModeOpt}; use crate::util::clap::parsers::{cycle_amount_parser, icrc_subaccount_parser}; use crate::util::clap::subnet_selection_opt::SubnetSelectionOpt; use crate::util::url::{construct_frontend_url, construct_ui_canister_url}; -use anyhow::{anyhow, bail, Context}; +use anyhow::{anyhow, bail}; use candid::Principal; use clap::Parser; use console::Style; use dfx_core::config::model::network_descriptor::NetworkDescriptor; use dfx_core::identity::CallSender; -use ic_utils::interfaces::management_canister::builders::InstallMode; use icrc_ledger_types::icrc1::account::Subaccount; use slog::info; use std::collections::BTreeMap; use std::path::PathBuf; -use std::str::FromStr; use tokio::runtime::Runtime; use url::Url; @@ -37,12 +36,8 @@ pub struct DeployOpts { #[command(flatten)] argument_from_cli: ArgumentFromCliLongOpt, - /// Force the type of deployment to be reinstall, which overwrites the module. - /// In other words, this erases all data in the canister. - /// By default, upgrade will be chosen automatically if the module already exists, - /// or install if it does not. - #[arg(long, short, value_parser = ["reinstall"])] - mode: Option, + #[command(flatten)] + install_mode: InstallModeOpt, /// Upgrade the canister even if the .wasm did not change. #[arg(long)] @@ -127,21 +122,15 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { if argument_from_cli.is_some() && canister_name.is_none() { bail!("The init argument can only be set when deploying a single canister."); } - let mode = opts - .mode - .as_deref() - .map(InstallMode::from_str) - .transpose() - .map_err(|err| anyhow!(err)) - .context("Failed to parse InstallMode.")?; + let mode_hint = opts.install_mode.mode_for_deploy()?; let config = env.get_config_or_anyhow()?; let env_file = config.get_output_env_file(opts.output_env_file)?; let mut subnet_selection = runtime.block_on(opts.subnet_selection.into_subnet_selection_type(&env))?; let with_cycles = opts.with_cycles; - let deploy_mode = match (mode, canister_name) { - (Some(InstallMode::Reinstall), Some(canister_name)) => { + let deploy_mode = match (&mode_hint, canister_name) { + (InstallModeHint::Reinstall, Some(canister_name)) => { let network = env.get_network_descriptor(); if config .get_config() @@ -152,25 +141,22 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { } ForceReinstallSingleCanister(canister_name.to_string()) } - (Some(InstallMode::Reinstall), None) => { + (InstallModeHint::Reinstall, None) => { bail!("The --mode=reinstall is only valid when deploying a single canister, because reinstallation destroys all data in the canister."); } - (Some(_), _) => { - unreachable!("The only valid option for --mode is --mode=reinstall"); - } - (None, None) if opts.by_proposal => { + (_, None) if opts.by_proposal => { bail!("The --by-proposal flag is only valid when deploying a single canister."); } - (None, Some(canister_name)) if opts.by_proposal => { + (_, Some(canister_name)) if opts.by_proposal => { PrepareForProposal(canister_name.to_string()) } - (None, None) if opts.compute_evidence => { + (_, None) if opts.compute_evidence => { bail!("The --compute-evidence flag is only valid when deploying a single canister."); } - (None, Some(canister_name)) if opts.compute_evidence => { + (_, Some(canister_name)) if opts.compute_evidence => { ComputeEvidence(canister_name.to_string()) } - (None, _) => NormalDeploy, + (_, _) => NormalDeploy, }; let call_sender = CallSender::from(&opts.wallet, env.get_network_descriptor()) @@ -184,6 +170,7 @@ pub fn exec(env: &dyn Environment, opts: DeployOpts) -> DfxResult { argument_from_cli.as_deref(), argument_type.as_deref(), &deploy_mode, + &mode_hint, opts.upgrade_unchanged, with_cycles, opts.created_at_time, diff --git a/src/dfx/src/lib/operations/canister/deploy_canisters.rs b/src/dfx/src/lib/operations/canister/deploy_canisters.rs index 369ba1975f..b1bdff2a3a 100644 --- a/src/dfx/src/lib/operations/canister/deploy_canisters.rs +++ b/src/dfx/src/lib/operations/canister/deploy_canisters.rs @@ -13,6 +13,7 @@ use crate::lib::operations::canister::motoko_playground::reserve_canister_with_p use crate::lib::operations::canister::{ all_project_canisters_with_ids, create_canister, install_canister::install_canister, }; +use crate::util::clap::install_mode::InstallModeHint; use crate::util::clap::subnet_selection_opt::SubnetSelectionType; use anyhow::{anyhow, bail, Context}; use candid::Principal; @@ -23,7 +24,7 @@ use fn_error_context::context; use ic_utils::interfaces::management_canister::attributes::{ ComputeAllocation, FreezingThreshold, MemoryAllocation, ReservedCyclesLimit, }; -use ic_utils::interfaces::management_canister::builders::{InstallMode, WasmMemoryLimit}; +use ic_utils::interfaces::management_canister::builders::WasmMemoryLimit; use icrc_ledger_types::icrc1::account::Subaccount; use slog::info; use std::convert::TryFrom; @@ -45,6 +46,7 @@ pub async fn deploy_canisters( argument: Option<&str>, argument_type: Option<&str>, deploy_mode: &DeployMode, + mode_hint: &InstallModeHint, upgrade_unchanged: bool, with_cycles: Option, created_at_time: Option, @@ -143,15 +145,13 @@ pub async fn deploy_canisters( match deploy_mode { NormalDeploy | ForceReinstallSingleCanister(_) => { - let force_reinstall = matches!(deploy_mode, ForceReinstallSingleCanister(_)); install_canisters( env, &canisters_to_install, - &initial_canister_id_store, &config, argument, argument_type, - force_reinstall, + mode_hint, upgrade_unchanged, call_sender, pool, @@ -313,11 +313,10 @@ async fn build_canisters( async fn install_canisters( env: &dyn Environment, canister_names: &[String], - initial_canister_id_store: &CanisterIdStore, config: &Config, argument: Option<&str>, argument_type: Option<&str>, - force_reinstall: bool, + mode_hint: &InstallModeHint, upgrade_unchanged: bool, call_sender: &CallSender, pool: CanisterPool, @@ -331,15 +330,6 @@ async fn install_canisters( let mut canister_id_store = env.get_canister_id_store()?; for canister_name in canister_names { - let install_mode = if force_reinstall { - Some(InstallMode::Reinstall) - } else { - match initial_canister_id_store.find(canister_name) { - Some(_) => None, - None => Some(InstallMode::Install), - } - }; - let canister_id = canister_id_store.get(canister_name)?; let canister_info = CanisterInfo::load(config, canister_name, Some(canister_id))?; @@ -351,7 +341,7 @@ async fn install_canisters( None, argument, argument_type, - install_mode, + mode_hint, call_sender, upgrade_unchanged, Some(&pool), diff --git a/src/dfx/src/lib/operations/canister/install_canister.rs b/src/dfx/src/lib/operations/canister/install_canister.rs index d2e5efd8df..3591021dbb 100644 --- a/src/dfx/src/lib/operations/canister/install_canister.rs +++ b/src/dfx/src/lib/operations/canister/install_canister.rs @@ -9,6 +9,7 @@ use crate::lib::operations::canister::all_project_canisters_with_ids; use crate::lib::operations::canister::motoko_playground::authorize_asset_uploader; use crate::lib::state_tree::canister_info::read_state_tree_canister_module_hash; use crate::util::assets::wallet_wasm; +use crate::util::clap::install_mode::InstallModeHint; use crate::util::{blob_from_arguments, get_candid_init_type, read_module_metadata}; use anyhow::{anyhow, bail, Context}; use backoff::backoff::Backoff; @@ -21,9 +22,7 @@ use dfx_core::config::model::network_descriptor::NetworkDescriptor; use dfx_core::identity::CallSender; use fn_error_context::context; use ic_agent::Agent; -use ic_utils::interfaces::management_canister::builders::{ - CanisterUpgradeOptions, InstallMode, WasmMemoryPersistence, -}; +use ic_utils::interfaces::management_canister::builders::{InstallMode, WasmMemoryPersistence}; use ic_utils::interfaces::ManagementCanister; use ic_utils::Argument; use itertools::Itertools; @@ -44,7 +43,7 @@ pub async fn install_canister( wasm_path_override: Option<&Path>, argument_from_cli: Option<&str>, argument_type_from_cli: Option<&str>, - mode: Option, + mode_hint: &InstallModeHint, call_sender: &CallSender, upgrade_unchanged: bool, pool: Option<&CanisterPool>, @@ -65,20 +64,37 @@ pub async fn install_canister( "Previously installed module hash: {:?}", installed_module_hash.as_ref().map(hex::encode) ); - let wasm_memory_persistence = + let wasm_memory_persistence_embeded = read_module_metadata(agent, canister_id, "enhanced-orthogonal-persistence") .await .map(|_| WasmMemoryPersistence::Keep); - let mode = mode.unwrap_or_else(|| { - if installed_module_hash.is_some() { - InstallMode::Upgrade(Some(CanisterUpgradeOptions { - wasm_memory_persistence, - skip_pre_upgrade: None, - })) - } else { - InstallMode::Install - } - }); + // let mode = if let InstallModeHint::Auto(options) = mode_hint { + // if installed_module_hash.is_some() { + // InstallMode::Upgrade(Some(CanisterUpgradeOptions { + // wasm_memory_persistence, + // skip_pre_upgrade: None, + // })) + // } else { + // InstallMode::Install + // } + // } else { + // InstallMode::Install + // }; + let mode = mode_hint.to_install_mode( + installed_module_hash.is_some(), + wasm_memory_persistence_embeded, + ); + + // let mode = mode.unwrap_or_else(|| { + // if installed_module_hash.is_some() { + // InstallMode::Upgrade(Some(CanisterUpgradeOptions { + // wasm_memory_persistence, + // skip_pre_upgrade: None, + // })) + // } else { + // InstallMode::Install + // } + // }); let mode_str = install_mode_to_prompt(&mode); let canister_name = canister_info.get_name(); info!( diff --git a/src/dfx/src/util/clap/install_mode.rs b/src/dfx/src/util/clap/install_mode.rs new file mode 100644 index 0000000000..01847471d2 --- /dev/null +++ b/src/dfx/src/util/clap/install_mode.rs @@ -0,0 +1,137 @@ +use anyhow::bail; +use clap::Args; +use ic_utils::interfaces::management_canister::builders::{ + CanisterUpgradeOptions, InstallMode, WasmMemoryPersistence, +}; + +use crate::lib::error::DfxResult; + +/// CLI options for the mode of canister installation. +/// +/// Reused in `dfx canister install` and `dfx deploy`. +#[derive(Args, Clone, Debug, Default)] +pub struct InstallModeOpt { + /// Specifies the mode of canister installation. + /// + /// If set to 'auto', either 'install' or 'upgrade' will be used, depending on whether the canister is already installed. + #[arg(long, short, value_parser = ["install", "reinstall", "upgrade", "auto"])] + mode: Option, + + /// Skip the pre_upgrade hook on upgrade. + /// + /// This requires the mode to be set to 'upgrade' or 'auto'. + #[arg(long)] + skip_pre_upgrade: bool, + + /// Keep or replace the Wasm main memory on upgrade. + /// + /// This requires the mode to be set to 'upgrade' or 'auto'. + #[arg(long, value_parser = ["keep", "replace"])] + wasm_memory_persistence: Option, +} + +#[derive(Debug, Clone, PartialEq)] +pub enum InstallModeHint { + Install, + Reinstall, + Upgrade(Option), + Auto(Option), +} + +enum HighLevelMode { + Install, + Reinstall, + Upgrade, + Auto, +} + +impl InstallModeOpt { + /// `dfx canister install` defaults to 'install' mode. + pub fn mode_for_canister_install(&self) -> DfxResult { + self.resolve_install_mode(HighLevelMode::Install) + } + + /// `dfx deploy` defaults to 'auto' mode. + pub fn mode_for_deploy(&self) -> DfxResult { + self.resolve_install_mode(HighLevelMode::Auto) + } + + fn resolve_install_mode(&self, default_mode: HighLevelMode) -> DfxResult { + let wasm_memory_persistence = match self.wasm_memory_persistence { + Some(ref s) => match s.as_str() { + "keep" => Some(WasmMemoryPersistence::Keep), + "replace" => Some(WasmMemoryPersistence::Replace), + _ => unreachable!(), + }, + None => None, + }; + let canister_upgrade_options = match (self.skip_pre_upgrade, wasm_memory_persistence) { + (false, None) => None, + (s, w) => Some(CanisterUpgradeOptions { + skip_pre_upgrade: Some(s), + wasm_memory_persistence: w, + }), + }; + + let high_level_mode = match self.mode.as_deref() { + Some("install") => HighLevelMode::Install, + Some("reinstall") => HighLevelMode::Reinstall, + Some("upgrade") => HighLevelMode::Upgrade, + Some("auto") => HighLevelMode::Auto, + None => default_mode, + _ => unreachable!(), + }; + + if canister_upgrade_options.is_some() + && matches!( + high_level_mode, + HighLevelMode::Install | HighLevelMode::Reinstall + ) + { + bail!("--skip-pre-upgrade and --wasm-memory-persistence can only be used with mode 'upgrade' or 'auto'."); + } + match high_level_mode { + HighLevelMode::Install => Ok(InstallModeHint::Install), + HighLevelMode::Reinstall => Ok(InstallModeHint::Reinstall), + HighLevelMode::Upgrade => Ok(InstallModeHint::Upgrade(canister_upgrade_options)), + HighLevelMode::Auto => Ok(InstallModeHint::Auto(canister_upgrade_options)), + } + } +} + +impl InstallModeHint { + pub fn to_install_mode_with_wasm_path(&self) -> DfxResult { + match self { + InstallModeHint::Install => Ok(InstallMode::Install), + InstallModeHint::Reinstall => Ok(InstallMode::Reinstall), + InstallModeHint::Upgrade(opt) => Ok(InstallMode::Upgrade(*opt)), + InstallModeHint::Auto(_) => bail!("The install mode cannot be auto when using --wasm"), + } + } + + pub fn to_install_mode( + &self, + upgrade_in_auto: bool, + wasm_memory_persistence_embeded: Option, + ) -> InstallMode { + match self { + InstallModeHint::Install => InstallMode::Install, + InstallModeHint::Reinstall => InstallMode::Reinstall, + InstallModeHint::Upgrade(opt) => InstallMode::Upgrade(*opt), + InstallModeHint::Auto(opt) => { + let opt = if opt.is_none() && wasm_memory_persistence_embeded.is_some() { + Some(CanisterUpgradeOptions { + skip_pre_upgrade: None, + wasm_memory_persistence: wasm_memory_persistence_embeded, + }) + } else { + *opt + }; + match upgrade_in_auto { + true => InstallMode::Upgrade(opt), + false => InstallMode::Install, + } + } + } + } +} diff --git a/src/dfx/src/util/clap/mod.rs b/src/dfx/src/util/clap/mod.rs index 285c04f0f6..7c56ed974e 100644 --- a/src/dfx/src/util/clap/mod.rs +++ b/src/dfx/src/util/clap/mod.rs @@ -2,6 +2,7 @@ use anstyle::{AnsiColor, Style}; use clap::builder::Styles; pub mod argument_from_cli; +pub mod install_mode; pub mod parsers; pub mod subnet_selection_opt;