diff --git a/crates/icp-cli/src/operations/create.rs b/crates/icp-cli/src/operations/create.rs index c9aa9bf3..833d6f05 100644 --- a/crates/icp-cli/src/operations/create.rs +++ b/crates/icp-cli/src/operations/create.rs @@ -107,9 +107,7 @@ impl CreateOperation { .get_subnet_by_id(&selected_subnet) .await .context(GetSubnetSnafu)?; - let cid = if let Some(SubnetType::Unknown(kind)) = subnet_info.subnet_type() - && kind == "cloud_engine" - { + let cid = if let Some(SubnetType::CloudEngine) = subnet_info.subnet_type() { self.create_mgmt(settings, &subnet_info).await? } else { self.create_ledger(settings, selected_subnet).await? diff --git a/crates/icp-cli/tests/canister_create_tests.rs b/crates/icp-cli/tests/canister_create_tests.rs index 77373a0a..aa21b6e9 100644 --- a/crates/icp-cli/tests/canister_create_tests.rs +++ b/crates/icp-cli/tests/canister_create_tests.rs @@ -4,8 +4,12 @@ use predicates::{ prelude::PredicateBooleanExt, str::{contains, starts_with}, }; +use test_tag::tag; -use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; +use crate::common::{ + ENVIRONMENT_DOCKER_ENGINE, ENVIRONMENT_RANDOM_PORT, NETWORK_DOCKER_ENGINE, NETWORK_RANDOM_PORT, + TestContext, clients, +}; use icp::{fs::write_string, prelude::*}; mod common; @@ -537,3 +541,77 @@ async fn canister_create_detached() { .assert() .failure(); } + +#[tag(docker)] +#[tokio::test] +async fn canister_create_cloud_engine() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: echo hi + + {NETWORK_DOCKER_ENGINE} + {ENVIRONMENT_DOCKER_ENGINE} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + ctx.docker_pull_engine_network(); + let _guard = ctx + .start_network_in(&project_dir, "docker-engine-network") + .await; + ctx.ping_until_healthy(&project_dir, "docker-engine-network"); + + // Find the CloudEngine subnet by querying the topology endpoint + // TODO replace with a subnet selection parameter once we have one + let topology_url = ctx.gateway_url().join("/_/topology").unwrap(); + let topology: serde_json::Value = reqwest::get(topology_url) + .await + .expect("failed to fetch topology") + .json() + .await + .expect("failed to parse topology"); + + let subnet_configs = topology["subnet_configs"] + .as_object() + .expect("subnet_configs should be an object"); + let cloud_engine_subnet_id = subnet_configs + .iter() + .find_map(|(id, config)| { + (config["subnet_kind"].as_str()? == "CloudEngine").then_some(id.clone()) + }) + .expect("no CloudEngine subnet found in topology"); + + // Create the canister on the CloudEngine subnet + // Only the admin can do this. In local envs, the admin is the anonymous principal + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "create", + "my-canister", + "--subnet", + &cloud_engine_subnet_id, + "--environment", + "docker-engine-environment", + ]) + .assert() + .success(); + + let id_mapping_path = project_dir + .join(".icp") + .join("cache") + .join("mappings") + .join("docker-engine-environment.ids.json"); + assert!( + id_mapping_path.exists(), + "ID mapping file should exist at {id_mapping_path}" + ); +} diff --git a/crates/icp-cli/tests/common/context.rs b/crates/icp-cli/tests/common/context.rs index 3f0ff6ae..3f5a7f36 100644 --- a/crates/icp-cli/tests/common/context.rs +++ b/crates/icp-cli/tests/common/context.rs @@ -395,6 +395,10 @@ impl TestContext { .expect("Failed to write network descriptor file"); } + pub(crate) fn gateway_url(&self) -> &Url { + self.http_gateway_url.get().unwrap() + } + pub(crate) fn agent(&self) -> Agent { let agent = Agent::builder() .with_url(self.http_gateway_url.get().unwrap().as_str()) @@ -441,18 +445,21 @@ impl TestContext { } pub(crate) fn docker_pull_network(&self) { + self.docker_pull_image("ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0"); + } + + pub(crate) fn docker_pull_engine_network(&self) { + self.docker_pull_image("ghcr.io/dfinity/icp-cli-network-launcher:engine-beta"); + } + + fn docker_pull_image(&self, image: &str) { let platform = if cfg!(target_arch = "aarch64") { "linux/arm64" } else { "linux/amd64" }; Command::new("docker") - .args([ - "pull", - "--platform", - platform, - "ghcr.io/dfinity/icp-cli-network-launcher:v11.0.0", - ]) + .args(["pull", "--platform", platform, image]) .assert() .success(); } diff --git a/crates/icp-cli/tests/common/mod.rs b/crates/icp-cli/tests/common/mod.rs index 7ec02d52..30694f63 100644 --- a/crates/icp-cli/tests/common/mod.rs +++ b/crates/icp-cli/tests/common/mod.rs @@ -49,6 +49,22 @@ environments: network: docker-network "#; +pub(crate) const NETWORK_DOCKER_ENGINE: &str = r#" +networks: + - name: docker-engine-network + mode: managed + image: ghcr.io/dfinity/icp-cli-network-launcher:engine-beta + port-mapping: + - 0:4943 + - 0:4942 +"#; + +pub(crate) const ENVIRONMENT_DOCKER_ENGINE: &str = r#" +environments: + - name: docker-engine-environment + network: docker-engine-network +"#; + /// This ID is dependent on the toplogy being served by pocket-ic /// NOTE: If the topology is changed (another subnet is added, etc) the ID may change. /// References: diff --git a/crates/icp-cli/tests/deploy_tests.rs b/crates/icp-cli/tests/deploy_tests.rs index b18d7b40..37e05271 100644 --- a/crates/icp-cli/tests/deploy_tests.rs +++ b/crates/icp-cli/tests/deploy_tests.rs @@ -3,8 +3,12 @@ use predicates::{ ord::eq, str::{PredicateStrExt, contains}, }; +use test_tag::tag; -use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext, clients}; +use crate::common::{ + ENVIRONMENT_DOCKER_ENGINE, ENVIRONMENT_RANDOM_PORT, NETWORK_DOCKER_ENGINE, NETWORK_RANDOM_PORT, + TestContext, clients, +}; use icp::{ fs::{create_dir_all, write_string}, prelude::*, @@ -599,3 +603,83 @@ async fn deploy_upgrade_rejects_incompatible_candid() { .success() .stdout(eq("(\"Hello, 42!\")").trim()); } + +#[tag(docker)] +#[tokio::test] +async fn deploy_cloud_engine() { + let ctx = TestContext::new(); + + let project_dir = ctx.create_project_dir("icp"); + + let wasm = ctx.make_asset("example_icp_mo.wasm"); + + let pm = formatdoc! {r#" + canisters: + - name: my-canister + build: + steps: + - type: script + command: cp '{wasm}' "$ICP_WASM_OUTPUT_PATH" + + {NETWORK_DOCKER_ENGINE} + {ENVIRONMENT_DOCKER_ENGINE} + "#}; + + write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); + + ctx.docker_pull_engine_network(); + let _guard = ctx + .start_network_in(&project_dir, "docker-engine-network") + .await; + ctx.ping_until_healthy(&project_dir, "docker-engine-network"); + + // Find the CloudEngine subnet by querying the topology endpoint + // TODO replace with a subnet selection parameter once we have one + let topology_url = ctx.gateway_url().join("/_/topology").unwrap(); + let topology: serde_json::Value = reqwest::get(topology_url) + .await + .expect("failed to fetch topology") + .json() + .await + .expect("failed to parse topology"); + + let subnet_configs = topology["subnet_configs"] + .as_object() + .expect("subnet_configs should be an object"); + let cloud_engine_subnet_id = subnet_configs + .iter() + .find_map(|(id, config)| { + (config["subnet_kind"].as_str()? == "CloudEngine").then_some(id.clone()) + }) + .expect("no CloudEngine subnet found in topology"); + + // Deploy to the CloudEngine subnet + // Only the admin can do this. In local envs, the admin is the anonymous principal + ctx.icp() + .current_dir(&project_dir) + .args([ + "deploy", + "--subnet", + &cloud_engine_subnet_id, + "--environment", + "docker-engine-environment", + ]) + .assert() + .success(); + + // Query canister to verify it works + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "docker-engine-environment", + "my-canister", + "greet", + "(\"test\")", + ]) + .assert() + .success() + .stdout(eq("(\"Hello, test!\")").trim()); +}