Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Unreleased

* feat: `icp settings autocontainerize true`, always use a docker container for all networks
* feat: `icp identity export` to print the PEM file for the identity

# v0.1.0-beta.5

* fix: Fix error when loading network descriptors from v0.1.0-beta.3
* feat: `icp identity delete` and `icp identity rename`
* feat: `icp identity export` to print the PEM file for the identity

# v0.1.0-beta.4

Expand Down
4 changes: 4 additions & 0 deletions crates/icp-cli/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub(crate) mod network;
pub(crate) mod new;
pub(crate) mod parsers;
pub(crate) mod project;
pub(crate) mod settings;
pub(crate) mod sync;
pub(crate) mod token;

Expand Down Expand Up @@ -54,6 +55,9 @@ pub(crate) enum Command {
#[command(subcommand)]
Project(project::Command),

/// Configure user settings
Settings(settings::SettingsArgs),

/// Synchronize canisters
Sync(sync::SyncArgs),

Expand Down
11 changes: 11 additions & 0 deletions crates/icp-cli/src/commands/network/start.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use icp::prelude::*;
use icp::{
identity::manifest::IdentityList,
network::{Configuration, run_network},
settings::Settings,
};
use tracing::debug;

Expand Down Expand Up @@ -95,6 +96,12 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::

let candid_ui_wasm = crate::artifacts::get_candid_ui_wasm();

let settings = ctx
.dirs
.settings()?
.with_read(async |dirs| Settings::load_from(dirs))
.await??;

let network_launcher_path = if let Ok(var) = std::env::var("ICP_CLI_NETWORK_LAUNCHER_PATH") {
Some(PathBuf::from(var))
} else {
Expand Down Expand Up @@ -124,6 +131,9 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::
}
};

// On Windows, always use Docker since the native launcher doesn't run there
let autocontainerize = cfg!(windows) || settings.autocontainerize;

run_network(
cfg,
nd,
Expand All @@ -133,6 +143,7 @@ pub(crate) async fn exec(ctx: &Context, args: &StartArgs) -> Result<(), anyhow::
args.background,
ctx.debug,
network_launcher_path.as_deref(),
autocontainerize,
)
.await?;
Ok(())
Expand Down
66 changes: 66 additions & 0 deletions crates/icp-cli/src/commands/settings.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
use clap::{Args, Subcommand};
use icp::{context::Context, settings::Settings};

#[derive(Debug, Args)]
pub(crate) struct SettingsArgs {
#[command(subcommand)]
setting: Setting,
}

#[derive(Debug, Subcommand)]
#[command(
subcommand_value_name = "SETTING",
subcommand_help_heading = "Settings",
override_usage = "icp settings [OPTIONS] <SETTING> [VALUE]",
disable_help_subcommand = true
)]
enum Setting {
/// Use Docker for the network launcher even when native mode is requested
Autocontainerize(AutocontainerizeArgs),
}

#[derive(Debug, Args)]
struct AutocontainerizeArgs {
/// Set to true or false. If omitted, prints the current value.
value: Option<bool>,
}

pub(crate) async fn exec(ctx: &Context, args: &SettingsArgs) -> Result<(), anyhow::Error> {
match &args.setting {
Setting::Autocontainerize(sub_args) => exec_autocontainerize(ctx, sub_args).await,
}
}

async fn exec_autocontainerize(
ctx: &Context,
args: &AutocontainerizeArgs,
) -> Result<(), anyhow::Error> {
let dirs = ctx.dirs.settings()?;

match args.value {
Some(value) => {
dirs.with_write(async |dirs| {
let mut settings = Settings::load_from(dirs.read())?;
settings.autocontainerize = value;
settings.write_to(dirs)?;
println!("Set autocontainerize to {value}");
if cfg!(windows) {
eprintln!(
"Warning: This setting is ignored on Windows. \
Docker is always used because the network launcher does not run natively."
);
}
Ok(())
})
.await?
}

None => {
let settings = dirs
.with_read(async |dirs| Settings::load_from(dirs))
.await??;
println!("{}", settings.autocontainerize);
Ok(())
}
}
}
7 changes: 7 additions & 0 deletions crates/icp-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,13 @@ async fn main() -> Result<(), Error> {
}
},

// Settings
Command::Settings(args) => {
commands::settings::exec(&ctx, &args)
.instrument(trace_span)
.await?
}

// Sync
Command::Sync(args) => {
commands::sync::exec(&ctx, &args)
Expand Down
16 changes: 14 additions & 2 deletions crates/icp-cli/tests/common/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,12 @@ impl TestContext {
cmd.current_dir(self.home_path());
// Isolate the whole user directory in Unix, test in normal mode
#[cfg(unix)]
cmd.env("HOME", self.home_path()).env_remove("ICP_HOME");
cmd.env("HOME", self.home_path())
.env_remove("ICP_HOME")
// Also set XDG directories to ensure isolation on Linux
.env("XDG_CONFIG_HOME", self.home_path().join(".config"))
.env("XDG_DATA_HOME", self.home_path().join(".local/share"))
.env("XDG_CACHE_HOME", self.home_path().join(".cache"));
// Run in portable mode on Windows, the user directory cannot be mocked
#[cfg(windows)]
cmd.env("ICP_HOME", self.home_path().join("icp"));
Expand Down Expand Up @@ -204,7 +209,14 @@ impl TestContext {
cmd.current_dir(project_dir);
// isolate the whole user directory in Unix, test in normal mode
#[cfg(unix)]
cmd.env("HOME", self.home_path()).env_remove("ICP_HOME");
{
cmd.env("HOME", self.home_path())
.env_remove("ICP_HOME")
// Also set XDG directories to ensure isolation on Linux
.env("XDG_CONFIG_HOME", self.home_path().join(".config"))
.env("XDG_DATA_HOME", self.home_path().join(".local/share"))
.env("XDG_CACHE_HOME", self.home_path().join(".cache"));
}
// run in portable mode on Windows, the user directory cannot be mocked
#[cfg(windows)]
cmd.env("ICP_HOME", self.home_path().join("icp"));
Expand Down
78 changes: 77 additions & 1 deletion crates/icp-cli/tests/network_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use icp_canister_interfaces::{
use indoc::{formatdoc, indoc};
use predicates::{
ord::eq,
str::{PredicateStrExt, contains, is_match},
prelude::*,
str::{contains, is_match},
};
use serde_json::Value;
use serial_test::file_serial;
Expand Down Expand Up @@ -618,3 +619,78 @@ async fn override_local_network_as_connected() {
.assert()
.success();
}

/// Test that setting autocontainerize=true causes the network launcher to run in Docker
/// even when a native launcher configuration is used.
///
/// This test is skipped on Windows because autocontainerize has no effect there
/// (Docker is always used on Windows).
#[cfg(not(windows))]
#[tag(docker)]
#[tokio::test]
async fn network_autocontainerize_uses_docker() {
let ctx = TestContext::new();

// Set autocontainerize to true
ctx.icp()
.args(["settings", "autocontainerize", "true"])
.assert()
.success();

let project_dir = ctx.create_project_dir("autocontainerize-test");

// Use a native launcher configuration (not an explicit docker image)
write_string(&project_dir.join("icp.yaml"), NETWORK_RANDOM_PORT)
.expect("failed to write project manifest");

// Pull the docker image first
ctx.docker_pull_network();

// Start the network
let _guard = ctx.start_network_in(&project_dir, "random-network").await;

// Verify the descriptor contains a container ID (not a PID)
let descriptor_file_path = project_dir
.join(".icp")
.join("cache")
.join("networks")
.join("random-network")
.join("descriptor.json");

let descriptor_contents =
read_to_string(&descriptor_file_path).expect("Failed to read network descriptor file");
let descriptor: Value = descriptor_contents
.trim()
.parse()
.expect("Descriptor file should contain valid JSON");

// When running in Docker, the child-locator should have an "id" field (container ID)
// rather than a "pid" field
let child_locator = descriptor
.get("child-locator")
.expect("Descriptor should have child-locator");

assert!(
child_locator.get("id").is_some(),
"With autocontainerize=true, child-locator should have container 'id', not 'pid'. Got: {child_locator}"
);
assert!(
child_locator.get("pid").is_none(),
"With autocontainerize=true, child-locator should not have 'pid'. Got: {child_locator}"
);

let container_id = child_locator
.get("id")
.and_then(|id| id.as_str())
.expect("Container ID should be a string");

// Verify the container is running
let output = std::process::Command::new("docker")
.args(["inspect", container_id])
.output()
.expect("Failed to run docker inspect");
assert!(
output.status.success(),
"Container should be running while network is active"
);
}
84 changes: 84 additions & 0 deletions crates/icp-cli/tests/settings_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
use predicates::{ord::eq, prelude::*};

mod common;
use common::TestContext;

#[test]
fn settings_autocontainerize_default() {
let ctx = TestContext::new();

// Default value should be false
ctx.icp()
.args(["settings", "autocontainerize"])
.assert()
.success()
.stdout(eq("false").trim());
}

#[test]
fn settings_autocontainerize_set_true() {
let ctx = TestContext::new();

// Set to true
ctx.icp()
.args(["settings", "autocontainerize", "true"])
.assert()
.success()
.stdout(eq("Set autocontainerize to true").trim());

// Verify it's now true
ctx.icp()
.args(["settings", "autocontainerize"])
.assert()
.success()
.stdout(eq("true").trim());
}

#[test]
fn settings_autocontainerize_set_false() {
let ctx = TestContext::new();

// Set to true first
ctx.icp()
.args(["settings", "autocontainerize", "true"])
.assert()
.success();

// Set back to false
ctx.icp()
.args(["settings", "autocontainerize", "false"])
.assert()
.success()
.stdout(eq("Set autocontainerize to false").trim());

// Verify it's now false
ctx.icp()
.args(["settings", "autocontainerize"])
.assert()
.success()
.stdout(eq("false").trim());
}

#[test]
fn settings_autocontainerize_persists() {
let ctx = TestContext::new();

// Set to true
ctx.icp()
.args(["settings", "autocontainerize", "true"])
.assert()
.success();

// Verify it persists across multiple reads
ctx.icp()
.args(["settings", "autocontainerize"])
.assert()
.success()
.stdout(eq("true").trim());

ctx.icp()
.args(["settings", "autocontainerize"])
.assert()
.success()
.stdout(eq("true").trim());
}
Loading
Loading