Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0fc6010
Add post-install tasks
adamspofford-dfinity Jun 15, 2022
a9a3421
Update changelog
adamspofford-dfinity Jun 15, 2022
bee3124
fmt/lint
adamspofford-dfinity Jun 15, 2022
13040e6
shellcheck
adamspofford-dfinity Jun 15, 2022
23af40c
Switch from kebab-case to snake_case
adamspofford-dfinity Jun 16, 2022
3a262b3
Refactor command-processing logic into own function
adamspofford-dfinity Jun 16, 2022
a889840
Update e2e test script
adamspofford-dfinity Jun 16, 2022
67f3b65
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 16, 2022
14c4290
Add test for dependency variable
adamspofford-dfinity Jun 16, 2022
c488fce
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 17, 2022
aaf1b0a
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 17, 2022
aa4e74e
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 21, 2022
cd5ae0c
Merge branch 'spofford/postbuild' of https://github.com/dfinity/sdk i…
adamspofford-dfinity Jun 21, 2022
d7c68e1
switch to post-#2267 format & reorganize tests
adamspofford-dfinity Jun 21, 2022
16e9d8f
shellcheck
adamspofford-dfinity Jun 21, 2022
f33c864
fmt
adamspofford-dfinity Jun 22, 2022
61f1826
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 24, 2022
4c0ebc5
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 24, 2022
9b5c003
Merge branch 'master' into spofford/postbuild
adamspofford-dfinity Jun 27, 2022
47991a0
unused param
adamspofford-dfinity Jun 27, 2022
339f8dd
Make two canisters distinguishable
adamspofford-dfinity Jun 27, 2022
776ba75
Merge branch 'spofford/postbuild' of https://github.com/dfinity/sdk i…
adamspofford-dfinity Jun 27, 2022
62b63b3
clarify
adamspofford-dfinity Jun 27, 2022
65fa7b6
Merge branch 'master' into spofford/postbuild
Jun 27, 2022
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.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@

== DFX

=== feat: Post-installation tasks

You can now add your own custom post-installation/post-deployment tasks to any canister type. The new `+post-install+` key for canister objects in `+dfx.json+` can be a command or list of commands, similar to the `+build+` key of `+custom+` canisters, and receives all the same environment variables. For example, to replicate the upload task performed with `+assets+` canisters, you might set `+"post-install": "icx-asset sync $CANISTER_ID dist"+`.

=== chore: updates webpack dependencies for dfx new project

Resolves an issue where `+webpack-cli+` was was breaking when users tried to run `+npm start+` in a fresh project. For affected users of 0.10.1, you can resolve this issue manually by running `+npm install webpack@latest webpack-cli@latest terser-webpack-plugin@latest+`.
Expand Down
37 changes: 37 additions & 0 deletions e2e/tests-dfx/install.bash
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,40 @@ teardown() {
dfx canister create --all
assert_command_fail dfx canister install --all --wasm "${archive:?}/wallet/0.10.0/wallet.wasm"
}

@test "install runs post-install tasks" {
dfx_start
# shellcheck disable=SC2094
cat <<<"$(jq '.canisters.e2e_project.post_install="sh -c \"echo hello\""' dfx.json)" >dfx.json
Comment thread
adamspofford-dfinity marked this conversation as resolved.
Outdated

assert_command dfx canister create --all
assert_command dfx build

assert_command dfx canister install --all
Comment thread
adamspofford-dfinity marked this conversation as resolved.
Outdated
assert_match hello

# shellcheck disable=SC2094
cat <<<"$(jq '.canisters.e2e_project.post_install=["sh -c \"echo hello\"", "sh -c \"return 1\""]' dfx.json)" >dfx.json
assert_command_fail dfx canister install --all --mode upgrade
assert_match hello

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain why "hello" is expected in the output here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's expected because stdout is still printed; it's tested for to ensure that the script is why the command failed.

}

@test "post-install tasks receive environment variables" {
dfx_start
# shellcheck disable=SC2094
cat <<<"$(jq '.canisters.e2e_project.post_install="sh -c \"echo hello $CANISTER_ID\""' dfx.json)" >dfx.json
Comment thread
adamspofford-dfinity marked this conversation as resolved.
Outdated

assert_command dfx canister create --all
assert_command dfx build
id=$(dfx canister id e2e_project)

assert_command dfx canister install --all
assert_match "hello $id"
Comment thread
adamspofford-dfinity marked this conversation as resolved.
assert_command dfx canister install e2e_project --mode upgrade
assert_match "hello $id"

assert_command dfx deploy
assert_match "hello $id"
assert_command dfx deploy e2e_project
assert_match "hello $id"
}
2 changes: 2 additions & 0 deletions src/dfx/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub async fn exec(
call_sender,
installed_module_hash,
opts.upgrade_unchanged,
None,
)
.await
}
Expand Down Expand Up @@ -165,6 +166,7 @@ pub async fn exec(
call_sender,
installed_module_hash,
opts.upgrade_unchanged,
None,
)
.await?;
}
Expand Down
4 changes: 4 additions & 0 deletions src/dfx/src/config/dfinity.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![allow(dead_code)]
use crate::lib::error::{BuildError, DfxError, DfxResult};
use crate::util::SerdeVec;
use crate::{error_invalid_argument, error_invalid_config, error_invalid_data};

use anyhow::{anyhow, Context};
Expand Down Expand Up @@ -71,6 +72,9 @@ pub struct ConfigCanistersCanister {
#[serde(default)]
pub remote: Option<ConfigCanistersCanisterRemote>,

#[serde(default)]
pub post_install: SerdeVec<String>,

#[serde(flatten)]
pub extras: BTreeMap<String, Value>,
}
Expand Down
2 changes: 1 addition & 1 deletion src/dfx/src/lib/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ fn ensure_trailing_newline(s: String) -> String {

type Env<'a> = (Cow<'static, str>, Cow<'a, OsStr>);

fn environment_variables<'a>(
pub fn environment_variables<'a>(
info: &CanisterInfo,
network_name: &'a str,
pool: &'a CanisterPool,
Expand Down
10 changes: 10 additions & 0 deletions src/dfx/src/lib/canister_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct CanisterInfo {
packtool: Option<String>,
args: Option<String>,

post_install: Vec<String>,

extras: BTreeMap<String, serde_json::Value>,
}

Expand Down Expand Up @@ -112,6 +114,8 @@ impl CanisterInfo {
.cloned()
.unwrap_or_else(|| "motoko".to_owned());

let post_install = canister_config.post_install.clone().into_vec();

let canister_info = CanisterInfo {
name: name.to_string(),
canister_type,
Expand All @@ -130,6 +134,8 @@ impl CanisterInfo {
packtool: build_defaults.get_packtool(),
args: build_defaults.get_args(),
extras,

post_install,
};

let canister_args: Option<String> = canister_info.get_extra_optional("args")?;
Expand Down Expand Up @@ -233,6 +239,10 @@ impl CanisterInfo {
&self.packtool
}

pub fn get_post_install(&self) -> &[String] {
&self.post_install
}

pub fn get_args(&self) -> &Option<String> {
&self.args
}
Expand Down
14 changes: 11 additions & 3 deletions src/dfx/src/lib/operations/canister/deploy_canisters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ pub async fn deploy_canisters(
)
.await?;

build_canisters(env, &canisters_to_build, &config)?;
let pool = build_canisters(env, &canisters_to_build, &config)?;

install_canisters(
env,
Expand All @@ -104,6 +104,7 @@ pub async fn deploy_canisters(
upgrade_unchanged,
timeout,
call_sender,
pool,
)
.await?;

Expand Down Expand Up @@ -193,12 +194,17 @@ async fn register_canisters(
}

#[context("Failed to build call canisters.")]
fn build_canisters(env: &dyn Environment, canister_names: &[String], config: &Config) -> DfxResult {
fn build_canisters(
env: &dyn Environment,
canister_names: &[String],
config: &Config,
) -> DfxResult<CanisterPool> {
info!(env.get_logger(), "Building canisters...");
let build_mode_check = false;
let canister_pool = CanisterPool::load(env, build_mode_check, canister_names)?;

canister_pool.build_or_fail(&BuildConfig::from_config(config)?)
canister_pool.build_or_fail(&BuildConfig::from_config(config)?)?;
Ok(canister_pool)
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -214,6 +220,7 @@ async fn install_canisters(
upgrade_unchanged: bool,
timeout: Duration,
call_sender: &CallSender,
pool: CanisterPool,
) -> DfxResult {
info!(env.get_logger(), "Installing canisters...");

Expand Down Expand Up @@ -265,6 +272,7 @@ async fn install_canisters(
call_sender,
installed_module_hash,
upgrade_unchanged,
Some(&pool),
)
.await?;
}
Expand Down
74 changes: 74 additions & 0 deletions src/dfx/src/lib/operations/canister/install_canister.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
use crate::lib::builders::environment_variables;
use crate::lib::canister_info::CanisterInfo;
use crate::lib::environment::Environment;
use crate::lib::error::DfxResult;
use crate::lib::identity::identity_utils::CallSender;
use crate::lib::identity::Identity;
use crate::lib::installers::assets::post_install_store_assets;
use crate::lib::models::canister::CanisterPool;
use crate::lib::named_canister;
use crate::lib::network::network_descriptor::NetworkDescriptor;
use crate::lib::waiter::waiter_with_timeout;
use crate::util::assets::wallet_wasm;
use crate::util::{expiry_duration, read_module_metadata};
Expand All @@ -17,10 +20,12 @@ use ic_utils::call::AsyncCall;
use ic_utils::interfaces::management_canister::builders::{CanisterInstall, InstallMode};
use ic_utils::interfaces::ManagementCanister;
use ic_utils::Argument;
use itertools::Itertools;
use openssl::sha::Sha256;
use slog::info;
use std::collections::HashSet;
use std::io::stdin;
use std::process::{Command, Stdio};
use std::time::Duration;

#[allow(clippy::too_many_arguments)]
Expand All @@ -35,6 +40,7 @@ pub async fn install_canister(
call_sender: &CallSender,
installed_module_hash: Option<Vec<u8>>,
upgrade_unchanged: bool,
pool: Option<&CanisterPool>,
) -> DfxResult {
let log = env.get_logger();
let network = env.get_network_descriptor();
Expand Down Expand Up @@ -159,6 +165,74 @@ pub async fn install_canister(
post_install_store_assets(canister_info, agent, timeout).await?;
}

if !canister_info.get_post_install().is_empty() {
Comment thread
adamspofford-dfinity marked this conversation as resolved.
run_post_install_tasks(env, canister_info, network, pool)?;
}

Ok(())
}

#[context("Failed to run post-install tasks")]
fn run_post_install_tasks(
env: &dyn Environment,
canister: &CanisterInfo,
network: &NetworkDescriptor,
pool: Option<&CanisterPool>,
) -> DfxResult {
let tmp;
let pool = match pool {
Some(pool) => pool,
None => {
tmp = env
.get_config_or_anyhow()?
.get_config()
.get_canister_names_with_dependencies(Some(canister.get_name()))
.and_then(|deps| CanisterPool::load(env, false, &deps))
.context("Error collecting canisters for post-install task")?;
&tmp
}
};
let dependencies = pool
.get_canister_list()
.iter()
.map(|can| can.canister_id())
.collect_vec();
for task in canister.get_post_install() {
run_post_install_task(env, canister, task, network, pool, &dependencies)?;
}
Ok(())
}

#[context("Failed to run post-install task {task}")]
fn run_post_install_task(
_: &dyn Environment,
Comment thread
adamspofford-dfinity marked this conversation as resolved.
Outdated
canister: &CanisterInfo,
task: &str,
network: &NetworkDescriptor,
pool: &CanisterPool,
dependencies: &[Principal],
) -> DfxResult {
let words = shell_words::split(task)
.with_context(|| format!("Error interpreting post-install task `{task}`"))?;
let mut command = Command::new(&words[0]);
command.args(&words[1..]);
let vars = environment_variables(canister, &network.name, pool, dependencies);
for (key, val) in vars {
command.env(&*key, val);
}
command
.stdin(Stdio::piped())
.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}")
}
None => bail!("The post-install task `{task}` was terminated by a signal"),
}
}
Ok(())
}

Expand Down
23 changes: 23 additions & 0 deletions src/dfx/src/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use candid::{parser::value::IDLValue, IDLArgs};
use fn_error_context::context;
use net2::TcpListenerExt;
use net2::{unix::UnixTcpBuilderExt, TcpBuilder};
use serde::{Deserialize, Serialize};
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;

Expand Down Expand Up @@ -220,3 +221,25 @@ pub fn blob_from_arguments(
v => Err(error_unknown!("Invalid type: {}", v)),
}
}

#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(untagged)]
pub enum SerdeVec<T> {
One(T),
Many(Vec<T>),
}

impl<T> SerdeVec<T> {
pub fn into_vec(self) -> Vec<T> {
match self {
Self::One(t) => vec![t],
Self::Many(ts) => ts,
}
}
}

impl<T> Default for SerdeVec<T> {
fn default() -> Self {
Self::Many(vec![])
}
}