diff --git a/Cargo.lock b/Cargo.lock index 6286d670f..793a65de5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3817,6 +3817,7 @@ dependencies = [ "semver", "snafu", "tokio", + "url", "wasmtime", "wasmtime-wasi", ] diff --git a/crates/icp-cli/src/commands/sync.rs b/crates/icp-cli/src/commands/sync.rs index 0827952f1..22415374e 100644 --- a/crates/icp-cli/src/commands/sync.rs +++ b/crates/icp-cli/src/commands/sync.rs @@ -128,6 +128,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E let pkg_cache = ctx.dirs.package_cache()?; let project_dir = ctx.project.load().await?.dir; + let urls = ctx.network.urls(&env.network).await?; rendered(ctx.debug, async |reporter| { sync_many( @@ -137,6 +138,7 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E project_dir, environment_selection.name().to_owned(), env.network.name.clone(), + urls, canister_ids, args.proxy, &pkg_cache, diff --git a/crates/icp-cli/tests/sync_tests.rs b/crates/icp-cli/tests/sync_tests.rs index 7b8c9a346..867a766b2 100644 --- a/crates/icp-cli/tests/sync_tests.rs +++ b/crates/icp-cli/tests/sync_tests.rs @@ -448,12 +448,19 @@ async fn sync_plugin_registers_seed_data() { // example's ic-wasm step, so the section genuinely isn't there — proving the // host performed the round-trip and mapped a proven-absent section to `none` // rather than to an error. + // + // It reports the network's URLs too, which for a managed network are the + // gateway the launcher happened to bind — so the port in the plugin's + // output is proof the running network's address reached it. + let gateway_url = ctx.gateway_url().clone(); ctx.icp() .current_dir(&project_dir) .args(["deploy", "--environment", "random-environment"]) .assert() .success() - .stderr(contains("candid:service: absent")); + .stderr(contains("candid:service: absent").and(contains(format!( + "gateway: {gateway_url} (api: {gateway_url})" + )))); // Query the canister to verify all three fruits were registered ctx.icp() diff --git a/crates/icp-sync-plugin/Cargo.toml b/crates/icp-sync-plugin/Cargo.toml index 117086abf..edf8396d6 100644 --- a/crates/icp-sync-plugin/Cargo.toml +++ b/crates/icp-sync-plugin/Cargo.toml @@ -20,6 +20,7 @@ icp-events.workspace = true semver.workspace = true snafu.workspace = true tokio.workspace = true +url.workspace = true wasmtime.workspace = true wasmtime-wasi.workspace = true diff --git a/crates/icp-sync-plugin/DESIGN.md b/crates/icp-sync-plugin/DESIGN.md index ea22acb86..30dafca07 100644 --- a/crates/icp-sync-plugin/DESIGN.md +++ b/crates/icp-sync-plugin/DESIGN.md @@ -90,11 +90,12 @@ pub fn run_plugin(invocation: PluginInvocation) -> Result, RunPlugin `PluginInvocation` bundles the inputs: `wasm_path`, `base_dir`, `project_dir`, `dirs`, `files`, `fields`, `host_canister_id` (the canister being synced), -`agent`, `proxy`, `identity_principal`, `environment`, `compute_limit_secs`, the -exposed `canister_ids` table, the `callable: CallableCanisters` enforcement set, -and `reporter`. The CLI resolves the manifest's declared `canisters:` into -`CallableCanisters` before calling; this crate stays free of any manifest -knowledge. +`agent`, `proxy`, `identity_principal`, `environment`, `api_url` and +`gateway_url` (where the network is reached — informational, since the guest has +no sockets), `compute_limit_secs`, the exposed `canister_ids` table, the +`callable: CallableCanisters` enforcement set, and `reporter`. The CLI resolves +the manifest's declared `canisters:` into `CallableCanisters` before calling; +this crate stays free of any manifest knowledge. `dirs` and `files` are the manifest's own `dirs:`/`files:` settings as manifest-relative paths (`KeyedPath`s carrying the map key each was declared diff --git a/crates/icp-sync-plugin/src/runtime.rs b/crates/icp-sync-plugin/src/runtime.rs index 9d07f9a99..5f2098843 100644 --- a/crates/icp-sync-plugin/src/runtime.rs +++ b/crates/icp-sync-plugin/src/runtime.rs @@ -33,6 +33,7 @@ use snafu::prelude::*; // Aliased because wasmtime-wasi also has an `OutputStream` (imported below). use icp_events::{OutputStream as EventStream, StepReporter}; use tokio::io::{self, AsyncWrite}; +use url::Url; use wasmtime::component::{Component, HasSelf, Linker}; use wasmtime::{Config, Engine, Store}; use wasmtime_wasi::cli::{IsTerminal, StdoutStream}; @@ -756,6 +757,12 @@ pub struct PluginInvocation { pub identity_principal: Principal, /// Name of the environment being synced. pub environment: String, + /// The network's API endpoint — where canister calls are submitted. + /// Surfaced to v0.2.0 plugins; v0.1.0 plugins have no field for it. + pub api_url: Url, + /// The network's HTTP gateway, when it exposes one. Surfaced to v0.2.0 + /// plugins; v0.1.0 plugins have no field for it. + pub gateway_url: Option, /// Pure-wasm compute-time budget in seconds. pub compute_limit_secs: u64, /// The project's canister ID table for this environment, as exposed to the @@ -783,6 +790,8 @@ pub fn run_plugin(invocation: PluginInvocation) -> Result, RunPlugin proxy, identity_principal, environment, + api_url, + gateway_url, compute_limit_secs, canister_ids, callable, @@ -968,6 +977,8 @@ pub fn run_plugin(invocation: PluginInvocation) -> Result, RunPlugin let input = v2::SyncExecInput { canister_id: canister_id_text, environment, + api_url: api_url.to_string(), + gateway_url: gateway_url.map(|url| url.to_string()), dirs: dir_inputs .into_iter() .map(|entry| v2::DirInput { @@ -1248,8 +1259,9 @@ mod tests { /// A [`PluginInvocation`] with test-friendly defaults: anonymous canister /// and identity, no proxy, no declared callable canisters, the default - /// compute limit, and the current directory as both the base and the - /// project. Tests override the few fields they care about. + /// compute limit, a local network with no gateway of its own, and the + /// current directory as both the base and the project. Tests override the + /// few fields they care about. fn invocation(wasm_path: &str, environment: &str) -> PluginInvocation { PluginInvocation { wasm_path: wasm_path.into(), @@ -1263,6 +1275,8 @@ mod tests { proxy: None, identity_principal: anon(), environment: environment.to_string(), + api_url: Url::parse("http://127.0.0.1:4943").expect("valid api url"), + gateway_url: None, compute_limit_secs: DEFAULT_PLUGIN_COMPUTE_LIMIT_SECS, canister_ids: BTreeMap::new(), callable: CallableCanisters::default(), @@ -1785,6 +1799,36 @@ mod tests { )); } + /// Both network URLs reach the plugin, the gateway one as a `some`. + #[test] + fn plugin_network_urls_are_passed_through() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + return; + }; + let mut inv = invocation(wasm_path, "urls"); + inv.api_url = Url::parse("https://icp-api.io").expect("valid api url"); + inv.gateway_url = Some(Url::parse("https://icp0.io").expect("valid gateway url")); + let lines = run_plugin(inv).expect("plugin should succeed"); + assert_eq!( + lines, + vec!["api=https://icp-api.io/ gateway=https://icp0.io/".to_string()] + ); + } + + /// A network with no HTTP gateway leaves `gateway-url` absent rather than + /// passing an empty or invented URL. + #[test] + fn plugin_gateway_url_is_absent_without_a_gateway() { + let Some(wasm_path) = option_env!("TEST_PLUGIN_WASM") else { + return; + }; + let lines = run_plugin(invocation(wasm_path, "urls")).expect("plugin should succeed"); + assert_eq!( + lines, + vec!["api=http://127.0.0.1:4943/ gateway=-".to_string()] + ); + } + /// One `files:` map holds directories and files alike; the host splits them /// by what is on disk, so each lands in the interface list its kind calls /// for, carrying the key it was declared under. diff --git a/crates/icp-sync-plugin/sync-plugin.wit b/crates/icp-sync-plugin/sync-plugin.wit index 66e4df491..ce72848e7 100644 --- a/crates/icp-sync-plugin/sync-plugin.wit +++ b/crates/icp-sync-plugin/sync-plugin.wit @@ -92,6 +92,16 @@ interface types { canister-id: string, /// Name of the environment being synced (e.g. "production", "local"). environment: string, + /// URL of the network's API endpoint: where the host submits the + /// canister calls it makes on the plugin's behalf. The plugin has no + /// sockets of its own, so this is something to compose a URL from or + /// hand to a canister, not something to fetch. Normalized, so a URL + /// with no path carries a trailing slash ("http://127.0.0.1:4943/"). + api-url: string, + /// URL of the network's HTTP gateway, which serves canisters over + /// HTTP, normalized the same way. `none` when the network exposes no + /// gateway. + gateway-url: option, /// Those entries of the manifest step's `files` setting that name a /// directory, in the order they were written. The manifest declares /// directories and files together under `files:`; the host splits them diff --git a/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs b/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs index ca6ec1522..33be617ef 100644 --- a/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs +++ b/crates/icp-sync-plugin/tests/fixtures/test-plugin/src/lib.rs @@ -33,6 +33,16 @@ impl Guest for TestPlugin { eprintln!("{rendered}"); Ok(()) } + // Echo the network URLs back as `api= gateway=`, using + // "-" for a network with no gateway. + "urls" => { + eprintln!( + "api={} gateway={}", + input.api_url, + input.gateway_url.as_deref().unwrap_or("-") + ); + Ok(()) + } // Echo each entry as `kind key=path`, so the host can assert that // keys survive the boundary and that a `files:` entry lands in the // list its kind on disk calls for. diff --git a/crates/icp/src/canister/sync/mod.rs b/crates/icp/src/canister/sync/mod.rs index e20e96ca4..b5b033321 100644 --- a/crates/icp/src/canister/sync/mod.rs +++ b/crates/icp/src/canister/sync/mod.rs @@ -8,6 +8,7 @@ use icp_events::StepReporter; use snafu::prelude::*; use crate::manifest::canister::SyncStep; +use crate::network::NetworkUrls; use crate::package::PackageCache; use crate::prelude::*; @@ -32,6 +33,10 @@ pub struct Params { pub environment: String, /// Name of the network (e.g. "local", "ic"). pub network: String, + /// The network's API endpoint, where canister calls are submitted, and its + /// HTTP gateway if it exposes one. Passed to sync plugin steps via + /// `SyncExecInput`. + pub urls: NetworkUrls, /// IDs of all named canisters in the project for this environment. pub canister_ids: BTreeMap, /// Proxy canister to route calls through, if `--proxy` was passed. @@ -177,6 +182,10 @@ mod tests { name: "backend".to_owned(), environment: "production".to_owned(), network: "ic".to_owned(), + urls: NetworkUrls { + api_url: "https://icp-api.io".parse().expect("valid api url"), + http_gateway_url: Some("https://icp0.io".parse().expect("valid gateway url")), + }, canister_ids: BTreeMap::from([( "my-frontend".to_owned(), Principal::from_slice(&[8; 4]), diff --git a/crates/icp/src/canister/sync/plugin.rs b/crates/icp/src/canister/sync/plugin.rs index 1ff3fc214..2a2a0b2d6 100644 --- a/crates/icp/src/canister/sync/plugin.rs +++ b/crates/icp/src/canister/sync/plugin.rs @@ -148,6 +148,8 @@ pub(super) async fn sync( proxy, identity_principal, environment: environment_owned, + api_url: params.urls.api_url.clone(), + gateway_url: params.urls.http_gateway_url.clone(), compute_limit_secs, canister_ids, callable, @@ -258,6 +260,7 @@ mod tests { } use crate::manifest::adapter::prebuilt::{LocalSource, SourceField}; + use crate::network::NetworkUrls; fn principal(byte: u8) -> Principal { Principal::from_slice(&[byte; 4]) @@ -271,6 +274,10 @@ mod tests { name: name.to_owned(), environment: "demo".to_owned(), network: "ic".to_owned(), + urls: NetworkUrls { + api_url: "https://icp-api.io".parse().expect("valid api url"), + http_gateway_url: Some("https://icp0.io".parse().expect("valid gateway url")), + }, canister_ids: ids.iter().map(|(n, p)| ((*n).to_owned(), *p)).collect(), proxy: None, } diff --git a/crates/icp/src/canister/sync/script.rs b/crates/icp/src/canister/sync/script.rs index 08b0a034e..82732d6a0 100644 --- a/crates/icp/src/canister/sync/script.rs +++ b/crates/icp/src/canister/sync/script.rs @@ -131,6 +131,7 @@ mod tests { use super::*; use crate::manifest::adapter::script::CommandField; + use crate::network::NetworkUrls; /// Serializes the tests here that mutate the process environment, since /// cargo runs tests in parallel threads. Async-aware because the variable @@ -149,6 +150,10 @@ mod tests { name: "backend".to_owned(), environment: "production".to_owned(), network: "ic".to_owned(), + urls: NetworkUrls { + api_url: "https://icp-api.io".parse().expect("valid api url"), + http_gateway_url: Some("https://icp0.io".parse().expect("valid gateway url")), + }, canister_ids: canister_ids .iter() .map(|(n, p)| ((*n).to_owned(), *p)) diff --git a/crates/icp/src/network/access.rs b/crates/icp/src/network/access.rs index 89fc10bed..0a0309f9d 100644 --- a/crates/icp/src/network/access.rs +++ b/crates/icp/src/network/access.rs @@ -9,7 +9,10 @@ use crate::{ agent::{Create, CreateAgentError}, context::IC_ROOT_KEY, manifest::network::RootKeySpec, - network::{Connected, NetworkDirectory, directory::LoadNetworkFileError}, + network::{ + Connected, NetworkDirectory, config::NetworkDescriptorModel, + directory::LoadNetworkFileError, + }, prelude::*, }; @@ -28,6 +31,19 @@ pub enum RootKeySource { Fetched, } +/// The URLs a network is reached at, without any of the trust material +/// [`NetworkAccess`] carries. Resolving these never talks to the network, so a +/// caller that only needs to name an endpoint — to show it, or to hand it to a +/// sync plugin — does not trigger a root key fetch. +#[derive(Clone, Debug)] +pub struct NetworkUrls { + /// Endpoint canister calls are submitted to. + pub api_url: Url, + + /// Gateway that serves canisters over HTTP, if the network exposes one. + pub http_gateway_url: Option, +} + #[derive(Clone)] pub struct NetworkAccess { /// Network's (resolved) root key. @@ -88,6 +104,35 @@ pub enum GetNetworkAccessError { pub async fn get_managed_network_access( nd: NetworkDirectory, ) -> Result { + let (desc, gateway_url) = managed_network_gateway(nd).await?; + Ok(NetworkAccess { + root_key: desc.root_key, + root_key_source: RootKeySource::Managed, + api_url: gateway_url.clone(), + http_gateway_url: Some(gateway_url), + use_friendly_domains: desc.use_friendly_domains, + }) +} + +/// The URLs a running managed network is reached at. Its gateway serves the API +/// as well, so both URLs are the same one. +pub async fn get_managed_network_urls( + nd: NetworkDirectory, +) -> Result { + let (_, gateway_url) = managed_network_gateway(nd).await?; + Ok(NetworkUrls { + api_url: gateway_url.clone(), + http_gateway_url: Some(gateway_url), + }) +} + +/// A running managed network's descriptor and the URL its gateway is reachable +/// at. A network that is not running has no descriptor, and one whose fixed port +/// has since been taken by another project's network is not the network the +/// descriptor describes — both are errors rather than a URL nothing answers on. +async fn managed_network_gateway( + nd: NetworkDirectory, +) -> Result<(NetworkDescriptorModel, Url), GetNetworkAccessError> { // Load network descriptor let desc = nd .load_network_descriptor() @@ -118,13 +163,7 @@ pub async fn get_managed_network_access( } } let http_gateway_url = Url::parse(&format!("http://{}:{port}", desc.gateway.host)).unwrap(); - Ok(NetworkAccess { - root_key: desc.root_key, - root_key_source: RootKeySource::Managed, - api_url: http_gateway_url.clone(), - http_gateway_url: Some(http_gateway_url), - use_friendly_domains: desc.use_friendly_domains, - }) + Ok((desc, http_gateway_url)) } pub async fn get_connected_network_access( diff --git a/crates/icp/src/network/mod.rs b/crates/icp/src/network/mod.rs index 3d025fb40..f8faebbad 100644 --- a/crates/icp/src/network/mod.rs +++ b/crates/icp/src/network/mod.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Deserializer, Serialize}; use snafu::prelude::*; pub use crate::manifest::network::RootKeySpec; -pub use access::RootKeySource; +pub use access::{NetworkUrls, RootKeySource}; pub use directory::{LoadPidError, NetworkDirectory, SavePidError}; pub use managed::run::{RunNetworkError, run_network}; use strum::EnumString; @@ -20,7 +20,7 @@ use crate::{ }, network::access::{ GetNetworkAccessError, NetworkAccess, get_connected_network_access, - get_managed_network_access, + get_managed_network_access, get_managed_network_urls, }, prelude::*, project::DEFAULT_LOCAL_NETWORK_PORT, @@ -348,6 +348,11 @@ pub enum AccessError { pub trait Access: Sync + Send { fn get_network_directory(&self, network: &Network) -> Result; async fn access(&self, network: &Network) -> Result; + + /// The network's URLs alone. Unlike [`Access::access`] this resolves no root + /// key, so a caller that only needs an endpoint does not make a connected + /// network fetch one. + async fn urls(&self, network: &Network) -> Result; } pub struct Accessor { @@ -389,6 +394,21 @@ impl Access for Accessor { } } } + + async fn urls(&self, network: &Network) -> Result { + match &network.configuration { + Configuration::Managed { managed: _ } => { + let nd = self.get_network_directory(network)?; + Ok(get_managed_network_urls(nd).await?) + } + // A connected network's endpoints are configured, so there is + // nothing to resolve. + Configuration::Connected { connected: cfg } => Ok(NetworkUrls { + api_url: cfg.api_url.clone(), + http_gateway_url: cfg.http_gateway_url.clone(), + }), + } + } } #[cfg(test)] @@ -443,6 +463,14 @@ impl Access for MockNetworkAccessor { }, }) } + + async fn urls(&self, network: &Network) -> Result { + let access = self.access(network).await?; + Ok(NetworkUrls { + api_url: access.api_url, + http_gateway_url: access.http_gateway_url, + }) + } } #[cfg(test)] diff --git a/crates/icp/src/operations/deploy.rs b/crates/icp/src/operations/deploy.rs index 7501b701e..0d160947a 100644 --- a/crates/icp/src/operations/deploy.rs +++ b/crates/icp/src/operations/deploy.rs @@ -148,6 +148,9 @@ pub enum DeployError { #[snafu(transparent)] LoadProject { source: ProjectLoadError }, + #[snafu(transparent)] + NetworkUrls { source: crate::network::AccessError }, + #[snafu(transparent)] Sync { source: SyncOperationError }, } @@ -592,6 +595,7 @@ async fn sync( let pkg_cache = ctx.dirs.package_cache()?; let project_dir = ctx.project.load().await?.dir; + let urls = ctx.network.urls(&env.network).await?; let phase = reporter.task(Task::phase("Syncing canisters:")); let result = sync_many( @@ -601,6 +605,7 @@ async fn sync( project_dir, environment_selection.name().to_owned(), env.network.name.clone(), + urls, canister_ids, proxy, &pkg_cache, diff --git a/crates/icp/src/operations/sync.rs b/crates/icp/src/operations/sync.rs index 46be0dc6a..1dab5605b 100644 --- a/crates/icp/src/operations/sync.rs +++ b/crates/icp/src/operations/sync.rs @@ -1,6 +1,7 @@ use crate::{ Canister, canister::sync::{Params, Synchronize, SynchronizeError}, + network::NetworkUrls, package::PackageCache, prelude::{Path, PathBuf}, }; @@ -22,6 +23,7 @@ pub struct SyncOperationError { /// Synchronizes a single canister using its configured sync steps, returning /// the stderr lines the steps retained for the persistent output channel. +#[allow(clippy::too_many_arguments)] async fn sync_canister( syncer: &Arc, agent: &Agent, @@ -31,6 +33,7 @@ async fn sync_canister( canister_info: &Canister, environment: &str, network: &str, + urls: &NetworkUrls, canister_ids: &BTreeMap, proxy: Option, task: &TaskReporter, @@ -52,6 +55,7 @@ async fn sync_canister( name: canister_info.name.clone(), environment: environment.to_owned(), network: network.to_owned(), + urls: urls.clone(), canister_ids: canister_ids.clone(), proxy, }, @@ -91,6 +95,7 @@ pub async fn sync_many( project_dir: PathBuf, environment: String, network: String, + urls: NetworkUrls, canister_ids: BTreeMap, proxy: Option, pkg_cache: &PackageCache, @@ -106,6 +111,7 @@ pub async fn sync_many( let syncer = syncer.clone(); let environment = environment.clone(); let network = network.clone(); + let urls = urls.clone(); let canister_ids = canister_ids.clone(); let project_dir = project_dir.clone(); @@ -119,6 +125,7 @@ pub async fn sync_many( &canister_info, &environment, &network, + &urls, &canister_ids, proxy, &task, diff --git a/docs/concepts/sync-plugins.md b/docs/concepts/sync-plugins.md index 34200fff7..9efc2a393 100644 --- a/docs/concepts/sync-plugins.md +++ b/docs/concepts/sync-plugins.md @@ -71,6 +71,8 @@ The authoritative interface, including all record fields, lives in [`sync-plugin |-------|-------------| | `canister-id` | Textual principal of the canister being synced | | `environment` | Name of the environment being synced (e.g. `local`, `production`) | +| `api-url` | URL of the network's API endpoint, where the host submits the plugin's canister calls | +| `gateway-url` | URL of the network's HTTP gateway, or absent when it exposes none | | `dirs` | Those `files:` entries that name a directory; the host preopened each one read-only. Each carries its `key` (see below) and `path` | | `files` | Those `files:` entries that name a file, each with its `key`, `name` (path), and `content` read by the host | | `fields` | The key-value fields you declared in `fields:`, each as a `(name, value)` pair; values are strings | @@ -82,6 +84,8 @@ Each `canister-ids` entry's name is the canister's fully-qualified project key: Every canister the subproject being synced can name for itself is additionally listed under that name, so a plugin looks a canister up by the name that subproject's own manifest uses. For a canister in `services/crm`, that means its siblings under their bare local names, and the canisters of its own dependencies under keys relative to it — `services/crm/vendor/ledger:ledger` is also listed as `vendor/ledger:ledger`. Those are the names the subproject uses when it is deployed on its own, so a plugin written against them keeps working once the subproject is vendored into a workspace. Such a name always means what the subproject means by it: a canister elsewhere in the workspace whose key is spelled the same way is not listed under it for that sync. +Both URLs are informational: the plugin has no sockets of its own (see [The Sandbox](#the-sandbox)), so they are there to be composed into a URL — the public address of the canister just synced, say — or handed to a canister, not fetched. They arrive normalized, so a URL with no path carries a trailing slash (`http://127.0.0.1:4943/`). A network reached through a single URL, which is the usual local case, reports that one URL as both. + The manifest declares directories and files together under `files:`; the host splits them into these two lists by what is on disk, so a plugin never has to say up front which an entry will turn out to be. Every entry carries a `key`: the name it was declared under in the manifest. A name holding a list of paths produces several entries sharing that key, so the key is not unique. Use it to group or label declared paths — e.g. distinguish `seed:` directories from `migrations:` directories — without hardcoding paths in the plugin. diff --git a/docs/guides/writing-sync-plugins.md b/docs/guides/writing-sync-plugins.md index c1cfd9715..0f7722d28 100644 --- a/docs/guides/writing-sync-plugins.md +++ b/docs/guides/writing-sync-plugins.md @@ -149,6 +149,19 @@ for field in &input.fields { A value always arrives as a string, so parse the ones you want as another type — a manifest may write `retries: 3` unquoted, and the plugin receives `"3"`. +## Know Where the Network Is + +`input.api_url` is the endpoint the host submits your canister calls to, and `input.gateway_url` is the HTTP gateway serving canisters over HTTP — absent when the network exposes none. You have no sockets, so neither is something to fetch: use them to tell the user where something landed, or to hand a canister the address it is reachable at. + +```rust +match &input.gateway_url { + Some(gateway) => eprintln!("{} is served from {gateway}", input.canister_id), + None => eprintln!("{} synced ({} has no HTTP gateway)", input.canister_id, input.environment), +} +``` + +Both arrive normalized, so a URL with no path carries a trailing slash (`http://127.0.0.1:4943/`) — strip it before joining a path onto it. + ## Build ```bash diff --git a/examples/icp-sync-plugin/plugin/src/lib.rs b/examples/icp-sync-plugin/plugin/src/lib.rs index 09595a47d..b10817e74 100644 --- a/examples/icp-sync-plugin/plugin/src/lib.rs +++ b/examples/icp-sync-plugin/plugin/src/lib.rs @@ -17,6 +17,14 @@ impl Guest for Plugin { input.canister_id, input.environment ); + // Report where the network being synced is reached. The plugin has no + // sockets of its own, so these are for the user reading the output (or + // for handing a canister its own public address), not for fetching. + match &input.gateway_url { + Some(gateway) => eprintln!("gateway: {gateway} (api: {})", input.api_url), + None => eprintln!("no HTTP gateway (api: {})", input.api_url), + } + // 1. Report the canister's Candid interface, read from its metadata. // Reported rather than required: the section is only there if the // build embedded it (this project's build does, via ic-wasm).