Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pre-install hooks to canisters. #4055

Merged
merged 2 commits into from
Jan 6, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ Your principal for ICP wallets and decentralized exchanges: ueuar-wxbnk-bdcsr-dn
(run `dfx identity get-principal` to display)
```

### feat: Add pre-install tasks

Add pre-install tasks, which can be defined by the new `pre-install` key for canister objects in `dfx.json` with a command or list of commands.

## Dependencies

### Frontend canister
Expand Down
10 changes: 10 additions & 0 deletions docs/dfx-json-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,16 @@
}
]
},
"pre_install": {
"title": "Pre-Install Commands",
"description": "One or more commands to run pre canister installation. These commands are executed in the root of the project.",
"default": [],
"allOf": [
{
"$ref": "#/definitions/SerdeVec_for_String"
}
]
},
"pullable": {
"title": "Pullable",
"description": "Defines required properties so that this canister is ready for `dfx deps pull` by other projects.",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
{
"version": 1,
"canisters": {
"preinstall": {
"main": "main.mo",
"pre_install": "echo hello-pre-file"
},
"preinstall_script": {
"main": "main.mo",
"pre_install": "preinstall.sh",
"dependencies": ["preinstall"]
},
"postinstall": {
"main": "main.mo",
"post_install": "echo hello-file"
"post_install": "echo hello-post-file"
},
"postinstall_script": {
"main": "main.mo",
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env bash
echo "working directory of post-install script: '$(pwd)'"
echo hello-script
echo hello-post-script
3 changes: 3 additions & 0 deletions e2e/assets/pre_post_install/preinstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
echo "working directory of pre-install script: '$(pwd)'"
echo hello-pre-script
63 changes: 44 additions & 19 deletions e2e/tests-dfx/install.bash
Original file line number Diff line number Diff line change
Expand Up @@ -90,69 +90,94 @@ teardown() {
assert_command_fail dfx canister install --all --wasm "${archive:?}/wallet/0.10.0/wallet.wasm"
}

@test "install runs post-install tasks" {
install_asset post_install
@test "install runs pre-post-install tasks" {
install_asset pre_post_install
dfx_start

assert_command dfx canister create --all
assert_command dfx build

assert_command dfx canister install preinstall
assert_match 'hello-pre-file'

assert_command dfx canister install preinstall_script
assert_match 'hello-pre-script'

echo 'return 1' >> preinstall.sh
assert_command_fail dfx canister install preinstall_script --mode upgrade
assert_match 'hello-pre-script'

assert_command dfx canister install postinstall
assert_match 'hello-file'
assert_match 'hello-post-file'

assert_command dfx canister install postinstall_script
assert_match 'hello-script'
assert_match 'hello-post-script'

echo 'return 1' >> postinstall.sh
assert_command_fail dfx canister install postinstall_script --mode upgrade
assert_match 'hello-script'
assert_match 'hello-post-script'
}

@test "post-install tasks run in project root" {
install_asset post_install
@test "pre-post-install tasks run in project root" {
install_asset pre_post_install
dfx_start

assert_command dfx canister create --all
assert_command dfx build

cd src/e2e_project_backend

assert_command dfx canister install preinstall_script
assert_match 'hello-pre-script'
assert_match "working directory of pre-install script: '.*/working-dir/e2e_project'"

assert_command dfx canister install postinstall_script
assert_match 'hello-script'
assert_match 'hello-post-script'
assert_match "working directory of post-install script: '.*/working-dir/e2e_project'"
}

@test "post-install tasks receive environment variables" {
install_asset post_install
@test "pre-post-install tasks receive environment variables" {
install_asset pre_post_install
dfx_start
echo "echo hello \$CANISTER_ID" >> preinstall.sh
echo "echo hello \$CANISTER_ID" >> postinstall.sh

assert_command dfx canister create --all
assert_command dfx build
id=$(dfx canister id postinstall_script)
id_pre=$(dfx canister id preinstall_script)
id_post=$(dfx canister id postinstall_script)

assert_command dfx canister install --all
assert_match "hello $id"
assert_match "hello $id_post"
assert_command dfx canister install preinstall_script --mode upgrade
assert_match "hello $id_pre"
assert_command dfx canister install postinstall_script --mode upgrade
assert_match "hello $id"
assert_match "hello $id_post"

assert_command dfx deploy
assert_match "hello $id"
assert_match "hello $id_post"
assert_command dfx deploy preinstall_script
assert_match "hello $id_pre"
assert_command dfx deploy postinstall_script
assert_match "hello $id"
assert_match "hello $id_post"
}

@test "post-install tasks discover dependencies" {
install_asset post_install
@test "pre-post-install tasks discover dependencies" {
install_asset pre_post_install
dfx_start
echo "echo hello \$CANISTER_ID_PREINSTALL" >> preinstall.sh
echo "echo hello \$CANISTER_ID_POSTINSTALL" >> postinstall.sh

assert_command dfx canister create --all
assert_command dfx build
id=$(dfx canister id postinstall)
id_pre=$(dfx canister id preinstall)
id_post=$(dfx canister id postinstall)

assert_command dfx canister install preinstall_script
assert_match "hello $id_pre"

assert_command dfx canister install postinstall_script
assert_match "hello $id"
assert_match "hello $id_post"
}

@test "can install gzip wasm" {
Expand Down
6 changes: 6 additions & 0 deletions src/dfx-core/src/config/model/dfinity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,12 @@ pub struct ConfigCanistersCanister {
#[serde(flatten)]
pub type_specific: CanisterTypeProperties,

/// # Pre-Install Commands
/// One or more commands to run pre canister installation.
/// These commands are executed in the root of the project.
#[serde(default)]
pub pre_install: SerdeVec<String>,

/// # Post-Install Commands
/// One or more commands to run post canister installation.
/// These commands are executed in the root of the project.
Expand Down
7 changes: 7 additions & 0 deletions src/dfx/src/lib/canister_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ pub struct CanisterInfo {
type_specific: CanisterTypeProperties,

dependencies: Vec<String>,
pre_install: Vec<String>,
post_install: Vec<String>,
main: Option<PathBuf>,
shrink: Option<bool>,
Expand Down Expand Up @@ -171,6 +172,7 @@ impl CanisterInfo {
_ => build_defaults.get_args(),
};

let pre_install = canister_config.pre_install.clone().into_vec();
let post_install = canister_config.post_install.clone().into_vec();
let metadata = CanisterMetadataConfig::new(&canister_config.metadata, &network_name);

Expand All @@ -190,6 +192,7 @@ impl CanisterInfo {
args,
type_specific,
dependencies,
pre_install,
post_install,
main: canister_config.main.clone(),
shrink: canister_config.shrink,
Expand Down Expand Up @@ -262,6 +265,10 @@ impl CanisterInfo {
&self.packtool
}

pub fn get_pre_install(&self) -> &[String] {
&self.pre_install
}

pub fn get_post_install(&self) -> &[String] {
&self.post_install
}
Expand Down
56 changes: 44 additions & 12 deletions src/dfx/src/lib/operations/canister/install_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ pub async fn install_canister(
let log = env.get_logger();
let agent = env.get_agent();
let network = env.get_network_descriptor();
if !canister_info.get_pre_install().is_empty() {
let config = env.get_config()?;
run_customized_install_tasks(
env,
canister_info,
true,
network,
pool,
env_file.or_else(|| config.as_ref()?.get_config().output_env_file.as_deref()),
)?;
}
if !network.is_ic && named_canister::get_ui_canister_id(canister_id_store).is_none() {
named_canister::install_ui_canister(env, canister_id_store, None).await?;
}
Expand Down Expand Up @@ -282,9 +293,10 @@ The command line value will be used.",
}
if !canister_info.get_post_install().is_empty() {
let config = env.get_config()?;
run_post_install_tasks(
run_customized_install_tasks(
env,
canister_info,
false,
network,
pool,
env_file.or_else(|| config.as_ref()?.get_config().output_env_file.as_deref()),
Expand Down Expand Up @@ -435,23 +447,26 @@ fn check_stable_compatibility(
})
}

#[context("Failed to run post-install tasks")]
fn run_post_install_tasks(
#[context("Failed to run {}-install tasks", if is_pre_install { "pre" } else { "post" })]
fn run_customized_install_tasks(
env: &dyn Environment,
canister: &CanisterInfo,
is_pre_install: bool,
network: &NetworkDescriptor,
pool: Option<&CanisterPool>,
env_file: Option<&Path>,
) -> DfxResult {
let pre_or_post = if is_pre_install { "pre" } else { "post" };
let tmp;
let pool = match pool {
Some(pool) => pool,
None => {
let config = env.get_config_or_anyhow()?;
let canisters_to_load = all_project_canisters_with_ids(env, &config);

tmp = CanisterPool::load(env, false, &canisters_to_load)
.context("Error collecting canisters for post-install task")?;
tmp = CanisterPool::load(env, false, &canisters_to_load).context(format!(
"Error collecting canisters for {pre_or_post}-install task"
))?;
&tmp
}
};
Expand All @@ -460,27 +475,43 @@ fn run_post_install_tasks(
.iter()
.map(|can| can.canister_id())
.collect_vec();
for task in canister.get_post_install() {
run_post_install_task(canister, task, network, pool, &dependencies, env_file)?;
let tasks = if is_pre_install {
canister.get_pre_install()
} else {
canister.get_post_install()
};
for task in tasks {
run_customized_install_task(
canister,
task,
is_pre_install,
network,
pool,
&dependencies,
env_file,
)?;
}
Ok(())
}

#[context("Failed to run post-install task {task}")]
fn run_post_install_task(
#[context("Failed to run {}-install task {}", if is_pre_install { "pre" } else { "post" }, task)]
fn run_customized_install_task(
canister: &CanisterInfo,
task: &str,
is_pre_install: bool,
network: &NetworkDescriptor,
pool: &CanisterPool,
dependencies: &[Principal],
env_file: Option<&Path>,
) -> DfxResult {
let pre_or_post = if is_pre_install { "pre" } else { "post" };
let cwd = canister.get_workspace_root();
let words = shell_words::split(task)
.with_context(|| format!("Error interpreting post-install task `{task}`"))?;
.with_context(|| format!("Error interpreting {pre_or_post}-install task `{task}`"))?;
let canonicalized = dfx_core::fs::canonicalize(&cwd.join(&words[0]))
.or_else(|_| which::which(&words[0]))
.map_err(|_| anyhow!("Cannot find command or file {}", &words[0]))?;

let mut command = Command::new(canonicalized);
command.args(&words[1..]);
let vars =
Expand All @@ -492,13 +523,14 @@ fn run_post_install_task(
.current_dir(cwd)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());

let status = command.status()?;
if !status.success() {
match status.code() {
Some(code) => {
bail!("The post-install task `{task}` failed with exit code {code}")
bail!("The {pre_or_post}-install task `{task}` failed with exit code {code}")
}
None => bail!("The post-install task `{task}` was terminated by a signal"),
None => bail!("The {pre_or_post}-install task `{task}` was terminated by a signal"),
}
}
Ok(())
Expand Down
Loading