diff --git a/apps/wallet/src/generated/station/station.did b/apps/wallet/src/generated/station/station.did index 74e3d96d..993217c1 100644 --- a/apps/wallet/src/generated/station/station.did +++ b/apps/wallet/src/generated/station/station.did @@ -847,6 +847,21 @@ type RestoreExternalCanisterOperation = record { input : RestoreExternalCanisterOperationInput; }; +type PruneExternalCanisterOperationInput = record { + // The canister to prune. + canister_id : principal; + // The resource to prune. + prune : variant { + chunk_store; + snapshot : text; + state; + }; +}; + +type PruneExternalCanisterOperation = record { + input : PruneExternalCanisterOperationInput; +}; + type EditPermissionOperationInput = record { // The updated resource that this policy will apply to. resource : Resource; @@ -942,6 +957,8 @@ type RequestOperation = variant { SnapshotExternalCanister : SnapshotExternalCanisterOperation; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : RestoreExternalCanisterOperation; + // An operation for pruning an external canister. + PruneExternalCanister : PruneExternalCanisterOperation; // An operation for editing an permission. EditPermission : EditPermissionOperation; // An operation for adding a request policy. @@ -995,6 +1012,8 @@ type RequestOperationInput = variant { SnapshotExternalCanister : SnapshotExternalCanisterOperationInput; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : RestoreExternalCanisterOperationInput; + // An operation for pruning an external canister. + PruneExternalCanister : PruneExternalCanisterOperationInput; // An operation for editing an permission. EditPermission : EditPermissionOperationInput; // An operation for adding a request policy. @@ -1048,6 +1067,8 @@ type RequestOperationType = variant { SnapshotExternalCanister; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister; + // An operation for pruning an external canister. + PruneExternalCanister; // An operation for editing an permission. EditPermission; // An operation for adding a request policy. @@ -1200,6 +1221,8 @@ type ListRequestsOperationType = variant { SnapshotExternalCanister : opt principal; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : opt principal; + // An operation for pruning an external canister. + PruneExternalCanister : opt principal; // An operation for editing an permission. EditPermission; // An operation for adding a request policy. diff --git a/apps/wallet/src/generated/station/station.did.d.ts b/apps/wallet/src/generated/station/station.did.d.ts index 80cefc5c..bf5d78fb 100644 --- a/apps/wallet/src/generated/station/station.did.d.ts +++ b/apps/wallet/src/generated/station/station.did.d.ts @@ -759,6 +759,7 @@ export interface ListRequestsInput { export type ListRequestsOperationType = { 'AddUserGroup' : null } | { 'EditPermission' : null } | { 'SnapshotExternalCanister' : [] | [Principal] } | + { 'PruneExternalCanister' : [] | [Principal] } | { 'ConfigureExternalCanister' : [] | [Principal] } | { 'ChangeExternalCanister' : [] | [Principal] } | { 'AddUser' : null } | @@ -895,6 +896,15 @@ export interface PermissionCallerPrivileges { } export type PermissionResourceAction = { 'Read' : null } | { 'Update' : null }; +export interface PruneExternalCanisterOperation { + 'input' : PruneExternalCanisterOperationInput, +} +export interface PruneExternalCanisterOperationInput { + 'canister_id' : Principal, + 'prune' : { 'snapshot' : string } | + { 'state' : null } | + { 'chunk_store' : null }, +} export interface Quorum { 'min_approved' : number, 'approvers' : UserSpecifier } export interface QuorumPercentage { 'min_approved' : number, @@ -955,6 +965,7 @@ export type RequestExecutionSchedule = { 'Immediate' : null } | export type RequestOperation = { 'AddUserGroup' : AddUserGroupOperation } | { 'EditPermission' : EditPermissionOperation } | { 'SnapshotExternalCanister' : SnapshotExternalCanisterOperation } | + { 'PruneExternalCanister' : PruneExternalCanisterOperation } | { 'ConfigureExternalCanister' : ConfigureExternalCanisterOperation } | { 'ChangeExternalCanister' : ChangeExternalCanisterOperation } | { 'AddUser' : AddUserOperation } | @@ -982,6 +993,7 @@ export type RequestOperationInput = { } | { 'EditPermission' : EditPermissionOperationInput } | { 'SnapshotExternalCanister' : SnapshotExternalCanisterOperationInput } | + { 'PruneExternalCanister' : PruneExternalCanisterOperationInput } | { 'ConfigureExternalCanister' : ConfigureExternalCanisterOperationInput } | { 'ChangeExternalCanister' : ChangeExternalCanisterOperationInput } | { 'AddUser' : AddUserOperationInput } | @@ -1007,6 +1019,7 @@ export type RequestOperationInput = { export type RequestOperationType = { 'AddUserGroup' : null } | { 'EditPermission' : null } | { 'SnapshotExternalCanister' : null } | + { 'PruneExternalCanister' : null } | { 'ConfigureExternalCanister' : null } | { 'ChangeExternalCanister' : null } | { 'AddUser' : null } | diff --git a/apps/wallet/src/generated/station/station.did.js b/apps/wallet/src/generated/station/station.did.js index a86ea8a8..195db363 100644 --- a/apps/wallet/src/generated/station/station.did.js +++ b/apps/wallet/src/generated/station/station.did.js @@ -158,6 +158,17 @@ export const idlFactory = ({ IDL }) => { 'input' : SnapshotExternalCanisterOperationInput, 'snapshot_id' : IDL.Opt(IDL.Text), }); + const PruneExternalCanisterOperationInput = IDL.Record({ + 'canister_id' : IDL.Principal, + 'prune' : IDL.Variant({ + 'snapshot' : IDL.Text, + 'state' : IDL.Null, + 'chunk_store' : IDL.Null, + }), + }); + const PruneExternalCanisterOperation = IDL.Record({ + 'input' : PruneExternalCanisterOperationInput, + }); const Allow = IDL.Record({ 'user_groups' : IDL.Vec(UUID), 'auth_scope' : AuthScope, @@ -617,6 +628,7 @@ export const idlFactory = ({ IDL }) => { 'AddUserGroup' : AddUserGroupOperation, 'EditPermission' : EditPermissionOperation, 'SnapshotExternalCanister' : SnapshotExternalCanisterOperation, + 'PruneExternalCanister' : PruneExternalCanisterOperation, 'ConfigureExternalCanister' : ConfigureExternalCanisterOperation, 'ChangeExternalCanister' : ChangeExternalCanisterOperation, 'AddUser' : AddUserOperation, @@ -764,6 +776,7 @@ export const idlFactory = ({ IDL }) => { 'AddUserGroup' : AddUserGroupOperationInput, 'EditPermission' : EditPermissionOperationInput, 'SnapshotExternalCanister' : SnapshotExternalCanisterOperationInput, + 'PruneExternalCanister' : PruneExternalCanisterOperationInput, 'ConfigureExternalCanister' : ConfigureExternalCanisterOperationInput, 'ChangeExternalCanister' : ChangeExternalCanisterOperationInput, 'AddUser' : AddUserOperationInput, @@ -962,6 +975,7 @@ export const idlFactory = ({ IDL }) => { 'AddUserGroup' : IDL.Null, 'EditPermission' : IDL.Null, 'SnapshotExternalCanister' : IDL.Opt(IDL.Principal), + 'PruneExternalCanister' : IDL.Opt(IDL.Principal), 'ConfigureExternalCanister' : IDL.Opt(IDL.Principal), 'ChangeExternalCanister' : IDL.Opt(IDL.Principal), 'AddUser' : IDL.Null, @@ -1196,6 +1210,7 @@ export const idlFactory = ({ IDL }) => { 'AddUserGroup' : IDL.Null, 'EditPermission' : IDL.Null, 'SnapshotExternalCanister' : IDL.Null, + 'PruneExternalCanister' : IDL.Null, 'ConfigureExternalCanister' : IDL.Null, 'ChangeExternalCanister' : IDL.Null, 'AddUser' : IDL.Null, diff --git a/core/station/api/spec.did b/core/station/api/spec.did index 74e3d96d..993217c1 100644 --- a/core/station/api/spec.did +++ b/core/station/api/spec.did @@ -847,6 +847,21 @@ type RestoreExternalCanisterOperation = record { input : RestoreExternalCanisterOperationInput; }; +type PruneExternalCanisterOperationInput = record { + // The canister to prune. + canister_id : principal; + // The resource to prune. + prune : variant { + chunk_store; + snapshot : text; + state; + }; +}; + +type PruneExternalCanisterOperation = record { + input : PruneExternalCanisterOperationInput; +}; + type EditPermissionOperationInput = record { // The updated resource that this policy will apply to. resource : Resource; @@ -942,6 +957,8 @@ type RequestOperation = variant { SnapshotExternalCanister : SnapshotExternalCanisterOperation; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : RestoreExternalCanisterOperation; + // An operation for pruning an external canister. + PruneExternalCanister : PruneExternalCanisterOperation; // An operation for editing an permission. EditPermission : EditPermissionOperation; // An operation for adding a request policy. @@ -995,6 +1012,8 @@ type RequestOperationInput = variant { SnapshotExternalCanister : SnapshotExternalCanisterOperationInput; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : RestoreExternalCanisterOperationInput; + // An operation for pruning an external canister. + PruneExternalCanister : PruneExternalCanisterOperationInput; // An operation for editing an permission. EditPermission : EditPermissionOperationInput; // An operation for adding a request policy. @@ -1048,6 +1067,8 @@ type RequestOperationType = variant { SnapshotExternalCanister; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister; + // An operation for pruning an external canister. + PruneExternalCanister; // An operation for editing an permission. EditPermission; // An operation for adding a request policy. @@ -1200,6 +1221,8 @@ type ListRequestsOperationType = variant { SnapshotExternalCanister : opt principal; // An operation for restoring an external canister from a snapshot. RestoreExternalCanister : opt principal; + // An operation for pruning an external canister. + PruneExternalCanister : opt principal; // An operation for editing an permission. EditPermission; // An operation for adding a request policy. diff --git a/core/station/api/src/external_canister.rs b/core/station/api/src/external_canister.rs index 86b3702b..099ffe00 100644 --- a/core/station/api/src/external_canister.rs +++ b/core/station/api/src/external_canister.rs @@ -405,3 +405,24 @@ pub struct RestoreExternalCanisterOperationInput { pub struct RestoreExternalCanisterOperationDTO { pub input: RestoreExternalCanisterOperationInput, } + +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] +pub enum PruneExternalCanisterResourceDTO { + #[serde(rename = "snapshot")] + Snapshot(String), + #[serde(rename = "chunk_store")] + ChunkStore, + #[serde(rename = "state")] + State, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] +pub struct PruneExternalCanisterOperationInput { + pub canister_id: Principal, + pub prune: PruneExternalCanisterResourceDTO, +} + +#[derive(CandidType, serde::Serialize, Deserialize, Debug, Clone)] +pub struct PruneExternalCanisterOperationDTO { + pub input: PruneExternalCanisterOperationInput, +} diff --git a/core/station/api/src/request.rs b/core/station/api/src/request.rs index 59af3709..dafc80a6 100644 --- a/core/station/api/src/request.rs +++ b/core/station/api/src/request.rs @@ -14,6 +14,7 @@ use crate::{ EditUserGroupOperationInput, EditUserOperationDTO, EditUserOperationInput, FundExternalCanisterOperationDTO, FundExternalCanisterOperationInput, ManageSystemInfoOperationDTO, ManageSystemInfoOperationInput, PaginationInput, + PruneExternalCanisterOperationDTO, PruneExternalCanisterOperationInput, RemoveAddressBookEntryOperationDTO, RemoveAddressBookEntryOperationInput, RemoveUserGroupOperationDTO, RemoveUserGroupOperationInput, RequestEvaluationResultDTO, RequestPolicyRuleDTO, RequestSpecifierDTO, RestoreExternalCanisterOperationDTO, @@ -82,6 +83,7 @@ pub enum RequestOperationDTO { FundExternalCanister(Box), SnapshotExternalCanister(Box), RestoreExternalCanister(Box), + PruneExternalCanister(Box), EditPermission(Box), AddRequestPolicy(Box), EditRequestPolicy(Box), @@ -111,6 +113,7 @@ pub enum RequestOperationInput { FundExternalCanister(FundExternalCanisterOperationInput), SnapshotExternalCanister(SnapshotExternalCanisterOperationInput), RestoreExternalCanister(RestoreExternalCanisterOperationInput), + PruneExternalCanister(PruneExternalCanisterOperationInput), EditPermission(EditPermissionOperationInput), AddRequestPolicy(AddRequestPolicyOperationInput), EditRequestPolicy(EditRequestPolicyOperationInput), @@ -139,6 +142,7 @@ pub enum RequestOperationTypeDTO { FundExternalCanister, SnapshotExternalCanister, RestoreExternalCanister, + PruneExternalCanister, EditPermission, AddRequestPolicy, EditRequestPolicy, @@ -167,6 +171,7 @@ pub enum ListRequestsOperationTypeDTO { FundExternalCanister(Option), SnapshotExternalCanister(Option), RestoreExternalCanister(Option), + PruneExternalCanister(Option), EditPermission, AddRequestPolicy, EditRequestPolicy, diff --git a/core/station/impl/src/factories/requests/mod.rs b/core/station/impl/src/factories/requests/mod.rs index 9fec56c2..2176f7bc 100644 --- a/core/station/impl/src/factories/requests/mod.rs +++ b/core/station/impl/src/factories/requests/mod.rs @@ -33,6 +33,7 @@ mod edit_user; mod edit_user_group; mod fund_external_canister; mod manage_system_info; +mod prune_external_canister; mod remove_address_book_entry; mod remove_request_policy; mod remove_user_group; @@ -64,6 +65,8 @@ use self::{ edit_request_policy::{EditRequestPolicyRequestCreate, EditRequestPolicyRequestExecute}, edit_user::{EditUserRequestCreate, EditUserRequestExecute}, edit_user_group::{EditUserGroupRequestCreate, EditUserGroupRequestExecute}, + prune_external_canister::PruneExternalCanisterRequestCreate, + prune_external_canister::PruneExternalCanisterRequestExecute, remove_address_book_entry::{ RemoveAddressBookEntryRequestCreate, RemoveAddressBookEntryRequestExecute, }, @@ -235,6 +238,12 @@ impl RequestFactory { .create(id, requested_by_user, input.clone(), operation.clone()) .await } + RequestOperationInput::PruneExternalCanister(operation) => { + let creator = Box::new(PruneExternalCanisterRequestCreate {}); + creator + .create(id, requested_by_user, input.clone(), operation.clone()) + .await + } RequestOperationInput::EditPermission(operation) => { let creator = Box::new(EditPermissionRequestCreate {}); creator @@ -361,6 +370,12 @@ impl RequestFactory { Arc::clone(&CHANGE_CANISTER_SERVICE), )) } + RequestOperation::PruneExternalCanister(operation) => { + Box::new(PruneExternalCanisterRequestExecute::new( + operation, + Arc::clone(&CHANGE_CANISTER_SERVICE), + )) + } RequestOperation::EditPermission(operation) => { Box::new(EditPermissionRequestExecute::new( request, diff --git a/core/station/impl/src/factories/requests/prune_external_canister.rs b/core/station/impl/src/factories/requests/prune_external_canister.rs new file mode 100644 index 00000000..5acc43bf --- /dev/null +++ b/core/station/impl/src/factories/requests/prune_external_canister.rs @@ -0,0 +1,74 @@ +use super::{Create, Execute, RequestExecuteStage}; +use crate::{ + errors::{RequestError, RequestExecuteError}, + models::{PruneExternalCanisterOperation, Request, RequestOperation}, + services::ChangeCanisterService, +}; +use async_trait::async_trait; +use orbit_essentials::types::UUID; +use station_api::{CreateRequestInput, PruneExternalCanisterOperationInput}; +use std::sync::Arc; + +pub struct PruneExternalCanisterRequestCreate; + +#[async_trait] +impl Create for PruneExternalCanisterRequestCreate { + async fn create( + &self, + request_id: UUID, + requested_by_user: UUID, + input: CreateRequestInput, + operation_input: PruneExternalCanisterOperationInput, + ) -> Result { + let request = Request::from_request_creation_input( + request_id, + requested_by_user, + input, + RequestOperation::PruneExternalCanister(PruneExternalCanisterOperation { + input: operation_input.into(), + }), + "Prune canister".to_string(), + ); + + Ok(request) + } +} + +pub struct PruneExternalCanisterRequestExecute<'o> { + operation: &'o PruneExternalCanisterOperation, + change_canister_service: Arc, +} + +impl<'o> PruneExternalCanisterRequestExecute<'o> { + pub fn new( + operation: &'o PruneExternalCanisterOperation, + change_canister_service: Arc, + ) -> Self { + Self { + operation, + change_canister_service, + } + } +} + +#[async_trait] +impl Execute for PruneExternalCanisterRequestExecute<'_> { + async fn execute(&self) -> Result { + self.change_canister_service + .prune_canister( + self.operation.input.canister_id, + self.operation.input.prune.clone(), + ) + .await + .map_err(|err| RequestExecuteError::Failed { + reason: format!( + "failed to prune {} on external canister {}: {}", + self.operation.input.canister_id, self.operation.input.prune, err + ), + })?; + + Ok(RequestExecuteStage::Completed( + RequestOperation::PruneExternalCanister(self.operation.clone()), + )) + } +} diff --git a/core/station/impl/src/mappers/authorization.rs b/core/station/impl/src/mappers/authorization.rs index 685f508d..b4063c75 100644 --- a/core/station/impl/src/mappers/authorization.rs +++ b/core/station/impl/src/mappers/authorization.rs @@ -284,6 +284,11 @@ impl From<&station_api::CreateRequestInput> for Resource { ExternalCanisterId::Canister(input.canister_id), )) } + RequestOperationInput::PruneExternalCanister(input) => { + Resource::ExternalCanister(ExternalCanisterResourceAction::Change( + ExternalCanisterId::Canister(input.canister_id), + )) + } RequestOperationInput::EditPermission(_) => { Resource::Permission(PermissionResourceAction::Update) } diff --git a/core/station/impl/src/mappers/notification_type.rs b/core/station/impl/src/mappers/notification_type.rs index ed49bdec..bec21298 100644 --- a/core/station/impl/src/mappers/notification_type.rs +++ b/core/station/impl/src/mappers/notification_type.rs @@ -95,7 +95,8 @@ impl TryFrom for NotificationTypeDTO { | RequestOperation::FundExternalCanister(_) | RequestOperation::CallExternalCanister(_) | RequestOperation::SnapshotExternalCanister(_) - | RequestOperation::RestoreExternalCanister(_) => None, + | RequestOperation::RestoreExternalCanister(_) + | RequestOperation::PruneExternalCanister(_) => None, }; let user_id: Option<[u8; 16]> = match &request.operation { @@ -123,7 +124,8 @@ impl TryFrom for NotificationTypeDTO { | RequestOperation::FundExternalCanister(_) | RequestOperation::CallExternalCanister(_) | RequestOperation::SnapshotExternalCanister(_) - | RequestOperation::RestoreExternalCanister(_) => None, + | RequestOperation::RestoreExternalCanister(_) + | RequestOperation::PruneExternalCanister(_) => None, }; NotificationTypeDTO::RequestCreated(RequestCreatedNotificationDTO { diff --git a/core/station/impl/src/mappers/request_operation.rs b/core/station/impl/src/mappers/request_operation.rs index 7eade6ea..3c287911 100644 --- a/core/station/impl/src/mappers/request_operation.rs +++ b/core/station/impl/src/mappers/request_operation.rs @@ -34,9 +34,11 @@ use crate::{ ExternalCanisterChangeRequestPolicyRuleInput, ExternalCanisterPermissionsCreateInput, ExternalCanisterPermissionsUpdateInput, ExternalCanisterRequestPoliciesCreateInput, ExternalCanisterRequestPoliciesUpdateInput, FundExternalCanisterOperation, LogVisibility, - ManageSystemInfoOperation, ManageSystemInfoOperationInput, RemoveAddressBookEntryOperation, - RemoveRequestPolicyOperation, RemoveRequestPolicyOperationInput, RemoveUserGroupOperation, - RequestOperation, RestoreExternalCanisterOperation, RestoreExternalCanisterOperationInput, + ManageSystemInfoOperation, ManageSystemInfoOperationInput, PruneExternalCanisterOperation, + PruneExternalCanisterOperationInput, PruneExternalCanisterResource, + RemoveAddressBookEntryOperation, RemoveRequestPolicyOperation, + RemoveRequestPolicyOperationInput, RemoveUserGroupOperation, RequestOperation, + RestoreExternalCanisterOperation, RestoreExternalCanisterOperationInput, SetDisasterRecoveryOperation, SetDisasterRecoveryOperationInput, SnapshotExternalCanisterOperation, SnapshotExternalCanisterOperationInput, SystemUpgradeOperation, SystemUpgradeOperationInput, SystemUpgradeTarget, @@ -52,7 +54,8 @@ use station_api::{ AddAccountOperationDTO, AddAddressBookEntryOperationDTO, AddUserOperationDTO, CallExternalCanisterOperationDTO, CanisterMethodDTO, ChangeExternalCanisterOperationDTO, CreateExternalCanisterOperationDTO, EditAccountOperationDTO, EditAddressBookEntryOperationDTO, - EditUserOperationDTO, NetworkDTO, RemoveAddressBookEntryOperationDTO, RequestOperationDTO, + EditUserOperationDTO, NetworkDTO, PruneExternalCanisterOperationDTO, + PruneExternalCanisterResourceDTO, RemoveAddressBookEntryOperationDTO, RequestOperationDTO, RestoreExternalCanisterOperationDTO, SnapshotExternalCanisterOperationDTO, TransferOperationDTO, }; @@ -1369,6 +1372,75 @@ impl From for RestoreExternalCanisterOperation } } +impl From for PruneExternalCanisterResourceDTO { + fn from(input: PruneExternalCanisterResource) -> PruneExternalCanisterResourceDTO { + match input { + PruneExternalCanisterResource::Snapshot(snapshot_id) => { + PruneExternalCanisterResourceDTO::Snapshot(hex::encode(snapshot_id)) + } + PruneExternalCanisterResource::ChunkStore => { + PruneExternalCanisterResourceDTO::ChunkStore + } + PruneExternalCanisterResource::State => PruneExternalCanisterResourceDTO::State, + } + } +} + +impl From for PruneExternalCanisterResource { + fn from(input: PruneExternalCanisterResourceDTO) -> PruneExternalCanisterResource { + match input { + PruneExternalCanisterResourceDTO::Snapshot(snapshot_id) => { + PruneExternalCanisterResource::Snapshot(hex::decode(&snapshot_id).unwrap_or_else( + |err| { + ic_cdk::trap(&format!( + "Failed to convert snapshot id {} to hex: {}", + snapshot_id, err + )) + }, + )) + } + PruneExternalCanisterResourceDTO::ChunkStore => { + PruneExternalCanisterResource::ChunkStore + } + PruneExternalCanisterResourceDTO::State => PruneExternalCanisterResource::State, + } + } +} + +impl From + for station_api::PruneExternalCanisterOperationInput +{ + fn from( + input: PruneExternalCanisterOperationInput, + ) -> station_api::PruneExternalCanisterOperationInput { + station_api::PruneExternalCanisterOperationInput { + canister_id: input.canister_id, + prune: input.prune.into(), + } + } +} + +impl From + for PruneExternalCanisterOperationInput +{ + fn from( + input: station_api::PruneExternalCanisterOperationInput, + ) -> PruneExternalCanisterOperationInput { + PruneExternalCanisterOperationInput { + canister_id: input.canister_id, + prune: input.prune.into(), + } + } +} + +impl From for PruneExternalCanisterOperationDTO { + fn from(operation: PruneExternalCanisterOperation) -> PruneExternalCanisterOperationDTO { + PruneExternalCanisterOperationDTO { + input: operation.input.into(), + } + } +} + impl From for station_api::EditPermissionOperationInput { fn from(input: EditPermissionOperationInput) -> station_api::EditPermissionOperationInput { station_api::EditPermissionOperationInput { @@ -1695,6 +1767,9 @@ impl From for RequestOperationDTO { RequestOperation::RestoreExternalCanister(operation) => { RequestOperationDTO::RestoreExternalCanister(Box::new(operation.into())) } + RequestOperation::PruneExternalCanister(operation) => { + RequestOperationDTO::PruneExternalCanister(Box::new(operation.into())) + } RequestOperation::EditPermission(operation) => { RequestOperationDTO::EditPermission(Box::new(operation.into())) } @@ -1897,6 +1972,19 @@ impl RequestOperation { )), ] } + RequestOperation::PruneExternalCanister(PruneExternalCanisterOperation { + input, + .. + }) => { + vec![ + Resource::ExternalCanister(ExternalCanisterResourceAction::Change( + ExternalCanisterId::Any, + )), + Resource::ExternalCanister(ExternalCanisterResourceAction::Change( + ExternalCanisterId::Canister(input.canister_id), + )), + ] + } RequestOperation::EditRequestPolicy(EditRequestPolicyOperation { input }) => { vec![ Resource::RequestPolicy(ResourceAction::Update(ResourceId::Id( diff --git a/core/station/impl/src/mappers/request_operation_type.rs b/core/station/impl/src/mappers/request_operation_type.rs index c205ab0a..21b0d512 100644 --- a/core/station/impl/src/mappers/request_operation_type.rs +++ b/core/station/impl/src/mappers/request_operation_type.rs @@ -66,6 +66,9 @@ impl From for ListRequestsOperationTy station_api::ListRequestsOperationTypeDTO::RestoreExternalCanister(canister_id) => { ListRequestsOperationType::RestoreExternalCanister(canister_id) } + station_api::ListRequestsOperationTypeDTO::PruneExternalCanister(canister_id) => { + ListRequestsOperationType::PruneExternalCanister(canister_id) + } station_api::ListRequestsOperationTypeDTO::EditPermission => { ListRequestsOperationType::EditPermission } @@ -127,6 +130,9 @@ impl From for RequestOperationType { RequestOperationTypeDTO::RestoreExternalCanister => { RequestOperationType::RestoreExternalCanister } + RequestOperationTypeDTO::PruneExternalCanister => { + RequestOperationType::PruneExternalCanister + } RequestOperationTypeDTO::EditPermission => RequestOperationType::EditPermission, RequestOperationTypeDTO::AddRequestPolicy => RequestOperationType::AddRequestPolicy, RequestOperationTypeDTO::EditRequestPolicy => RequestOperationType::EditRequestPolicy, @@ -183,6 +189,9 @@ impl From for RequestOperationTypeDTO { RequestOperationType::RestoreExternalCanister => { RequestOperationTypeDTO::RestoreExternalCanister } + RequestOperationType::PruneExternalCanister => { + RequestOperationTypeDTO::PruneExternalCanister + } RequestOperationType::EditPermission => RequestOperationTypeDTO::EditPermission, RequestOperationType::AddRequestPolicy => RequestOperationTypeDTO::AddRequestPolicy, RequestOperationType::EditRequestPolicy => RequestOperationTypeDTO::EditRequestPolicy, @@ -234,6 +243,9 @@ impl From for RequestOperationType { RequestOperation::RestoreExternalCanister(_) => { RequestOperationType::RestoreExternalCanister } + RequestOperation::PruneExternalCanister(_) => { + RequestOperationType::PruneExternalCanister + } RequestOperation::EditPermission(_) => RequestOperationType::EditPermission, RequestOperation::AddRequestPolicy(_) => RequestOperationType::AddRequestPolicy, RequestOperation::EditRequestPolicy(_) => RequestOperationType::EditRequestPolicy, diff --git a/core/station/impl/src/migration.rs b/core/station/impl/src/migration.rs index 47933142..5bd466c5 100644 --- a/core/station/impl/src/migration.rs +++ b/core/station/impl/src/migration.rs @@ -423,7 +423,7 @@ impl<'de> Deserialize<'de> for RequestOperation { const REMOVED_VARIANTS: [&str; 1] = ["ChangeCanister"]; // IMPORTANT: The size of the array must be hardcoded, to make sure it can be checked at compile-time. - static EXPECTED_VARIANTS: [&str; 26] = { + static EXPECTED_VARIANTS: [&str; 27] = { let variants: [&str; CURRENT_VARIANTS.len() + REMOVED_VARIANTS.len()] = concat_str_arrays!(CURRENT_VARIANTS, REMOVED_VARIANTS); @@ -535,6 +535,10 @@ impl<'de> Deserialize<'de> for RequestOperation { let value = variant_access.newtype_variant()?; Ok(RequestOperation::RestoreExternalCanister(value)) } + "PruneExternalCanister" => { + let value = variant_access.newtype_variant()?; + Ok(RequestOperation::PruneExternalCanister(value)) + } "AddRequestPolicy" => { let value = variant_access.newtype_variant()?; Ok(RequestOperation::AddRequestPolicy(value)) diff --git a/core/station/impl/src/models/request.rs b/core/station/impl/src/models/request.rs index e6937d5a..5f36bf83 100644 --- a/core/station/impl/src/models/request.rs +++ b/core/station/impl/src/models/request.rs @@ -297,6 +297,7 @@ fn validate_request_operation_foreign_keys( } RequestOperation::SnapshotExternalCanister(_) => (), RequestOperation::RestoreExternalCanister(_) => (), + RequestOperation::PruneExternalCanister(_) => (), RequestOperation::AddRequestPolicy(op) => { op.input.specifier.validate()?; op.input.rule.validate()?; diff --git a/core/station/impl/src/models/request_operation.rs b/core/station/impl/src/models/request_operation.rs index f3adb924..506af126 100644 --- a/core/station/impl/src/models/request_operation.rs +++ b/core/station/impl/src/models/request_operation.rs @@ -41,6 +41,7 @@ pub enum RequestOperation { FundExternalCanister(FundExternalCanisterOperation), SnapshotExternalCanister(SnapshotExternalCanisterOperation), RestoreExternalCanister(RestoreExternalCanisterOperation), + PruneExternalCanister(PruneExternalCanisterOperation), AddRequestPolicy(AddRequestPolicyOperation), EditRequestPolicy(EditRequestPolicyOperation), RemoveRequestPolicy(RemoveRequestPolicyOperation), @@ -77,6 +78,9 @@ impl Display for RequestOperation { RequestOperation::RestoreExternalCanister(_) => { write!(f, "restore_external_canister") } + RequestOperation::PruneExternalCanister(_) => { + write!(f, "prune_external_canister") + } RequestOperation::AddRequestPolicy(_) => write!(f, "add_request_policy"), RequestOperation::EditRequestPolicy(_) => write!(f, "edit_request_policy"), RequestOperation::RemoveRequestPolicy(_) => write!(f, "remove_request_policy"), @@ -647,6 +651,39 @@ pub struct RestoreExternalCanisterOperation { pub input: RestoreExternalCanisterOperationInput, } +#[storable] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum PruneExternalCanisterResource { + Snapshot(Vec), + ChunkStore, + State, +} + +impl Display for PruneExternalCanisterResource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PruneExternalCanisterResource::Snapshot(snapshot_id) => { + write!(f, "snapshot({})", hex::encode(snapshot_id)) + } + PruneExternalCanisterResource::ChunkStore => write!(f, "chunk_store"), + PruneExternalCanisterResource::State => write!(f, "state"), + } + } +} + +#[storable] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PruneExternalCanisterOperationInput { + pub canister_id: Principal, + pub prune: PruneExternalCanisterResource, +} + +#[storable] +#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct PruneExternalCanisterOperation { + pub input: PruneExternalCanisterOperationInput, +} + #[storable] #[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct EditPermissionOperationInput { diff --git a/core/station/impl/src/models/request_operation_filter_type.rs b/core/station/impl/src/models/request_operation_filter_type.rs index 79cb623f..2c3af2e7 100644 --- a/core/station/impl/src/models/request_operation_filter_type.rs +++ b/core/station/impl/src/models/request_operation_filter_type.rs @@ -30,6 +30,7 @@ pub enum RequestOperationFilterType { FundExternalCanister(Principal), SnapshotExternalCanister(Principal), RestoreExternalCanister(Principal), + PruneExternalCanister(Principal), } impl From for RequestOperationFilterType { @@ -88,6 +89,9 @@ impl From for RequestOperationFilterType { RequestOperation::RestoreExternalCanister(operation) => { RequestOperationFilterType::RestoreExternalCanister(operation.input.canister_id) } + RequestOperation::PruneExternalCanister(operation) => { + RequestOperationFilterType::PruneExternalCanister(operation.input.canister_id) + } } } } diff --git a/core/station/impl/src/models/request_operation_type.rs b/core/station/impl/src/models/request_operation_type.rs index c492f705..f91baa9c 100644 --- a/core/station/impl/src/models/request_operation_type.rs +++ b/core/station/impl/src/models/request_operation_type.rs @@ -35,6 +35,7 @@ pub enum RequestOperationType { FundExternalCanister = 25, SnapshotExternalCanister = 26, RestoreExternalCanister = 27, + PruneExternalCanister = 28, } /// A helper enum to filter the requests based on the operation type and @@ -58,6 +59,7 @@ pub enum ListRequestsOperationType { FundExternalCanister(Option), SnapshotExternalCanister(Option), RestoreExternalCanister(Option), + PruneExternalCanister(Option), EditPermission, AddRequestPolicy, EditRequestPolicy, @@ -165,6 +167,15 @@ impl PartialEq for RequestOperationFilterType { RequestOperationFilterType::RestoreExternalCanister(id) if id == canister_id ) } + ListRequestsOperationType::PruneExternalCanister(None) => { + matches!(self, RequestOperationFilterType::PruneExternalCanister(_)) + } + ListRequestsOperationType::PruneExternalCanister(Some(canister_id)) => { + matches!( + self, + RequestOperationFilterType::PruneExternalCanister(id) if id == canister_id + ) + } ListRequestsOperationType::EditPermission => { matches!(self, RequestOperationFilterType::EditPermission) } @@ -250,6 +261,9 @@ impl Display for RequestOperationType { RequestOperationType::RestoreExternalCanister => { write!(f, "restore_external_canister") } + RequestOperationType::PruneExternalCanister => { + write!(f, "prune_external_canister") + } RequestOperationType::EditPermission => write!(f, "edit_permission"), RequestOperationType::AddRequestPolicy => write!(f, "add_request_policy"), RequestOperationType::EditRequestPolicy => write!(f, "edit_request_policy"), diff --git a/core/station/impl/src/services/change_canister.rs b/core/station/impl/src/services/change_canister.rs index 9fbb3aa5..a5cef6c0 100644 --- a/core/station/impl/src/services/change_canister.rs +++ b/core/station/impl/src/services/change_canister.rs @@ -1,11 +1,14 @@ use crate::{ errors::ChangeCanisterError, - models::{CanisterInstallMode, WasmModuleExtraChunks}, + models::{CanisterInstallMode, PruneExternalCanisterResource, WasmModuleExtraChunks}, }; use candid::Principal; use ic_cdk::api::management_canister::{ main as mgmt, - main::{CanisterIdRecord, LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs}, + main::{ + CanisterIdRecord, ClearChunkStoreArgument, DeleteCanisterSnapshotArgs, + LoadCanisterSnapshotArgs, TakeCanisterSnapshotArgs, + }, }; use lazy_static::lazy_static; use orbit_essentials::api::ServiceResult; @@ -60,6 +63,41 @@ impl ChangeCanisterService { Ok(()) } + pub async fn prune_canister( + &self, + canister_id: Principal, + prune: PruneExternalCanisterResource, + ) -> ServiceResult<(), ChangeCanisterError> { + match prune { + PruneExternalCanisterResource::Snapshot(snapshot_id) => { + mgmt::delete_canister_snapshot(DeleteCanisterSnapshotArgs { + canister_id: canister_id.to_owned(), + snapshot_id, + }) + .await + .map_err(|(_, err)| ChangeCanisterError::Failed { + reason: err.to_string(), + }) + } + PruneExternalCanisterResource::ChunkStore => { + mgmt::clear_chunk_store(ClearChunkStoreArgument { + canister_id: canister_id.to_owned(), + }) + .await + .map_err(|(_, err)| ChangeCanisterError::Failed { + reason: err.to_string(), + }) + } + PruneExternalCanisterResource::State => mgmt::uninstall_code(CanisterIdRecord { + canister_id: canister_id.to_owned(), + }) + .await + .map_err(|(_, err)| ChangeCanisterError::Failed { + reason: err.to_string(), + }), + } + } + /// Take a snapshot of a canister. pub async fn snapshot_canister( &self, diff --git a/tests/integration/src/external_canister_tests.rs b/tests/integration/src/external_canister_tests.rs index ade016d9..18239404 100644 --- a/tests/integration/src/external_canister_tests.rs +++ b/tests/integration/src/external_canister_tests.rs @@ -1,7 +1,7 @@ use crate::setup::{create_canister, setup_new_env, WALLET_ADMIN_USER}; use crate::utils::{ add_user, bump_time_to_avoid_ratelimit, canister_status, deploy_test_canister, execute_request, - get_core_canister_health_status, get_request, submit_request, submit_request_approval, + get_core_canister_health_status, get_request, hash, submit_request, submit_request_approval, submit_request_raw, submit_request_with_expected_trap, update_raw, upload_canister_chunks_to_asset_canister, user_test_id, wait_for_request, COUNTER_WAT, }; @@ -20,7 +20,8 @@ use station_api::{ CreateExternalCanisterOperationKindCreateNewDTO, CreateExternalCanisterOperationKindDTO, EditPermissionOperationInput, ExecutionMethodResourceTargetDTO, ExternalCanisterIdDTO, ExternalCanisterPermissionsCreateInput, ExternalCanisterRequestPoliciesCreateInput, - HealthStatus, ListRequestsInput, ListRequestsOperationTypeDTO, ListRequestsResponse, QuorumDTO, + HealthStatus, ListRequestsInput, ListRequestsOperationTypeDTO, ListRequestsResponse, + PruneExternalCanisterOperationInput, PruneExternalCanisterResourceDTO, QuorumDTO, RequestApprovalStatusDTO, RequestOperationDTO, RequestOperationInput, RequestPolicyRuleDTO, RequestSpecifierDTO, RequestStatusDTO, RestoreExternalCanisterOperationInput, SnapshotExternalCanisterOperationInput, UserSpecifierDTO, ValidationMethodResourceTargetDTO, @@ -1561,6 +1562,119 @@ fn snapshot_external_canister_test() { .map(|snapshot| snapshot.id) .collect(); assert_eq!(snapshots, vec![hex::decode(&new_snapshot_id).unwrap()]); + + // prune the new snapshot + let prune_canister_operation = + RequestOperationInput::PruneExternalCanister(PruneExternalCanisterOperationInput { + canister_id: external_canister_id, + prune: PruneExternalCanisterResourceDTO::Snapshot(hex::encode(new_snapshot_id)), + }); + execute_request( + &env, + WALLET_ADMIN_USER, + canister_ids.station, + prune_canister_operation, + ) + .unwrap(); + + // retrieve the existing snapshots from the management canister: there should be no snapshots anymore + let snapshots: Vec<_> = env + .list_canister_snapshots(external_canister_id, Some(canister_ids.station)) + .unwrap(); + assert!(snapshots.is_empty()); +} + +#[test] +fn prune_external_canister_chunk_store_test() { + let TestEnv { + env, canister_ids, .. + } = setup_new_env(); + + // create and install the test canister (external canister) + let external_canister_id = deploy_test_canister(&env, canister_ids.station); + + // check that the external canister is not empty + let status = canister_status(&env, Some(canister_ids.station), external_canister_id); + status.module_hash.unwrap(); + + // check that the external canister has an empty chunk store + let chunks = env + .stored_chunks(external_canister_id, Some(canister_ids.station)) + .unwrap(); + assert!(chunks.is_empty()); + + // upload a chunk + let chunk = vec![1, 2, 3, 4]; + let chunk_hash = env + .upload_chunk( + external_canister_id, + Some(canister_ids.station), + chunk.clone(), + ) + .unwrap(); + assert_eq!(chunk_hash, hash(&chunk)); + + // check that the chunk is indeed in the external canister's chunk store + let chunks = env + .stored_chunks(external_canister_id, Some(canister_ids.station)) + .unwrap(); + assert_eq!(chunks, vec![chunk_hash]); + + // prune the external canister's chunk store + let prune_canister_operation = + RequestOperationInput::PruneExternalCanister(PruneExternalCanisterOperationInput { + canister_id: external_canister_id, + prune: PruneExternalCanisterResourceDTO::ChunkStore, + }); + execute_request( + &env, + WALLET_ADMIN_USER, + canister_ids.station, + prune_canister_operation, + ) + .unwrap(); + + // check that the external canister is still not empty + let status = canister_status(&env, Some(canister_ids.station), external_canister_id); + status.module_hash.unwrap(); + + // check that the external canister has an empty chunk store again + let chunks = env + .stored_chunks(external_canister_id, Some(canister_ids.station)) + .unwrap(); + assert!(chunks.is_empty()); +} + +#[test] +fn prune_external_canister_state_test() { + let TestEnv { + env, canister_ids, .. + } = setup_new_env(); + + // create and install the test canister (external canister) + let external_canister_id = deploy_test_canister(&env, canister_ids.station); + + // check that the external canister is not empty + let status = canister_status(&env, Some(canister_ids.station), external_canister_id); + status.module_hash.unwrap(); + + // prune the external canister + let prune_canister_operation = + RequestOperationInput::PruneExternalCanister(PruneExternalCanisterOperationInput { + canister_id: external_canister_id, + prune: PruneExternalCanisterResourceDTO::State, + }); + execute_request( + &env, + WALLET_ADMIN_USER, + canister_ids.station, + prune_canister_operation, + ) + .unwrap(); + + // check that the external canister is empty now + let status = canister_status(&env, Some(canister_ids.station), external_canister_id); + assert_eq!(status.module_hash, None); } #[test] diff --git a/tests/integration/src/utils.rs b/tests/integration/src/utils.rs index 6f815694..2e17c199 100644 --- a/tests/integration/src/utils.rs +++ b/tests/integration/src/utils.rs @@ -775,7 +775,7 @@ struct StoreArg { pub sha256: Option>, } -fn hash(data: &Vec) -> Vec { +pub(crate) fn hash(data: &Vec) -> Vec { let mut hasher = Sha256::new(); hasher.update(data); hasher.finalize().to_vec() diff --git a/tools/dfx-orbit/src/review/display.rs b/tools/dfx-orbit/src/review/display.rs index 876e8b6c..6983779f 100644 --- a/tools/dfx-orbit/src/review/display.rs +++ b/tools/dfx-orbit/src/review/display.rs @@ -238,6 +238,7 @@ pub(super) fn display_request_operation(op: &RequestOperationDTO) -> &'static st RequestOperationDTO::FundExternalCanister(_) => "FundExternalCanister", RequestOperationDTO::SnapshotExternalCanister(_) => "SnapshotExternalCanister", RequestOperationDTO::RestoreExternalCanister(_) => "RestoreExternalCanister", + RequestOperationDTO::PruneExternalCanister(_) => "PruneExternalCanister", RequestOperationDTO::EditPermission(_) => "EditPermission", RequestOperationDTO::AddRequestPolicy(_) => "AddRequestPolicy", RequestOperationDTO::EditRequestPolicy(_) => "EditRequestPolicy",