From c31d5493b867bca75b7b9216c48bbefa0ca298cb Mon Sep 17 00:00:00 2001 From: mraszyk <31483726+mraszyk@users.noreply.github.com> Date: Mon, 11 Nov 2024 15:23:09 +0100 Subject: [PATCH] feat: add new operation to PocketIC to get canister controllers (#2495) This PR adds a new PocketIC operation to get the controllers of a canister. --- packages/pocket-ic/CHANGELOG.md | 1 + packages/pocket-ic/src/common/rest.rs | 22 +++++++++ packages/pocket-ic/src/lib.rs | 8 +++ packages/pocket-ic/src/nonblocking.rs | 20 +++++++- packages/pocket-ic/tests/tests.rs | 32 ++++++++++++ rs/pocket_ic_server/CHANGELOG.md | 1 + rs/pocket_ic_server/src/pocket_ic.rs | 24 +++++++++ rs/pocket_ic_server/src/state_api/routes.rs | 54 +++++++++++++++++++-- rs/pocket_ic_server/src/state_api/state.rs | 9 +++- rs/state_machine_tests/src/lib.rs | 22 +++++++-- 10 files changed, 180 insertions(+), 13 deletions(-) diff --git a/packages/pocket-ic/CHANGELOG.md b/packages/pocket-ic/CHANGELOG.md index 4fd4549e84e..ef2eb4cd94f 100644 --- a/packages/pocket-ic/CHANGELOG.md +++ b/packages/pocket-ic/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - The function `PocketIcBuilder::new_with_config` to specify a custom `ExtendedSubnetConfigSet`. - The function `PocketIcBuilder::with_subnet_state` to load subnet state from a state directory for an arbitrary subnet kind and subnet id. - The function `get_default_effective_canister_id` to retrieve a default effective canister id for canister creation on a PocketIC instance. +- The function `PocketIc::get_controllers` to get the controllers of a canister. ### Removed - Functions `PocketIc::from_config`, `PocketIc::from_config_and_max_request_time`, and `PocketIc::from_config_and_server_url`. diff --git a/packages/pocket-ic/src/common/rest.rs b/packages/pocket-ic/src/common/rest.rs index dd164346c12..a48cce19f0d 100644 --- a/packages/pocket-ic/src/common/rest.rs +++ b/packages/pocket-ic/src/common/rest.rs @@ -241,6 +241,28 @@ pub struct RawCycles { pub cycles: u128, } +#[derive(Clone, Serialize, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema)] +pub struct RawPrincipalId { + // raw bytes of the principal + #[serde(deserialize_with = "base64::deserialize")] + #[serde(serialize_with = "base64::serialize")] + pub principal_id: Vec, +} + +impl From for RawPrincipalId { + fn from(principal: Principal) -> Self { + Self { + principal_id: principal.as_slice().to_vec(), + } + } +} + +impl From for Principal { + fn from(raw_principal_id: RawPrincipalId) -> Self { + Principal::from_slice(&raw_principal_id.principal_id) + } +} + #[derive(Clone, Serialize, Eq, PartialEq, Ord, PartialOrd, Deserialize, Debug, JsonSchema)] pub struct RawCanisterId { // raw bytes of the principal diff --git a/packages/pocket-ic/src/lib.rs b/packages/pocket-ic/src/lib.rs index 649d753d025..62bd0f0fa7f 100644 --- a/packages/pocket-ic/src/lib.rs +++ b/packages/pocket-ic/src/lib.rs @@ -567,6 +567,14 @@ impl PocketIc { runtime.block_on(async { self.pocket_ic.advance_time(duration).await }) } + /// Get the controllers of a canister. + /// Panics if the canister does not exist. + #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))] + pub fn get_controllers(&self, canister_id: CanisterId) -> Vec { + let runtime = self.runtime.clone(); + runtime.block_on(async { self.pocket_ic.get_controllers(canister_id).await }) + } + /// Get the current cycles balance of a canister. #[instrument(ret, skip(self), fields(instance_id=self.pocket_ic.instance_id, canister_id = %canister_id.to_string()))] pub fn cycle_balance(&self, canister_id: CanisterId) -> u128 { diff --git a/packages/pocket-ic/src/nonblocking.rs b/packages/pocket-ic/src/nonblocking.rs index dc2d38c44e8..134ba2c681c 100644 --- a/packages/pocket-ic/src/nonblocking.rs +++ b/packages/pocket-ic/src/nonblocking.rs @@ -4,8 +4,8 @@ use crate::common::rest::{ HttpGatewayConfig, HttpGatewayInfo, HttpsConfig, InstanceConfig, InstanceId, MockCanisterHttpResponse, RawAddCycles, RawCanisterCall, RawCanisterHttpRequest, RawCanisterId, RawCanisterResult, RawCycles, RawEffectivePrincipal, RawMessageId, RawMockCanisterHttpResponse, - RawSetStableMemory, RawStableMemory, RawSubmitIngressResult, RawSubnetId, RawTime, - RawVerifyCanisterSigArg, RawWasmResult, SubnetId, Topology, + RawPrincipalId, RawSetStableMemory, RawStableMemory, RawSubmitIngressResult, RawSubnetId, + RawTime, RawVerifyCanisterSigArg, RawWasmResult, SubnetId, Topology, }; use crate::management_canister::{ CanisterId, CanisterIdRecord, CanisterInstallMode, CanisterInstallModeUpgradeInner, @@ -456,6 +456,22 @@ impl PocketIc { self.set_time(now + duration).await; } + /// Get the controllers of a canister. + /// Panics if the canister does not exist. + #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))] + pub async fn get_controllers(&self, canister_id: CanisterId) -> Vec { + let endpoint = "read/get_controllers"; + let result: Vec = self + .post( + endpoint, + RawCanisterId { + canister_id: canister_id.as_slice().to_vec(), + }, + ) + .await; + result.into_iter().map(|p| p.into()).collect() + } + /// Get the current cycles balance of a canister. #[instrument(ret, skip(self), fields(instance_id=self.instance_id, canister_id = %canister_id.to_string()))] pub async fn cycle_balance(&self, canister_id: CanisterId) -> u128 { diff --git a/packages/pocket-ic/tests/tests.rs b/packages/pocket-ic/tests/tests.rs index 9edf0a6bda7..591b6160ef1 100644 --- a/packages/pocket-ic/tests/tests.rs +++ b/packages/pocket-ic/tests/tests.rs @@ -1595,3 +1595,35 @@ fn test_get_default_effective_canister_id_invalid_url() { err => panic!("Unexpected error: {}", err), }; } + +#[test] +fn get_controllers() { + let pic = PocketIc::new(); + + let canister_id = pic.create_canister(); + + let controllers = pic.get_controllers(canister_id); + assert_eq!(controllers, vec![Principal::anonymous()]); + + let user_id = Principal::from_slice(&[u8::MAX; 29]); + pic.set_controllers(canister_id, None, vec![Principal::anonymous(), user_id]) + .unwrap(); + + let controllers = pic.get_controllers(canister_id); + assert_eq!(controllers.len(), 2); + assert!(controllers.contains(&Principal::anonymous())); + assert!(controllers.contains(&user_id)); +} + +#[test] +#[should_panic(expected = "CanisterNotFound(CanisterId")] +fn get_controllers_of_nonexisting_canister() { + let pic = PocketIc::new(); + + let canister_id = pic.create_canister(); + pic.add_cycles(canister_id, 100_000_000_000_000); + pic.stop_canister(canister_id, None).unwrap(); + pic.delete_canister(canister_id, None).unwrap(); + + let _ = pic.get_controllers(canister_id); +} diff --git a/rs/pocket_ic_server/CHANGELOG.md b/rs/pocket_ic_server/CHANGELOG.md index 94bd1a30f3d..51162e725e7 100644 --- a/rs/pocket_ic_server/CHANGELOG.md +++ b/rs/pocket_ic_server/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 of the endpoint `/instances/` to create a new PocketIC instance. - New endpoint `/instances//_/topology` returning the topology of the PocketIC instance. - New CLI option `--log-levels` to specify the log levels for PocketIC server logs (defaults to `pocket_ic_server=info,tower_http=info,axum::rejection=trace`). +- New endpoint `/instances/ OpOut { + let subnet = pic.try_route_canister(self.canister_id); + match subnet { + Some(subnet) => subnet + .get_controllers(self.canister_id) + .map(OpOut::Controllers) + .unwrap_or(OpOut::Error(PocketIcError::CanisterNotFound( + self.canister_id, + ))), + None => OpOut::Error(PocketIcError::CanisterNotFound(self.canister_id)), + } + } + + fn id(&self) -> OpId { + OpId(format!("get_controllers({})", self.canister_id)) + } +} + #[derive(Clone, Debug)] pub struct GetCyclesBalance { pub canister_id: CanisterId, diff --git a/rs/pocket_ic_server/src/state_api/routes.rs b/rs/pocket_ic_server/src/state_api/routes.rs index 3c8a7cca28b..4221bb48ab6 100644 --- a/rs/pocket_ic_server/src/state_api/routes.rs +++ b/rs/pocket_ic_server/src/state_api/routes.rs @@ -8,9 +8,10 @@ use super::state::{ApiState, OpOut, PocketIcError, StateLabel, UpdateReply}; use crate::pocket_ic::{ AddCycles, AwaitIngressMessage, CallRequest, CallRequestVersion, CanisterReadStateRequest, - DashboardRequest, ExecuteIngressMessage, GetCanisterHttp, GetCyclesBalance, GetStableMemory, - GetSubnet, GetTime, GetTopology, MockCanisterHttp, PubKey, Query, QueryRequest, - SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage, SubnetReadStateRequest, Tick, + DashboardRequest, ExecuteIngressMessage, GetCanisterHttp, GetControllers, GetCyclesBalance, + GetStableMemory, GetSubnet, GetTime, GetTopology, MockCanisterHttp, PubKey, Query, + QueryRequest, SetStableMemory, SetTime, StatusRequest, SubmitIngressMessage, + SubnetReadStateRequest, Tick, }; use crate::{async_trait, pocket_ic::PocketIc, BlobStore, InstanceId, OpId, Operation}; use aide::{ @@ -38,8 +39,8 @@ use pocket_ic::common::rest::{ self, ApiResponse, AutoProgressConfig, ExtendedSubnetConfigSet, HttpGatewayConfig, HttpGatewayDetails, InstanceConfig, MockCanisterHttpResponse, RawAddCycles, RawCanisterCall, RawCanisterHttpRequest, RawCanisterId, RawCanisterResult, RawCycles, RawMessageId, - RawMockCanisterHttpResponse, RawSetStableMemory, RawStableMemory, RawSubmitIngressResult, - RawSubnetId, RawTime, RawWasmResult, Topology, + RawMockCanisterHttpResponse, RawPrincipalId, RawSetStableMemory, RawStableMemory, + RawSubmitIngressResult, RawSubnetId, RawTime, RawWasmResult, Topology, }; use pocket_ic::WasmResult; use serde::Serialize; @@ -75,6 +76,7 @@ where .directory_route("/topology", get(handler_topology)) .directory_route("/get_time", get(handler_get_time)) .directory_route("/get_canister_http", get(handler_get_canister_http)) + .directory_route("/get_controllers", post(handler_get_controllers)) .directory_route("/get_cycles", post(handler_get_cycles)) .directory_route("/get_stable_memory", post(handler_get_stable_memory)) .directory_route("/get_subnet", post(handler_get_subnet)) @@ -363,6 +365,19 @@ impl TryFrom for () { } } +impl TryFrom for Vec { + type Error = OpConversionError; + fn try_from(value: OpOut) -> Result { + match value { + OpOut::Controllers(controllers) => Ok(controllers + .into_iter() + .map(|principal_id| principal_id.0.into()) + .collect()), + _ => Err(OpConversionError), + } + } +} + impl TryFrom for RawCycles { type Error = OpConversionError; fn try_from(value: OpOut) -> Result { @@ -583,6 +598,28 @@ pub async fn handler_mock_canister_http( (code, Json(response)) } +pub async fn handler_get_controllers( + State(AppState { api_state, .. }): State, + Path(instance_id): Path, + headers: HeaderMap, + extract::Json(raw_canister_id): extract::Json, +) -> (StatusCode, Json>>) { + let timeout = timeout_or_default(headers); + match CanisterId::try_from(raw_canister_id.canister_id) { + Ok(canister_id) => { + let get_op = GetControllers { canister_id }; + let (code, response) = run_operation(api_state, instance_id, timeout, get_op).await; + (code, Json(response)) + } + Err(e) => ( + StatusCode::BAD_REQUEST, + Json(ApiResponse::Error { + message: format!("{:?}", e), + }), + ), + } +} + pub async fn handler_get_cycles( State(AppState { api_state, .. }): State, Path(instance_id): Path, @@ -803,6 +840,13 @@ async fn op_out_to_response(op_out: OpOut) -> Response { )), ) .into_response(), + opout @ OpOut::Controllers(_) => ( + StatusCode::OK, + Json(ApiResponse::Success( + Vec::::try_from(opout).unwrap(), + )), + ) + .into_response(), opout @ OpOut::Cycles(_) => ( StatusCode::OK, Json(ApiResponse::Success(RawCycles::try_from(opout).unwrap())), diff --git a/rs/pocket_ic_server/src/state_api/state.rs b/rs/pocket_ic_server/src/state_api/state.rs index b9988a75fd9..306eca91b34 100644 --- a/rs/pocket_ic_server/src/state_api/state.rs +++ b/rs/pocket_ic_server/src/state_api/state.rs @@ -33,7 +33,8 @@ use hyper::{Request, Response as HyperResponse}; use hyper_util::client::legacy::{connect::HttpConnector, Client}; use ic_http_endpoints_public::cors_layer; use ic_http_gateway::{CanisterRequest, HttpGatewayClient, HttpGatewayRequestArgs}; -use ic_types::{canister_http::CanisterHttpRequestId, CanisterId, SubnetId}; +use ic_types::{canister_http::CanisterHttpRequestId, CanisterId, PrincipalId, SubnetId}; +use itertools::Itertools; use pocket_ic::common::rest::{ CanisterHttpRequest, HttpGatewayBackend, HttpGatewayConfig, HttpGatewayDetails, HttpGatewayInfo, Topology, @@ -206,6 +207,7 @@ pub enum OpOut { Time(u64), CanisterResult(Result), CanisterId(CanisterId), + Controllers(Vec), Cycles(u128), Bytes(Vec), StableMemBytes(Vec), @@ -252,6 +254,11 @@ impl std::fmt::Debug for OpOut { OpOut::Time(x) => write!(f, "Time({})", x), OpOut::Topology(t) => write!(f, "Topology({:?})", t), OpOut::CanisterId(cid) => write!(f, "CanisterId({})", cid), + OpOut::Controllers(controllers) => write!( + f, + "Controllers({})", + controllers.iter().map(|c| c.to_string()).join(",") + ), OpOut::Cycles(x) => write!(f, "Cycles({})", x), OpOut::CanisterResult(Ok(x)) => write!(f, "CanisterResult: Ok({:?})", x), OpOut::CanisterResult(Err(x)) => write!(f, "CanisterResult: Err({})", x), diff --git a/rs/state_machine_tests/src/lib.rs b/rs/state_machine_tests/src/lib.rs index 3dbd9aec511..22a5dbb4283 100644 --- a/rs/state_machine_tests/src/lib.rs +++ b/rs/state_machine_tests/src/lib.rs @@ -2705,6 +2705,14 @@ impl StateMachine { )) } + /// Returns the controllers of a canister or `None` if the canister does not exist. + pub fn get_controllers(&self, canister_id: CanisterId) -> Option> { + let state = self.state_manager.get_latest_state().take(); + state + .canister_state(&canister_id) + .map(|s| s.controllers().iter().cloned().collect()) + } + pub fn install_wasm_in_mode( &self, canister_id: CanisterId, @@ -2712,11 +2720,15 @@ impl StateMachine { wasm: Vec, payload: Vec, ) -> Result<(), UserError> { - let state = self.state_manager.get_latest_state().take(); - let sender = state - .canister_state(&canister_id) - .and_then(|s| s.controllers().iter().next().cloned()) - .unwrap_or_else(PrincipalId::new_anonymous); + let sender = self + .get_controllers(canister_id) + .map(|controllers| { + controllers + .into_iter() + .next() + .unwrap_or(PrincipalId::new_anonymous()) + }) + .unwrap_or(PrincipalId::new_anonymous()); self.execute_ingress_as( sender, ic00::IC_00,