Skip to content
Open
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
9 changes: 5 additions & 4 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ RUN rust_version=$(grep -oP '(?<=channel = ")[^"]+' /usr/src/rust-toolchain.toml
curl --fail https://sh.rustup.rs -sSf \
| sh -s -- -y --default-toolchain ${rust_version}-x86_64-unknown-linux-gnu --no-modify-path && \
rustup default ${rust_version}-x86_64-unknown-linux-gnu && \
rustup component add rls && \
rustup component add rust-analyzer && \
Comment thread
r-birkner marked this conversation as resolved.
chown -R ubuntu.ubuntu ${RUSTUP_HOME} ${CARGO_HOME}

# Cargo maintains a local cache of the registry index and of git checkouts of crates at CARGO_HOME
Expand All @@ -75,9 +75,10 @@ ENV CARGO_HOME=/cargo
# Used to detect the Dockerfile changes and automatically rebuild the image
COPY docker /docker

# Download ic-admin
ARG ic_git_revision=89cc1c20223532c900b94de5bc6bd8cbde278797
RUN curl --fail https://download.dfinity.systems/ic/${ic_git_revision}/release/ic-admin.gz -o - | gunzip -c >| /usr/bin/ic-admin && \
# Download ic-admin from GitHub releases (download.dfinity.systems no longer serves it).
# The image is x86_64 linux, so we fetch the matching asset from a pinned IC release tag.
ARG ic_release_tag=release-2026-06-04_04-52-base
RUN curl --fail -L https://github.com/dfinity/ic/releases/download/${ic_release_tag}/ic-admin-x86_64-linux.gz -o - | gunzip -c >| /usr/bin/ic-admin && \
chmod +x /usr/bin/ic-admin

USER ubuntu
Expand Down
24 changes: 22 additions & 2 deletions rs/cli/src/ctx/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ic_management_backend::{
proposal::{ProposalAgent, ProposalAgentImpl},
};
use ic_management_types::Network;
use log::warn;
use log::{info, warn};

use crate::{
artifact_downloader::{ArtifactDownloader, ArtifactDownloaderImpl},
Expand Down Expand Up @@ -45,6 +45,9 @@ pub struct DreContext {
artifact_downloader: Arc<dyn ArtifactDownloader>,
neuron: RefCell<Option<Neuron>>,
version: IcAdminVersion,
/// Explicit ic-admin binary path/command (from `--ic-admin`). When set, it
/// takes precedence over `version` and no download is performed.
ic_admin_path_override: Option<String>,
neuron_opts: NeuronOpts,
cordoned_features_fetcher: Arc<dyn CordonedFeatureFetcher>,
health_client: Arc<dyn HealthStatusQuerier>,
Expand All @@ -60,6 +63,7 @@ impl DreContext {
verbose: bool,
auth_requirement: AuthRequirement,
ic_admin_version: IcAdminVersion,
ic_admin_path_override: Option<String>,
cordoned_features_fetcher: Arc<dyn CordonedFeatureFetcher>,
health_client: Arc<dyn HealthStatusQuerier>,
store: Store,
Expand All @@ -76,6 +80,7 @@ impl DreContext {
artifact_downloader: Arc::new(ArtifactDownloaderImpl {}) as Arc<dyn ArtifactDownloader>,
neuron: RefCell::new(None),
version: ic_admin_version,
ic_admin_path_override,
neuron_opts: NeuronOpts {
auth_opts: auth,
requirement: auth_requirement,
Expand Down Expand Up @@ -114,6 +119,7 @@ impl DreContext {
args.verbose,
require_auth,
args.ic_admin_version.clone(),
args.ic_admin.clone(),
store.cordoned_features_fetcher(args.cordoned_features_file.clone())?,
store.health_client(&network)?,
store,
Expand Down Expand Up @@ -155,7 +161,20 @@ impl DreContext {
return Ok(a.clone());
}

let ic_admin = self.store.ic_admin(&self.version, self.network(), self.neuron().await?).await?;
let ic_admin: Arc<dyn IcAdmin> = match &self.ic_admin_path_override {
// An explicit `--ic-admin` binary was provided: use it directly and skip
// any download/version resolution.
Some(path) => {
// If a filesystem path was given (rather than a bare command resolved
// via $PATH), fail early with a clear message when it's missing.
if (path.contains(std::path::MAIN_SEPARATOR) || path.contains('/')) && !std::path::Path::new(path).exists() {
return Err(anyhow::anyhow!("ic-admin binary specified via --ic-admin not found at `{}`", path));
}
info!("Using explicit ic-admin: {}", path);
Arc::new(IcAdminImpl::new(self.network().clone(), Some(path.clone()), self.neuron().await?))
}
None => self.store.ic_admin(&self.version, self.network(), self.neuron().await?).await?,
};
*self.ic_admin.borrow_mut() = Some(ic_admin.clone());
Ok(ic_admin)
}
Expand Down Expand Up @@ -314,6 +333,7 @@ pub mod tests {
artifact_downloader,
neuron: RefCell::new(Some(neuron.clone())),
version: IcAdminVersion::Strict("Shouldn't reach this because of mock".to_string()),
ic_admin_path_override: None,
neuron_opts: super::NeuronOpts {
auth_opts: AuthOpts {
private_key_pem: None,
Expand Down
41 changes: 28 additions & 13 deletions rs/cli/src/ctx/unit_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ async fn get_context(network: &Network, version: IcAdminVersion) -> anyhow::Resu
false,
AuthRequirement::Anonymous,
version,
None,
Arc::new(MockCordonedFeatureFetcher::new()),
Arc::new(MockHealthStatusQuerier::new()),
Store::new(false)?,
Expand All @@ -55,7 +56,9 @@ struct AdminVersionTestScenario<'a> {
name: &'static str,
version: IcAdminVersion,
should_delete_status_file: bool,
should_contain: Option<&'a str>,
// Acceptable substrings for the resolved ic-admin path. Empty means the
// resolution is expected to fail. Multiple values means "any of".
should_contain: Vec<&'a str>,
}

impl<'a> AdminVersionTestScenario<'a> {
Expand All @@ -64,7 +67,7 @@ impl<'a> AdminVersionTestScenario<'a> {
name,
version: IcAdminVersion::FromRegistry,
should_delete_status_file: false,
should_contain: None,
should_contain: vec![],
}
}

Expand All @@ -81,7 +84,14 @@ impl<'a> AdminVersionTestScenario<'a> {

fn should_contain(self, ver: &'a str) -> Self {
Self {
should_contain: Some(ver),
should_contain: vec![ver],
..self
}
}

fn should_contain_any(self, vers: Vec<&'a str>) -> Self {
Self {
should_contain: vers,
..self
}
}
Expand All @@ -90,21 +100,25 @@ impl<'a> AdminVersionTestScenario<'a> {
#[test]
fn init_tests_ic_admin_version() {
let runtime = tokio::runtime::Runtime::new().unwrap();
let version_on_s3 = "e47293c0bd7f39540245913f7f75be3d6863183c";
// A real commit that has a published IC release (release-2026-06-04_04-52-canister-logging).
let released_version = "fb721da900b9e9219773ee312f987971338f7c62";
let mainnet = Network::mainnet_unchecked().unwrap();
let registry_version = runtime.block_on(registry_canister_version(mainnet.get_nns_urls()[0].clone())).unwrap();

let tests = &[
// The registry/NNS canister commit is usually not a tagged release, so dre
// either matches it directly (if it happens to be released) or falls back to
// the embedded version. Both are valid outcomes.
AdminVersionTestScenario::new("match registry canister")
.delete_status_file()
.should_contain(&registry_version.stringified_hash),
.should_contain_any(vec![&registry_version.stringified_hash, FALLBACK_IC_ADMIN_VERSION]),
AdminVersionTestScenario::new("use default version")
.version(IcAdminVersion::Fallback)
.should_contain(FALLBACK_IC_ADMIN_VERSION),
AdminVersionTestScenario::new("existing version on s3")
.version(IcAdminVersion::Strict(version_on_s3.to_string()))
.should_contain(version_on_s3),
AdminVersionTestScenario::new("random version not present on s3").version(IcAdminVersion::Strict("random-version".to_string())),
AdminVersionTestScenario::new("strict released version")
.version(IcAdminVersion::Strict(released_version.to_string()))
.should_contain(released_version),
AdminVersionTestScenario::new("strict unreleased version errors").version(IcAdminVersion::Strict("random-version".to_string())),
];

for test in tests {
Expand All @@ -115,7 +129,7 @@ fn init_tests_ic_admin_version() {

let maybe_ctx = runtime.block_on(get_context(&mainnet, test.version.clone()));

if let Some(ver) = test.should_contain {
if !test.should_contain.is_empty() {
assert!(
maybe_ctx.is_ok(),
"Test `{}`: expected to create DreContext, but got error: {:?}",
Expand All @@ -128,11 +142,11 @@ fn init_tests_ic_admin_version() {
assert!(ic_admin_path.is_ok(), "Expected Ok, but was: {:?}", ic_admin_path);
let ic_admin_path = ic_admin_path.unwrap().ic_admin_path().unwrap_or_default();
assert!(
ic_admin_path.contains(ver),
"Test `{}`: ic_admin_path `{}`, expected version `{}`",
test.should_contain.iter().any(|ver| ic_admin_path.contains(ver)),
"Test `{}`: ic_admin_path `{}`, expected to contain one of `{:?}`",
test.name,
ic_admin_path,
ver
test.should_contain
)
} else {
assert!(
Expand Down Expand Up @@ -176,6 +190,7 @@ async fn get_ctx_for_neuron_test(
true,
requirement,
IcAdminVersion::Strict("Shouldn't get to here".to_string()),
None,
Arc::new(MockCordonedFeatureFetcher::new()),
Arc::new(MockHealthStatusQuerier::new()),
Store::new(offline)?,
Expand Down
47 changes: 39 additions & 8 deletions rs/cli/src/exe/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,16 @@ pub struct GlobalArgs {
#[clap(long, global = true, env = "NEURON_ID", visible_aliases = &["neuron", "proposer"])]
pub neuron_id: Option<u64>,

/// Path to explicitly state ic-admin path to use
/// Path to an explicit ic-admin binary to use. When set, dre will not
/// download ic-admin and will use this binary instead (overrides
/// --ic-admin-version). A bare command name is resolved via $PATH.
#[clap(long, global = true, env = "IC_ADMIN")]
pub ic_admin: Option<String>,

#[clap(long, global = true, env = "IC_ADMIN_VERSION", default_value = "from-registry", value_parser = clap::value_parser!(IcAdminVersion), help = r#"Specify the version of ic admin to use
Options:
1. from-governance, governance, govn, g => same as governance canister
2. default, d => strict default version, embedded at build time
3. <commit> => specific commit"#)]
#[clap(long, global = true, env = "IC_ADMIN_VERSION", default_value = "from-registry", value_parser = clap::value_parser!(IcAdminVersion), help = r#"Specify the version of ic-admin to use:
1. from-registry, registry, reg, r, from-governance, governance, govn, g => version matching the NNS registry canister (default)
2. fallback, default, f, d => fallback version embedded at build time
3. <commit> => specific commit/release (scans all IC releases)"#)]
pub ic_admin_version: IcAdminVersion,

#[clap(
Expand Down Expand Up @@ -75,9 +76,39 @@ pub enum IcAdminVersion {
impl From<&str> for IcAdminVersion {
fn from(value: &str) -> Self {
match value {
"from-registry" | "registry" | "r" | "reg" => Self::FromRegistry,
"fallback" | "f" => Self::Fallback,
// The registry canister version is the version the NNS/governance has
// deployed, hence the `governance` aliases are accepted here too.
"from-registry" | "registry" | "reg" | "r" | "from-governance" | "governance" | "govn" | "g" => Self::FromRegistry,
"fallback" | "f" | "default" | "d" => Self::Fallback,
s => Self::Strict(s.to_string()),
}
}
}

#[cfg(test)]
mod tests {
use super::IcAdminVersion;

#[test]
fn ic_admin_version_aliases() {
for alias in ["from-registry", "registry", "reg", "r", "from-governance", "governance", "govn", "g"] {
assert!(
matches!(IcAdminVersion::from(alias), IcAdminVersion::FromRegistry),
"`{}` should parse as FromRegistry",
alias
);
}
for alias in ["fallback", "f", "default", "d"] {
assert!(
matches!(IcAdminVersion::from(alias), IcAdminVersion::Fallback),
"`{}` should parse as Fallback",
alias
);
}
// Anything else is treated as a specific commit/release.
assert!(matches!(
IcAdminVersion::from("fb721da900b9e9219773ee312f987971338f7c62"),
IcAdminVersion::Strict(c) if c == "fb721da900b9e9219773ee312f987971338f7c62"
));
}
}
Loading
Loading