diff --git a/CHANGELOG.md b/CHANGELOG.md index 3daaeb69..4d5a422f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* feat: Added 'friendly name' domains for canisters - instead of `.localhost` you can access `frontend.local.localhost`. + # v0.2.0-beta.0 * feat: Added `bind` key to network gateway config to pick your network interface (previous documentation mentioned a `host` key, but it did not do anything) diff --git a/crates/icp-canister-interfaces/src/candid_ui.rs b/crates/icp-canister-interfaces/src/candid_ui.rs index 36fd5555..f7ca6720 100644 --- a/crates/icp-canister-interfaces/src/candid_ui.rs +++ b/crates/icp-canister-interfaces/src/candid_ui.rs @@ -1 +1,5 @@ -pub const MAINNET_CANDID_UI_CID: &str = "a4gq6-oaaaa-aaaab-qaa4q-cai"; +use candid::Principal; + +// a4gq6-oaaaa-aaaab-qaa4q-cai +pub const MAINNET_CANDID_UI_CID: Principal = + Principal::from_slice(&[0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0x39, 0x01, 0x01]); diff --git a/crates/icp-cli/src/commands/canister/create.rs b/crates/icp-cli/src/commands/canister/create.rs index 4ac16b72..22dc83ae 100644 --- a/crates/icp-cli/src/commands/canister/create.rs +++ b/crates/icp-cli/src/commands/canister/create.rs @@ -241,6 +241,7 @@ async fn create_project_canister(ctx: &Context, args: &CreateArgs) -> Result<(), ctx.set_canister_id_for_env(&canister, id, &selections.environment) .await?; + ctx.update_custom_domains(&selections.environment).await; if args.quiet { let _ = ctx.term.write_line(&format!("{id}")); diff --git a/crates/icp-cli/src/commands/canister/delete.rs b/crates/icp-cli/src/commands/canister/delete.rs index e638eb96..a82baa54 100644 --- a/crates/icp-cli/src/commands/canister/delete.rs +++ b/crates/icp-cli/src/commands/canister/delete.rs @@ -38,6 +38,7 @@ pub(crate) async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow: if let CanisterSelection::Named(canister_name) = &selections.canister { ctx.remove_canister_id_for_env(canister_name, &selections.environment) .await?; + ctx.update_custom_domains(&selections.environment).await; } Ok(()) diff --git a/crates/icp-cli/src/commands/deploy/mod.rs b/crates/icp-cli/src/commands/deploy/mod.rs index 0ef6bc86..5a13a511 100644 --- a/crates/icp-cli/src/commands/deploy/mod.rs +++ b/crates/icp-cli/src/commands/deploy/mod.rs @@ -3,6 +3,7 @@ use candid::{CandidType, Principal}; use clap::Args; use futures::{StreamExt, future::try_join_all, stream::FuturesOrdered}; use ic_agent::Agent; +use icp::network::{Managed, ManagedMode}; use icp::parsers::CyclesAmount; use icp::{ context::{CanisterSelection, Context, EnvironmentSelection}, @@ -176,6 +177,8 @@ pub(crate) async fn exec(ctx: &Context, args: &DeployArgs) -> Result<(), anyhow: } } + ctx.update_custom_domains(&environment_selection).await; + let _ = ctx.term.write_line("\n\nSetting environment variables:"); let env = ctx .get_environment(&environment_selection) @@ -377,21 +380,29 @@ async fn print_canister_urls( agent: Agent, canister_names: &[String], ) -> Result<(), anyhow::Error> { + use icp::network::custom_domains::{canister_gateway_url, gateway_domain}; + let env = ctx.get_environment(environment_selection).await?; // Get the network URL let http_gateway_url = match &env.network.configuration { NetworkConfiguration::Managed { managed: _ } => { - // For managed networks, construct localhost URL let access = ctx.network.access(&env.network).await?; access.http_gateway_url.clone() } - NetworkConfiguration::Connected { connected } => { - // For connected networks, use the configured URL - connected.http_gateway_url.clone() - } + NetworkConfiguration::Connected { connected } => connected.http_gateway_url.clone(), }; + // Friendly domains are available for managed networks where we write custom-domains.txt + let has_friendly = matches!( + &env.network.configuration, + NetworkConfiguration::Managed { + managed: Managed { + mode: ManagedMode::Launcher(config) + } + } if config.version.is_none() + ); + let _ = ctx.term.write_line("\n\nDeployed canisters:"); for name in canister_names { @@ -407,58 +418,40 @@ async fn print_canister_urls( }; if let Some(http_gateway_url) = &http_gateway_url { - // Check if canister has http_request let has_http = has_http_request(&agent, canister_id).await; - let domain = if let Some(domain) = http_gateway_url.domain() { - Some(domain) - } else if let Some(host) = http_gateway_url.host_str() - && (host == "127.0.0.1" || host == "[::1]") - { - Some("localhost") + let friendly = if has_friendly { + Some((name.as_str(), environment_selection.name())) } else { None }; if has_http { - let mut canister_url = http_gateway_url.clone(); - if let Some(domain) = domain { - canister_url - .set_host(Some(&format!("{canister_id}.{domain}"))) - .unwrap(); - } else { - canister_url.set_query(Some(&format!("canisterId={canister_id}"))); - } + let canister_url = canister_gateway_url(http_gateway_url, canister_id, friendly); let _ = ctx .term .write_line(&format!(" {}: {}", name, canister_url)); } else { // For canisters without http_request, show the Candid UI URL - if let Some(ref ui_id) = get_candid_ui_id(ctx, environment_selection).await { - let mut candid_url = http_gateway_url.clone(); - if let Some(domain) = domain { - candid_url - .set_host(Some(&format!("{ui_id}.{domain}",))) - .unwrap(); + if let Some(ui_id) = get_candid_ui_id(ctx, environment_selection).await { + let domain = gateway_domain(http_gateway_url); + let mut candid_url = canister_gateway_url(http_gateway_url, ui_id, None); + if domain.is_some() { candid_url.set_query(Some(&format!("id={canister_id}"))); } else { candid_url.set_query(Some(&format!("canisterId={ui_id}&id={canister_id}"))); } let _ = ctx .term - .write_line(&format!(" {} (Candid UI): {}", name, candid_url)); + .write_line(&format!(" {name} (Candid UI): {candid_url}")); } else { - // No Candid UI available - just show the canister ID let _ = ctx.term.write_line(&format!( - " {}: {} (Candid UI not available)", - name, canister_id + " {name}: {canister_id} (Candid UI not available)", )); } } } else { - // No gateway subdomains available - just show the canister ID let _ = ctx.term.write_line(&format!( - " {}: {} (No gateway URL available)", - name, canister_id + " {name}: {canister_id} (No gateway URL available)", )); } } @@ -471,7 +464,7 @@ async fn print_canister_urls( async fn get_candid_ui_id( ctx: &Context, environment_selection: &EnvironmentSelection, -) -> Option { +) -> Option { let env = ctx.get_environment(environment_selection).await.ok()?; match &env.network.configuration { @@ -481,14 +474,14 @@ async fn get_candid_ui_id( if let Ok(Some(desc)) = nd.load_network_descriptor().await && let Some(candid_ui) = desc.candid_ui_canister_id { - return Some(candid_ui.to_string()); + return Some(candid_ui); } // No Candid UI available for this managed network None } NetworkConfiguration::Connected { .. } => { // For connected networks, use the mainnet Candid UI - Some(MAINNET_CANDID_UI_CID.to_string()) + Some(MAINNET_CANDID_UI_CID) } } } diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index 9c4aa2e1..ad6408be 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -5,7 +5,10 @@ use predicates::{ }; use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; -use icp::{fs::write_string, prelude::*}; +use icp::{ + fs::{create_dir_all, write_string}, + prelude::*, +}; mod common; @@ -403,6 +406,63 @@ async fn deploy_prints_canister_urls() { .stdout(contains("?id=")); } +#[tokio::test] +async fn deploy_prints_friendly_url_for_asset_canister() { + let ctx = TestContext::new(); + + // Setup project + let project_dir = ctx.create_project_dir("icp"); + let assets_dir = project_dir.join("www"); + create_dir_all(&assets_dir).expect("failed to create assets directory"); + write_string(&assets_dir.join("index.html"), "hello").expect("failed to create index page"); + + // Project manifest with a pre-built asset canister + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: pre-built + url: https://github.com/dfinity/sdk/raw/refs/tags/0.27.0/src/distributed/assetstorage.wasm.gz + sha256: 865eb25df5a6d857147e078bb33c727797957247f7af2635846d65c5397b36a6 + + sync: + steps: + - type: assets + dirs: + - {assets_dir} + + {NETWORK_RANDOM_PORT} + {ENVIRONMENT_RANDOM_PORT} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + // Start network + let _g = ctx.start_network_in(&project_dir, "random-network").await; + ctx.ping_until_healthy(&project_dir, "random-network"); + + clients::icp(&ctx, &project_dir, Some("random-environment".to_string())) + .mint_cycles(10 * TRILLION); + + // Deploy and check that the friendly URL is printed (not the Candid UI form) + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--subnet", + common::SUBNET_ID, + "--environment", + "random-environment", + ]) + .assert() + .success() + .stdout(contains("Deployed canisters:")) + .stdout(contains( + "my-canister: http://my-canister.random-environment.localhost:", + )); +} + #[cfg(unix)] // moc #[tokio::test] async fn deploy_upgrade_rejects_incompatible_candid() { diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index d970058d..8f397e46 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -245,7 +245,7 @@ async fn sync_adapter_static_assets() { .assert() .success(); - // Verify that assets canister was synced + // Verify that assets canister was synced via canisterId query param let resp = reqwest::get(format!("http://localhost:{network_port}/?canisterId={cid}")) .await .expect("request failed"); @@ -256,6 +256,26 @@ async fn sync_adapter_static_assets() { .expect("failed to read canister response body"); assert_eq!(out, "hello"); + + // Verify that the friendly domain also works + let friendly_domain = "my-canister.random-environment.localhost"; + let client = reqwest::Client::builder() + .resolve( + friendly_domain, + std::net::SocketAddr::from(([127, 0, 0, 1], network_port)), + ) + .build() + .expect("failed to build reqwest client"); + let resp = client + .get(format!("http://{friendly_domain}:{network_port}/")) + .send() + .await + .expect("friendly domain request failed"); + let out = resp + .text() + .await + .expect("failed to read friendly domain response body"); + assert_eq!(out, "hello"); } #[tokio::test] diff --git a/crates/icp/src/context/mod.rs b/crates/icp/src/context/mod.rs index f20766d0..9e36d501 100644 --- a/crates/icp/src/context/mod.rs +++ b/crates/icp/src/context/mod.rs @@ -500,6 +500,65 @@ impl Context { }) } + /// Updates the `custom-domains.txt` file for the managed network used by the + /// given environment. Collects ID mappings from all environments that share + /// the same managed network, then writes the file to the network's status + /// directory. + /// + /// This is a best-effort operation: errors are logged but not propagated, + /// because a failure to update friendly domains should not block canister + /// creation or deletion. + pub async fn update_custom_domains(&self, environment: &EnvironmentSelection) { + let Ok(env) = self.get_environment(environment).await else { + return; + }; + let NetworkConfiguration::Managed { .. } = &env.network.configuration else { + return; + }; + let Ok(nd) = self.network.get_network_directory(&env.network) else { + return; + }; + let Ok(Some(desc)) = nd.load_network_descriptor().await else { + return; + }; + let Some(status_dir) = &desc.status_dir else { + return; + }; + let gateway_url_str = format!("http://{}:{}", desc.gateway.host, desc.gateway.port); + let Ok(gateway_url) = Url::parse(&gateway_url_str) else { + tracing::warn!("Failed to parse gateway URL {gateway_url_str:?} for custom domains"); + return; + }; + let domain = crate::network::custom_domains::gateway_domain(&gateway_url); + let Some(domain) = domain else { + return; + }; + // Collect mappings from all environments that use this network + let Ok(project) = self.project.load().await else { + return; + }; + let mut env_mappings = std::collections::BTreeMap::new(); + for (env_name, env) in &project.environments { + if env.network.name != desc.network { + continue; + } + let is_cache = matches!( + env.network.configuration, + NetworkConfiguration::Managed { .. } + ); + if let Ok(mapping) = self.ids.lookup_by_environment(is_cache, env_name) + && !mapping.is_empty() + { + env_mappings.insert(env_name.clone(), mapping); + } + } + if let Err(e) = + crate::network::custom_domains::write_custom_domains(status_dir, domain, &env_mappings) + { + tracing::warn!("Failed to update custom domains: {e}"); + } + } + #[cfg(test)] /// Creates a test context with all mocks pub fn mocked() -> Context { diff --git a/crates/icp/src/network/config.rs b/crates/icp/src/network/config.rs index 4a69de7d..cbf25f8f 100644 --- a/crates/icp/src/network/config.rs +++ b/crates/icp/src/network/config.rs @@ -75,6 +75,10 @@ pub struct NetworkDescriptorModel { pub candid_ui_canister_id: Option, /// Canister ID of the deployed proxy canister, if any. pub proxy_canister_id: Option, + /// Path to the status directory shared with the network launcher. + /// Used to write `custom-domains.txt` for friendly domain routing. + #[serde(default)] + pub status_dir: Option, } /// Identifies the process or container running a managed network. diff --git a/crates/icp/src/network/custom_domains.rs b/crates/icp/src/network/custom_domains.rs new file mode 100644 index 00000000..ce0f615e --- /dev/null +++ b/crates/icp/src/network/custom_domains.rs @@ -0,0 +1,183 @@ +use std::collections::BTreeMap; + +use candid::Principal; +use snafu::prelude::*; +use url::Url; + +use crate::{prelude::*, store_id::IdMapping}; + +/// Writes a `custom-domains.txt` file to the given status directory. +/// +/// Each line has the format `..:`. +/// The file is written fresh each time from the full set of current mappings +/// across all environments sharing this network. +pub fn write_custom_domains( + status_dir: &Path, + domain: &str, + env_mappings: &BTreeMap, +) -> Result<(), WriteCustomDomainsError> { + let file_path = status_dir.join("custom-domains.txt"); + let content: String = env_mappings + .iter() + .flat_map(|(env_name, mappings)| { + mappings + .iter() + .map(move |(name, principal)| format!("{name}.{env_name}.{domain}:{principal}\n")) + }) + .collect(); + crate::fs::write(&file_path, content.as_bytes())?; + Ok(()) +} + +/// Extracts the domain authority from a gateway URL for use in subdomain-based +/// canister routing. +/// +/// Returns `Some(domain)` if the URL has a domain name, or if it's a loopback +/// IP address (in which case `"localhost"` is returned). Returns `None` for +/// other IP addresses where subdomain routing is not available. +pub fn gateway_domain(http_gateway_url: &Url) -> Option<&str> { + if let Some(domain) = http_gateway_url.domain() { + Some(domain) + } else if let Some(host) = http_gateway_url.host_str() + && (host == "127.0.0.1" || host == "[::1]") + { + Some("localhost") + } else { + None + } +} + +/// Constructs a gateway URL for accessing a specific canister. +/// +/// For managed networks with a status directory (where friendly domains are +/// registered), returns `http://..:`. +/// +/// Otherwise falls back to `http://.:`, or +/// `http://:?canisterId=` if no subdomain is available. +pub fn canister_gateway_url( + http_gateway_url: &Url, + canister_id: Principal, + friendly: Option<(&str, &str)>, +) -> Url { + let domain = gateway_domain(http_gateway_url); + let mut url = http_gateway_url.clone(); + match (friendly, domain) { + (Some((canister_name, env_name)), Some(domain)) => { + url.set_host(Some(&format!("{canister_name}.{env_name}.{domain}"))) + .expect("friendly domain should be a valid host"); + } + (None, Some(domain)) => { + url.set_host(Some(&format!("{canister_id}.{domain}"))) + .expect("principal domain should be a valid host"); + } + (_, None) => { + url.set_query(Some(&format!("canisterId={canister_id}"))); + } + } + url +} + +#[derive(Debug, Snafu)] +pub enum WriteCustomDomainsError { + #[snafu(transparent)] + WriteFile { source: crate::fs::IoError }, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn write_custom_domains_produces_correct_file() { + let dir = camino_tempfile::Utf8TempDir::new().unwrap(); + let mut env_mappings = BTreeMap::new(); + + let mut local_mappings = BTreeMap::new(); + local_mappings.insert( + "backend".to_string(), + Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap(), + ); + local_mappings.insert( + "frontend".to_string(), + Principal::from_text("bd3sg-teaaa-aaaaa-qaaba-cai").unwrap(), + ); + env_mappings.insert("local".to_string(), local_mappings); + + let mut staging_mappings = BTreeMap::new(); + staging_mappings.insert( + "backend".to_string(), + Principal::from_text("aaaaa-aa").unwrap(), + ); + env_mappings.insert("staging".to_string(), staging_mappings); + + write_custom_domains(dir.path(), "localhost", &env_mappings).unwrap(); + + let content = std::fs::read_to_string(dir.path().join("custom-domains.txt")).unwrap(); + // BTreeMap is ordered, so local comes before staging + assert_eq!( + content, + "backend.local.localhost:bkyz2-fmaaa-aaaaa-qaaaq-cai\n\ + frontend.local.localhost:bd3sg-teaaa-aaaaa-qaaba-cai\n\ + backend.staging.localhost:aaaaa-aa\n" + ); + } + + #[test] + fn canister_gateway_url_with_friendly_domain() { + let base: Url = "http://localhost:8000".parse().unwrap(); + let cid = Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap(); + let url = canister_gateway_url(&base, cid, Some(("backend", "local"))); + assert_eq!(url.as_str(), "http://backend.local.localhost:8000/"); + } + + #[test] + fn canister_gateway_url_without_friendly_domain() { + let base: Url = "http://localhost:8000".parse().unwrap(); + let cid = Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap(); + let url = canister_gateway_url(&base, cid, None); + assert_eq!( + url.as_str(), + "http://bkyz2-fmaaa-aaaaa-qaaaq-cai.localhost:8000/" + ); + } + + #[test] + fn canister_gateway_url_ip_address_fallback() { + let base: Url = "http://192.168.1.1:8000".parse().unwrap(); + let cid = Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap(); + let url = canister_gateway_url(&base, cid, None); + assert_eq!( + url.as_str(), + "http://192.168.1.1:8000/?canisterId=bkyz2-fmaaa-aaaaa-qaaaq-cai" + ); + } + + #[test] + fn canister_gateway_url_loopback_ip_uses_localhost() { + let base: Url = "http://127.0.0.1:8000".parse().unwrap(); + let cid = Principal::from_text("bkyz2-fmaaa-aaaaa-qaaaq-cai").unwrap(); + let url = canister_gateway_url(&base, cid, None); + assert_eq!( + url.as_str(), + "http://bkyz2-fmaaa-aaaaa-qaaaq-cai.localhost:8000/" + ); + } + + #[test] + fn gateway_domain_extracts_from_domain() { + let url: Url = "http://example.com:8000".parse().unwrap(); + assert_eq!(gateway_domain(&url), Some("example.com")); + } + + #[test] + fn gateway_domain_loopback_ip() { + let url: Url = "http://127.0.0.1:8000".parse().unwrap(); + assert_eq!(gateway_domain(&url), Some("localhost")); + } + + #[test] + fn gateway_domain_non_loopback_ip() { + let url: Url = "http://192.168.1.1:8000".parse().unwrap(); + assert_eq!(gateway_domain(&url), None); + } +} diff --git a/crates/icp/src/network/managed/docker.rs b/crates/icp/src/network/managed/docker.rs index 1af70486..350de695 100644 --- a/crates/icp/src/network/managed/docker.rs +++ b/crates/icp/src/network/managed/docker.rs @@ -11,7 +11,6 @@ use bollard::{ }, secret::{ContainerCreateBody, HostConfig, Mount, MountTypeEnum, PortBinding}, }; -use camino_tempfile::Utf8TempDir; use futures::{StreamExt, TryStreamExt}; use itertools::Itertools; use snafu::ResultExt; @@ -238,6 +237,7 @@ pub(super) fn translate_launcher_args_for_docker(args: Vec) -> Vec Result< ( AsyncDropper, @@ -263,13 +263,11 @@ pub async fn spawn_docker_launcher( extra_hosts, } = options; - // Create status tmpdir and convert path for WSL2 if needed + // Convert path for WSL2 if needed let wsl2_distro = std::env::var("ICP_CLI_DOCKER_WSL2_DISTRO").ok(); let wsl2_distro = wsl2_distro.as_deref(); let wsl2_convert = cfg!(windows) && wsl2_distro.is_some(); - let host_status_tmpdir = Utf8TempDir::new().context(CreateStatusDirSnafu)?; - let host_status_dir = host_status_tmpdir.path(); - let host_status_dir_param = convert_path(wsl2_convert, wsl2_distro, host_status_tmpdir.path())?; + let host_status_dir_param = convert_path(wsl2_convert, wsl2_distro, host_status_dir)?; let socket = match std::env::var("DOCKER_HOST").ok() { Some(sock) => sock, @@ -700,8 +698,6 @@ pub enum DockerLauncherError { container_id: String, exit_status: i64, }, - #[snafu(display("failed to create status directory"))] - CreateStatusDir { source: std::io::Error }, #[snafu(display("failed to query docker image {image}"))] QueryImage { source: bollard::errors::Error, diff --git a/crates/icp/src/network/managed/launcher.rs b/crates/icp/src/network/managed/launcher.rs index 41a04681..4d5b46b5 100644 --- a/crates/icp/src/network/managed/launcher.rs +++ b/crates/icp/src/network/managed/launcher.rs @@ -1,6 +1,5 @@ use async_dropper::{AsyncDrop, AsyncDropper}; use async_trait::async_trait; -use camino_tempfile::Utf8TempDir; use candid::Principal; use notify::{EventHandler, Watcher}; use serde::Deserialize; @@ -23,8 +22,6 @@ pub struct NetworkInstance { #[derive(Debug, Snafu)] pub enum SpawnNetworkLauncherError { - #[snafu(display("failed to create status directory"))] - CreateStatusDir { source: std::io::Error }, #[snafu(display("failed to create stdio log at {path}"))] CreateStdioFile { source: std::io::Error, @@ -66,6 +63,7 @@ pub async fn spawn_network_launcher( verbose: bool, launcher_config: &ManagedLauncherConfig, state_dir: &Path, + status_dir: &Path, ) -> Result< ( AsyncDropper, @@ -85,8 +83,7 @@ pub async fn spawn_network_launcher( if let Port::Fixed(port) = launcher_config.gateway.port { cmd.args(["--gateway-port", &port.to_string()]); } - let status_dir = Utf8TempDir::new().context(CreateStatusDirSnafu)?; - cmd.args(["--status-dir", status_dir.path().as_str()]); + cmd.args(["--status-dir", status_dir.as_str()]); cmd.args(launcher_settings_flags(launcher_config)); if background { eprintln!("For background mode, network output will be redirected:"); @@ -105,7 +102,7 @@ pub async fn spawn_network_launcher( cmd.stdout(Stdio::null()); cmd.stderr(Stdio::null()); } - let watcher = wait_for_launcher_status(status_dir.as_ref()).context(WatchStatusDirSnafu)?; + let watcher = wait_for_launcher_status(status_dir).context(WatchStatusDirSnafu)?; let child = cmd.spawn().context(SpawnLauncherSnafu { network_launcher_path, })?; diff --git a/crates/icp/src/network/managed/run.rs b/crates/icp/src/network/managed/run.rs index e19cde66..209281b5 100644 --- a/crates/icp/src/network/managed/run.rs +++ b/crates/icp/src/network/managed/run.rs @@ -1,5 +1,6 @@ use async_dropper::{AsyncDrop, AsyncDropper}; use bigdecimal::BigDecimal; +use camino_tempfile::Utf8TempDir; use candid::{Decode, Encode, Nat, Principal}; use futures::future::{join, join_all}; use ic_agent::{ @@ -159,6 +160,8 @@ async fn run_network_launcher( (LaunchMode::NativeLauncher(launcher_config), fixed_ports) } }; + + let status_dir = Utf8TempDir::new().context(CreateStatusDirSnafu)?; let (mut guard, instance, gateway, locator) = network_root .with_write(async |root| -> Result<_, RunNetworkLauncherError> { // Acquire locks for all fixed ports and check they're not in use @@ -186,7 +189,8 @@ async fn run_network_launcher( match launch_mode { LaunchMode::Image(options) => { - let (guard, instance, locator, fixed) = spawn_docker_launcher(&options).await?; + let (guard, instance, locator, fixed) = + spawn_docker_launcher(&options, status_dir.path()).await?; let gateway = NetworkDescriptorGatewayPort { port: instance.gateway_port, fixed, @@ -211,6 +215,7 @@ async fn run_network_launcher( verbose, launcher_config, &root.state_dir(), + status_dir.path(), ) .await?; let host = match launcher_config.gateway.domains.first() { @@ -228,7 +233,8 @@ async fn run_network_launcher( } }) .await??; - + // The launcher owns cleanup, so we call keep() to prevent the Utf8TempDir from deleting it on drop. + let status_dir_path = status_dir.keep(); if background { // background means we're using stdio files - otherwise the launcher already prints this eprintln!("Network started on port {}", instance.gateway_port); @@ -267,6 +273,7 @@ async fn run_network_launcher( pocketic_instance_id: instance.pocketic_instance_id, candid_ui_canister_id, proxy_canister_id, + status_dir: Some(status_dir_path.clone()), }; // Save descriptor to project root and all fixed port directories @@ -372,6 +379,9 @@ pub enum RunNetworkLauncherError { #[snafu(display("ICP_CLI_NETWORK_LAUNCHER_PATH environment variable is not set"))] NoNetworkLauncherPath, + #[snafu(display("failed to create status directory"))] + CreateStatusDir { source: std::io::Error }, + #[snafu(display("failed to create dir"))] CreateDirAll { source: crate::fs::IoError }, diff --git a/crates/icp/src/network/mod.rs b/crates/icp/src/network/mod.rs index 8cca7fcd..6ac94746 100644 --- a/crates/icp/src/network/mod.rs +++ b/crates/icp/src/network/mod.rs @@ -21,10 +21,12 @@ use crate::{ get_managed_network_access, }, prelude::*, + project::DEFAULT_LOCAL_NETWORK_PORT, }; pub mod access; pub mod config; +pub mod custom_domains; pub mod directory; pub mod managed; @@ -117,7 +119,7 @@ pub enum SubnetKind { impl Default for ManagedMode { fn default() -> Self { - Self::default_for_port(0) + Self::default_for_port(DEFAULT_LOCAL_NETWORK_PORT) } } @@ -214,7 +216,7 @@ impl From for Gateway { let port = match port { Some(0) => Port::Random, Some(p) => Port::Fixed(p), - None => Port::Random, + None => Port::default(), }; let mut domains = domains.unwrap_or_default(); if bind == "127.0.0.1" || bind == "0.0.0.0" || bind == "::1" || bind == "::" {