-
Notifications
You must be signed in to change notification settings - Fork 11
feat: icp canister link
#655
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| use anyhow::bail; | ||
| use candid::Principal; | ||
| use clap::Args; | ||
| use icp::context::{Context, EnvironmentSelection}; | ||
| use tracing::info; | ||
|
|
||
| use crate::options::EnvironmentOpt; | ||
|
|
||
| /// Link an existing canister to the project by recording its ID in the canister ID store. | ||
| /// | ||
| /// This associates an already-deployed canister with a name declared in the target | ||
| /// environment, without creating a new canister. It is the inverse of the record that | ||
| /// `icp canister create` writes automatically. | ||
| #[derive(Debug, Args)] | ||
| pub(crate) struct LinkArgs { | ||
| /// Name of the project canister to associate the ID with. | ||
| /// Must be declared in the target environment. | ||
| pub(crate) name: String, | ||
|
|
||
| /// Principal of the existing canister to link. | ||
| pub(crate) principal: Principal, | ||
|
|
||
| #[command(flatten)] | ||
| pub(crate) environment: EnvironmentOpt, | ||
|
|
||
| /// Overwrite an ID already recorded for this canister. | ||
| #[arg(long)] | ||
| pub(crate) force: bool, | ||
| } | ||
|
|
||
| pub(crate) async fn exec(ctx: &Context, args: &LinkArgs) -> Result<(), anyhow::Error> { | ||
| let environment: EnvironmentSelection = args.environment.clone().into(); | ||
|
|
||
| // A principal must map to at most one canister within an environment; linking an | ||
| // ID already claimed by another canister would create an ambiguous mapping. | ||
| let existing = ctx.ids_by_environment(&environment).await?; | ||
| if let Some((owner, _)) = existing | ||
| .iter() | ||
| .find(|(name, id)| **id == args.principal && *name != &args.name) | ||
| { | ||
| bail!( | ||
| "canister ID {} is already linked to '{owner}' in environment '{}'", | ||
| args.principal, | ||
| environment.name() | ||
| ); | ||
| } | ||
|
|
||
| // Replacing an existing entry requires clearing it first; the id store refuses | ||
| // to register a name that is already mapped. | ||
| if args.force { | ||
| ctx.remove_canister_id_for_env(&args.name, &environment) | ||
| .await?; | ||
|
adamspofford-dfinity marked this conversation as resolved.
|
||
| } | ||
|
|
||
| ctx.set_canister_id_for_env(&args.name, args.principal, &environment) | ||
| .await?; | ||
|
adamspofford-dfinity marked this conversation as resolved.
|
||
|
|
||
| info!( | ||
| "Linked canister '{}' to ID {} in environment '{}'", | ||
| args.name, | ||
| args.principal, | ||
| environment.name() | ||
| ); | ||
|
|
||
| Ok(()) | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| use indoc::formatdoc; | ||
| use predicates::str::contains; | ||
|
|
||
| use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext}; | ||
| use icp::{fs::write_string, prelude::*}; | ||
|
|
||
| mod common; | ||
|
|
||
| /// A well-formed canister principal used as the link target. `link` never contacts a | ||
| /// network, so the canister does not need to actually exist. | ||
| const LINKED_ID: &str = "rrkah-fqaaa-aaaaa-aaaaq-cai"; | ||
|
|
||
| fn write_manifest(project_dir: &Path) { | ||
| let pm = formatdoc! {r#" | ||
| canisters: | ||
| - name: my-canister | ||
| build: | ||
| steps: | ||
| - type: script | ||
| command: echo hi | ||
| - name: other-canister | ||
| build: | ||
| steps: | ||
| - type: script | ||
| command: echo hi | ||
|
|
||
| {NETWORK_RANDOM_PORT} | ||
| {ENVIRONMENT_RANDOM_PORT} | ||
| "#}; | ||
|
|
||
| write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest"); | ||
| } | ||
|
|
||
| fn mapping_path(project_dir: &Path) -> PathBuf { | ||
| project_dir | ||
| .join(".icp") | ||
| .join("cache") | ||
| .join("mappings") | ||
| .join("random-environment.ids.json") | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn canister_link_records_id() { | ||
| let ctx = TestContext::new(); | ||
| let project_dir = ctx.create_project_dir("icp"); | ||
| write_manifest(&project_dir); | ||
|
|
||
| ctx.icp() | ||
| .current_dir(&project_dir) | ||
| .args([ | ||
| "canister", | ||
| "link", | ||
| "my-canister", | ||
| LINKED_ID, | ||
| "--environment", | ||
| "random-environment", | ||
| ]) | ||
| .assert() | ||
| .success(); | ||
|
|
||
| let path = mapping_path(&project_dir); | ||
| assert!(path.exists(), "ID mapping file should exist at {path}"); | ||
|
|
||
| let mapping = icp::fs::read_to_string(&path).expect("failed to read mapping file"); | ||
| assert!( | ||
| mapping.contains(LINKED_ID), | ||
| "mapping should contain the linked ID, got: {mapping}" | ||
| ); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn canister_link_unknown_name_fails() { | ||
| let ctx = TestContext::new(); | ||
| let project_dir = ctx.create_project_dir("icp"); | ||
| write_manifest(&project_dir); | ||
|
|
||
| ctx.icp() | ||
| .current_dir(&project_dir) | ||
| .args([ | ||
| "canister", | ||
| "link", | ||
| "not-a-canister", | ||
| LINKED_ID, | ||
| "--environment", | ||
| "random-environment", | ||
| ]) | ||
| .assert() | ||
| .failure(); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn canister_link_duplicate_principal_fails() { | ||
| let ctx = TestContext::new(); | ||
| let project_dir = ctx.create_project_dir("icp"); | ||
| write_manifest(&project_dir); | ||
|
|
||
| let link = |name: &str| { | ||
| let mut cmd = ctx.icp(); | ||
| cmd.current_dir(&project_dir).args([ | ||
| "canister", | ||
| "link", | ||
| name, | ||
| LINKED_ID, | ||
| "--environment", | ||
| "random-environment", | ||
| ]); | ||
| cmd | ||
| }; | ||
|
|
||
| link("my-canister").assert().success(); | ||
|
|
||
| // The same principal cannot be linked to a second canister. | ||
| link("other-canister") | ||
| .assert() | ||
| .failure() | ||
| .stderr(contains("already linked to 'my-canister'")); | ||
| } | ||
|
|
||
| #[tokio::test] | ||
| async fn canister_link_existing_requires_force() { | ||
| let ctx = TestContext::new(); | ||
| let project_dir = ctx.create_project_dir("icp"); | ||
| write_manifest(&project_dir); | ||
|
|
||
| let link = |id: &str, force: bool| { | ||
| let mut cmd = ctx.icp(); | ||
| cmd.current_dir(&project_dir).args([ | ||
| "canister", | ||
| "link", | ||
| "my-canister", | ||
| id, | ||
| "--environment", | ||
| "random-environment", | ||
| ]); | ||
| if force { | ||
| cmd.arg("--force"); | ||
| } | ||
| cmd | ||
| }; | ||
|
|
||
| link(LINKED_ID, false).assert().success(); | ||
|
|
||
| // A second link to a different ID must fail without --force. | ||
| let other_id = "ryjl3-tyaaa-aaaaa-aaaba-cai"; | ||
| link(other_id, false) | ||
| .assert() | ||
| .failure() | ||
| .stderr(contains("already registered")); | ||
|
|
||
| // With --force it overwrites the recorded ID. | ||
| link(other_id, true).assert().success(); | ||
|
|
||
| let mapping = icp::fs::read_to_string(&mapping_path(&project_dir)).expect("read mapping"); | ||
| assert!( | ||
| mapping.contains(other_id) && !mapping.contains(LINKED_ID), | ||
| "mapping should hold the forced ID only, got: {mapping}" | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.