Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
37 changes: 37 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ ic-management-canister-types = { version = "0.7.1" }
ic-utils = { version = "0.47.0" }
icp = { path = "crates/icp" }
icp-canister-interfaces = { path = "crates/icp-canister-interfaces" }
icp-deploy-canister = { path = "crates/icp-deploy-canister" }
icp-sync-plugin = { path = "crates/icp-sync-plugin" }
ic-identity-hsm = "0.47.0"
icrc-ledger-types = "0.1.10"
Expand Down
1 change: 1 addition & 0 deletions crates/icp-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ ic-management-canister-types.workspace = true
ic-utils.workspace = true
icp-canister-interfaces.workspace = true
icp = { workspace = true, features = ["clap"] }
icp-deploy-canister.workspace = true
icrc-ledger-types.workspace = true
indicatif.workspace = true
itertools.workspace = true
Expand Down
64 changes: 48 additions & 16 deletions crates/icp-cli/src/commands/canister/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ use icp::fs;
use icp::prelude::*;
use tracing::{info, warn};

use icp::host_files::HostFileAccess;
use icp_deploy_canister::install_canister_resolved;

use crate::{
commands::args::{self, ArgsOpt},
operations::{
access::{AgentIcpAccess, ArtifactFileAccess},
candid_compat::{CandidCompatibility, check_candid_compatibility},
install::{
WasmMemoryPersistenceOpt, install_canister, is_eop_canister,
resolve_install_mode_and_status,
},
install::{WasmMemoryPersistenceOpt, is_eop_canister, resolve_install_mode_and_status},
},
};

Expand Down Expand Up @@ -184,18 +185,49 @@ pub(crate) async fn exec(ctx: &Context, args: &InstallArgs) -> Result<(), anyhow
}
}

install_canister(
&agent,
args.proxy,
&canister_id,
&canister_display,
&wasm,
install_mode,
status,
init_args_bytes.as_deref(),
args.wasm_memory_persistence,
)
.await?;
let icp = AgentIcpAccess::new(agent.clone(), args.proxy);
let wmp = args
.wasm_memory_persistence
.map(WasmMemoryPersistenceOpt::to_ic);
match &args.wasm {
// Explicit wasm file: read it through the host filesystem.
Some(wasm_path) => {
install_canister_resolved(
&canister_display,
canister_id,
wasm_path,
install_mode,
status,
init_args_bytes.as_deref(),
wmp,
&HostFileAccess,
&icp,
)
.await?;
}
// Build output: read it through the artifact store, keyed by canister name.
None => {
let name = match &selections.canister {
CanisterSelection::Named(name) => name,
CanisterSelection::Principal(_) => {
unreachable!("installing from the build output requires a named canister")
}
};
let files = ArtifactFileAccess(ctx.artifacts.clone());
install_canister_resolved(
&canister_display,
canister_id,
Path::new(name),
install_mode,
status,
init_args_bytes.as_deref(),
wmp,
&files,
&icp,
)
.await?;
}
}

info!("Canister {canister_display} installed successfully");

Expand Down
21 changes: 20 additions & 1 deletion crates/icp-cli/src/commands/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use icp::identity::IdentitySelection;
use std::collections::BTreeMap;
use tracing::info;

use icp::Canister;

use crate::{
operations::{proxy_management, sync::sync_many},
operations::{binding_env_vars::set_binding_env_vars_many, proxy_management, sync::sync_many},
options::{EnvironmentOpt, IdentityOpt},
};

Expand Down Expand Up @@ -122,6 +124,23 @@ pub(crate) async fn exec(ctx: &Context, args: &SyncArgs) -> Result<(), anyhow::E
.into_iter()
.collect();

// Apply the generated `PUBLIC_CANISTER_ID:*` environment variables before
// syncing. `deploy` does this, but standalone `icp sync` previously did not,
// so a synced canister could run against stale/absent binding ids.
let target_canisters: Vec<(Principal, Canister)> = sync_canisters
.iter()
.map(|(cid, _, info)| (*cid, info.clone()))
.collect();
set_binding_env_vars_many(
agent.clone(),
args.proxy,
environment_selection.name(),
target_canisters,
canister_ids.clone(),
ctx.debug,
)
.await?;

let pkg_cache = ctx.dirs.package_cache()?;
sync_many(
ctx.syncer.clone(),
Expand Down
131 changes: 131 additions & 0 deletions crates/icp-cli/src/operations/access.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
//! Host implementations of the `icp-deploy-canister` IO traits, backing the
//! library's install/sync/deploy core with the CLI's `ic-agent` transport and
//! on-disk stores.

use std::sync::Arc;

use async_trait::async_trait;
use candid::Principal;
use icp::prelude::*;
use icp::store_artifact;
use icp_deploy_canister::files::{FileAccess, FileAccessError};
use icp_deploy_canister::icp_access::{IcpAccess, IcpAccessError};

use super::proxy::update_or_proxy_raw;

/// [`IcpAccess`] over an `ic-agent` `Agent`. Proxy routing is baked in (the
/// impl is constructed with the proxy principal); the library never threads a
/// proxy per call. The caller's principal is captured once at construction.
pub struct AgentIcpAccess {
agent: ic_agent::Agent,
proxy: Option<Principal>,
caller: Principal,
}

impl AgentIcpAccess {
pub fn new(agent: ic_agent::Agent, proxy: Option<Principal>) -> Self {
let caller = agent
.get_principal()
.unwrap_or_else(|_| Principal::anonymous());
Self {
agent,
proxy,
caller,
}
}
}

#[async_trait]
impl IcpAccess for AgentIcpAccess {
async fn canister_update(
&self,
canister: Principal,
method: &str,
arg: Vec<u8>,
effective_canister_id: Principal,
cycles: u128,
) -> Result<Vec<u8>, IcpAccessError> {
update_or_proxy_raw(
&self.agent,
canister,
method,
arg,
self.proxy,
Some(effective_canister_id),
cycles,
)
.await
.map_err(|e| IcpAccessError::Update {
canister,
method: method.to_owned(),
message: e.to_string(),
})
}

async fn read_canister_metadata(
&self,
canister: Principal,
path: &str,
) -> Result<Option<Vec<u8>>, IcpAccessError> {
// A read failure is treated as "metadata absent" (matching the previous
// EOP-detection behavior), so a missing custom section never aborts an
// install.
Ok(self
.agent
.read_state_canister_metadata(canister, path)
.await
.ok())
}

fn caller_principal(&self) -> Principal {
self.caller
}
}

/// [`FileAccess`] backed by the canister build-artifact store. The library reads
/// a canister's built wasm via `read_file(artifact_path)`; here the "path" is the
/// canister's store key, resolved through the (locked) artifact store. Only
/// `read_file` is used by the install path; the other methods have benign
/// defaults.
pub struct ArtifactFileAccess(pub Arc<dyn store_artifact::Access>);

#[async_trait]
impl FileAccess for ArtifactFileAccess {
async fn read_file(&self, path: &Path) -> Result<Vec<u8>, FileAccessError> {
self.0
.lookup(path.as_str())
.await
.map_err(|e| FileAccessError::Read {
path: path.to_owned(),
message: e.to_string(),
})
}

async fn read_to_string(&self, path: &Path) -> Result<String, FileAccessError> {
let bytes = self.read_file(path).await?;
String::from_utf8(bytes).map_err(|e| FileAccessError::Read {
path: path.to_owned(),
message: e.to_string(),
})
}

async fn exists(&self, path: &Path) -> bool {
self.0.lookup(path.as_str()).await.is_ok()
}

async fn is_file(&self, path: &Path) -> bool {
self.exists(path).await
}

async fn is_dir(&self, _path: &Path) -> bool {
false
}

async fn read_dir(&self, _path: &Path) -> Result<Vec<PathBuf>, FileAccessError> {
Ok(Vec::new())
}

async fn canonicalize(&self, path: &Path) -> Option<PathBuf> {
Some(path.to_owned())
}
}
Loading
Loading