Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ bigdecimal.workspace = true
bip32.workspace = true
byte-unit.workspace = true
camino-tempfile.workspace = true
candid_parser.workspace = true
candid.workspace = true
candid_parser.workspace = true
clap-markdown.workspace = true
clap.workspace = true
console.workspace = true
Expand All @@ -43,11 +43,13 @@ pkcs8.workspace = true
rand.workspace = true
reqwest.workspace = true
sec1.workspace = true
serde.workspace = true
serde_json.workspace = true
serde_yaml.workspace = true
serde.workspace = true
snafu.workspace = true
sysinfo.workspace = true
thiserror.workspace = true
time.workspace = true
tiny-bip39.workspace = true
tokio.workspace = true
tracing-subscriber.workspace = true
Expand All @@ -62,6 +64,7 @@ icp = { workspace = true }
nix = { version = "0.30.1", features = ["process", "signal"] }
pocket-ic.workspace = true
predicates = "3"
regex = "1"
rand.workspace = true
serde_yaml.workspace = true
serial_test = { version = "3.2.0", features = ["file_locks"] }
Expand Down
5 changes: 5 additions & 0 deletions crates/icp-cli/src/commands/canister/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub(crate) mod install;
pub(crate) mod list;
pub(crate) mod settings;
pub(crate) mod show;
pub(crate) mod snapshot;
pub(crate) mod start;
pub(crate) mod status;
pub(crate) mod stop;
Expand Down Expand Up @@ -41,6 +42,10 @@ pub(crate) enum Command {
/// Show a canister's details
Show(show::ShowArgs),

/// Manipulate canister snapshots
#[command(subcommand)]
Snapshot(snapshot::Command),

/// Start a canister on a network
Start(start::StartArgs),

Expand Down
63 changes: 63 additions & 0 deletions crates/icp-cli/src/commands/canister/snapshot/create.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use clap::Args;
use ic_management_canister_types::TakeCanisterSnapshotArgs;

use crate::commands::{
args,
canister::snapshot::{SnapshotId, ensure_canister_stopped},
};
use icp::context::Context;

#[derive(Debug, Args)]
pub struct CreateArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,

/// If a snapshot ID is specified, this snapshot will replace it and reuse the ID.
#[arg(long)]
replace: Option<SnapshotId>,
}

pub async fn exec(ctx: &Context, args: &CreateArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();

let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;
let cid = ctx
.get_canister_id(
&selections.canister,
&selections.network,
&selections.environment,
)
.await?;

// Management Interface
let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent);

// Ensure canister is stopped
let (status,) = mgmt.canister_status(&cid).await?;
ensure_canister_stopped(status.status, &args.cmd_args.canister.to_string())?;

// Create snapshot
let (snapshot,) = mgmt
.take_canister_snapshot(
&cid,
&TakeCanisterSnapshotArgs {
canister_id: cid,
replace_snapshot: args.replace.as_ref().map(|id| id.0.clone()),
},
)
.await?;

eprintln!(
"Created a new snapshot of canister '{}'. Snapshot ID: '{}'",
args.cmd_args.canister,
SnapshotId(snapshot.id)
);

Ok(())
}
53 changes: 53 additions & 0 deletions crates/icp-cli/src/commands/canister/snapshot/delete.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
use clap::Args;
use ic_management_canister_types::DeleteCanisterSnapshotArgs;

use crate::commands::{args, canister::snapshot::SnapshotId};
use icp::context::Context;

#[derive(Debug, Args)]
pub struct DeleteArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,

/// The ID of the snapshot to delete.
snapshot: SnapshotId,
}

pub async fn exec(ctx: &Context, args: &DeleteArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();

let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;
let cid = ctx
.get_canister_id(
&selections.canister,
&selections.network,
&selections.environment,
)
.await?;

// Management Interface
let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent);

// Delete snapshot
mgmt.delete_canister_snapshot(
&cid,
&DeleteCanisterSnapshotArgs {
canister_id: cid,
snapshot_id: args.snapshot.0.clone(),
},
)
.await?;

eprintln!(
"Deleted snapshot {} from canister '{}'",
args.snapshot, args.cmd_args.canister,
);

Ok(())
}
42 changes: 42 additions & 0 deletions crates/icp-cli/src/commands/canister/snapshot/download.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
use clap::Args;
use icp::{identity, prelude::PathBuf};

use crate::commands::{
args,
canister::snapshot::{SnapshotId, directory_parser},
};
use icp::context::Context;

#[derive(Debug, Args)]
pub struct DownloadArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,

/// The ID of the snapshot to download.
snapshot: SnapshotId,

/// The directory to download the snapshot to.
#[arg(long, value_parser = directory_parser)]
dir: PathBuf,

/// Whether to resume the download if the previous snapshot download failed.
#[arg(short, long, default_value = "false")]
resume: bool,

/// The number of concurrent downloads to perform.
#[arg(long, default_value = "3")]
concurrency: usize,
}

#[derive(Debug, thiserror::Error)]
pub enum CommandError {
#[error(transparent)]
Project(#[from] icp::LoadError),

#[error(transparent)]
Identity(#[from] identity::LoadError),
}

pub async fn exec(_ctx: &Context, _args: &DownloadArgs) -> Result<(), CommandError> {
unimplemented!("project mode is not implemented yet");
}
63 changes: 63 additions & 0 deletions crates/icp-cli/src/commands/canister/snapshot/list.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use clap::Args;
use ic_management_canister_types::Snapshot;
use indicatif::HumanBytes;
use time::{OffsetDateTime, macros::format_description};

use crate::commands::{args, canister::snapshot::SnapshotId};
use icp::context::Context;

#[derive(Debug, Args)]
pub struct ListArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,
}

pub async fn exec(ctx: &Context, args: &ListArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();

let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;
let cid = ctx
.get_canister_id(
&selections.canister,
&selections.network,
&selections.environment,
)
.await?;

// Management Interface
let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent);

let (snapshots,) = mgmt.list_canister_snapshots(&cid).await?;

if snapshots.is_empty() {
eprintln!(
"No snapshots found for canister '{}'",
args.cmd_args.canister
);
} else {
for snapshot in snapshots {
print_snapshot(&snapshot);
}
}
Ok(())
}

fn print_snapshot(snapshot: &Snapshot) {
let time_fmt = format_description!("[year]-[month]-[day] [hour]:[minute]:[second] UTC");

eprintln!(
"{}: {}, taken at {}",
SnapshotId(snapshot.id.clone()),
HumanBytes(snapshot.total_size),
OffsetDateTime::from_unix_timestamp_nanos(snapshot.taken_at_timestamp as i128)
.unwrap()
.format(time_fmt)
.unwrap()
);
}
61 changes: 61 additions & 0 deletions crates/icp-cli/src/commands/canister/snapshot/load.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
use clap::Args;
use ic_management_canister_types::LoadCanisterSnapshotArgs;

use crate::commands::{
args,
canister::snapshot::{SnapshotId, ensure_canister_stopped},
};
use icp::context::Context;

#[derive(Debug, Args)]
pub struct LoadArgs {
#[command(flatten)]
pub(crate) cmd_args: args::CanisterCommandArgs,

/// The ID of the snapshot to load.
snapshot: SnapshotId,
}

pub async fn exec(ctx: &Context, args: &LoadArgs) -> Result<(), anyhow::Error> {
let selections = args.cmd_args.selections();

let agent = ctx
.get_agent(
&selections.identity,
&selections.network,
&selections.environment,
)
.await?;
let cid = ctx
.get_canister_id(
&selections.canister,
&selections.network,
&selections.environment,
)
.await?;

// Management Interface
let mgmt = ic_utils::interfaces::ManagementCanister::create(&agent);

// Ensure canister is stopped
let (status,) = mgmt.canister_status(&cid).await?;
ensure_canister_stopped(status.status, &args.cmd_args.canister.to_string())?;

// Load snapshot
mgmt.load_canister_snapshot(
&cid,
&LoadCanisterSnapshotArgs {
canister_id: cid,
snapshot_id: args.snapshot.0.clone(),
sender_canister_version: None,
},
)
.await?;

eprintln!(
"Loaded snapshot {} into canister '{}'",
args.snapshot, args.cmd_args.canister,
);

Ok(())
}
Loading
Loading