-
Notifications
You must be signed in to change notification settings - Fork 86
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
feat: dfx canister url #3346
base: master
Are you sure you want to change the base?
feat: dfx canister url #3346
Changes from all commits
2dfb6b9
21e36ac
8024c14
1e204de
d48ead5
76fea27
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
#!/usr/bin/env bats | ||
|
||
load ../utils/_ | ||
|
||
setup() { | ||
standard_setup | ||
|
||
dfx_new hello | ||
} | ||
|
||
teardown() { | ||
dfx_stop | ||
|
||
standard_teardown | ||
} | ||
|
||
@test "canister url performs as expected on local deploy" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we have a test that demonstrates the expected behavior with a non-remote, non-pull canister on --network ic? The dfx deploy tests couldn't do this because they can't deploy to mainnet. The test would have to populate canister_ids.json. |
||
dfx_new_frontend hello | ||
dfx_start | ||
dfx deploy | ||
assert_command dfx canister url hello_backend | ||
assert_eq "http://127.0.0.1:4943/?canisterId=be2us-64aaa-aaaaa-qaabq-cai&id=bkyz2-fmaaa-aaaaa-qaaaq-cai" | ||
assert_command dfx canister url hello_frontend | ||
assert_eq "http://127.0.0.1:4943/?canisterId=bd3sg-teaaa-aaaaa-qaaba-cai" | ||
} | ||
|
||
@test "canister url performs as expected on remote canisters" { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. pullable canisters are different from remote canisters. Could we have a test for remote canisters too? They should show different urls depending on network local/ic, but don't need to deploy to mainnet in order to display a mainnet url. |
||
# set dfx.json to string | ||
echo '{"canisters": {"whoami": {"type": "pull", "id": "ivcos-eqaaa-aaaab-qablq-cai"}}}' > dfx.json | ||
assert_command dfx canister url whoami --network ic | ||
assert_eq "https://a4gq6-oaaaa-aaaab-qaa4q-cai.raw.icp0.io/?id=ivcos-eqaaa-aaaab-qablq-cai" | ||
} | ||
|
||
@test "missing ui canister error" { | ||
dfx_start | ||
dfx canister create hello_backend | ||
assert_command_fail dfx canister url hello_backend | ||
assert_contains "Network local does not have a ui canister id" | ||
} | ||
|
||
@test "missing local id error" { | ||
assert_command_fail dfx canister url hello_backend | ||
assert_contains "Cannot find canister id. Please issue 'dfx canister create hello_backend'" | ||
} | ||
|
||
@test "missing ic id error" { | ||
assert_command_fail dfx canister url hello_backend --network ic | ||
assert_contains "Cannot find canister id. Please issue 'dfx canister create hello_backend --network ic'." | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,4 @@ | ||
pub mod url; | ||
use crate::{ | ||
cli::ask_for_consent, | ||
error::canister::{CanisterBuilderError, CanisterInstallError}, | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,105 @@ | ||||||||||||||||||||||||||||||
use url::Host::Domain; | ||||||||||||||||||||||||||||||
use url::ParseError; | ||||||||||||||||||||||||||||||
use url::Url; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
const MAINNET_CANDID_INTERFACE_PRINCIPAL: &str = "a4gq6-oaaaa-aaaab-qaa4q-cai"; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
pub fn format_frontend_url(provider: &Url, canister_id: &str) -> Url { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please retain types in the method signature and convert to &str in the method
Suggested change
|
||||||||||||||||||||||||||||||
let mut url = Url::clone(&provider); | ||||||||||||||||||||||||||||||
if let Some(Domain(domain)) = url.host() { | ||||||||||||||||||||||||||||||
if domain.ends_with("icp-api.io") || domain.ends_with("ic0.app") { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looking for substrings in domain names is always iffy, but I suspect this should be the following:
Suggested change
|
||||||||||||||||||||||||||||||
let new_domain = domain.replace("icp-api.io", "icp0.io"); | ||||||||||||||||||||||||||||||
let new_domain = new_domain.replace("ic0.app", "icp0.io"); | ||||||||||||||||||||||||||||||
Comment on lines
+11
to
+12
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hostname manipulation is error-prone. What if the domain is |
||||||||||||||||||||||||||||||
let host = format!("{}.{}", canister_id, new_domain); | ||||||||||||||||||||||||||||||
let _ = url.set_host(Some(&host)); | ||||||||||||||||||||||||||||||
} else if domain.contains("localhost") { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if it's not-a-localhost.my-domain.yay |
||||||||||||||||||||||||||||||
let port = url.port().unwrap_or(4943); | ||||||||||||||||||||||||||||||
let host = format!("localhost:{}", port); | ||||||||||||||||||||||||||||||
Comment on lines
+16
to
+17
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it necessary to set the port here? set_host() leaves the port unchanged.
Suggested change
|
||||||||||||||||||||||||||||||
let query = format!("canisterId={}", canister_id); | ||||||||||||||||||||||||||||||
url.set_host(Some(&host)).unwrap(); | ||||||||||||||||||||||||||||||
url.set_query(Some(&query)); | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
let host = format!("{}.{}", canister_id, domain); | ||||||||||||||||||||||||||||||
let _ = url.set_host(Some(&host)); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
let query = format!("canisterId={}", canister_id); | ||||||||||||||||||||||||||||||
url.set_query(Some(&query)); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
url | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
pub fn format_ui_canister_url_ic(canister_id: &str) -> Result<Url, ParseError> { | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
let url_result = Url::parse( | ||||||||||||||||||||||||||||||
format!( | ||||||||||||||||||||||||||||||
"https://{}.raw.icp0.io/?id={}", | ||||||||||||||||||||||||||||||
MAINNET_CANDID_INTERFACE_PRINCIPAL, canister_id | ||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||
.as_str(), | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
return url_result; | ||||||||||||||||||||||||||||||
Comment on lines
+33
to
+40
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
pub fn format_ui_canister_url_custom( | ||||||||||||||||||||||||||||||
canister_id: &str, | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
provider: &Url, | ||||||||||||||||||||||||||||||
ui_canister_id: &str, | ||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||
) -> Url { | ||||||||||||||||||||||||||||||
let mut url = Url::clone(&provider); | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
if let Some(Domain(domain)) = url.host() { | ||||||||||||||||||||||||||||||
let host = format!("{}.{}", ui_canister_id, domain); | ||||||||||||||||||||||||||||||
let query = format!("id={}", canister_id); | ||||||||||||||||||||||||||||||
url.set_host(Some(&host)).unwrap(); | ||||||||||||||||||||||||||||||
url.set_query(Some(&query)); | ||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||
let query = format!("canisterId={}&id={}", ui_canister_id, canister_id); | ||||||||||||||||||||||||||||||
url.set_query(Some(&query)); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
return url; | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[cfg(test)] | ||||||||||||||||||||||||||||||
mod test { | ||||||||||||||||||||||||||||||
use crate::canister::url::format_frontend_url; | ||||||||||||||||||||||||||||||
use url::Url; | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||
fn print_local_frontend() { | ||||||||||||||||||||||||||||||
let provider1 = &Url::parse("http://127.0.0.1:4943").unwrap(); | ||||||||||||||||||||||||||||||
let provider2 = &Url::parse("http://localhost:4943").unwrap(); | ||||||||||||||||||||||||||||||
let provider3 = &Url::parse("http://127.0.0.1:8000").unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider1, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"http://127.0.0.1:4943/?canisterId=ryjl3-tyaaa-aaaaa-aaaba-cai" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider2, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"http://localhost:4943/?canisterId=ryjl3-tyaaa-aaaaa-aaaba-cai" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider3, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"http://127.0.0.1:8000/?canisterId=ryjl3-tyaaa-aaaaa-aaaba-cai" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||
fn print_ic_frontend() { | ||||||||||||||||||||||||||||||
let provider1 = &Url::parse("https://ic0.app").unwrap(); | ||||||||||||||||||||||||||||||
let provider2 = &Url::parse("https://icp-api.io").unwrap(); | ||||||||||||||||||||||||||||||
let provider3 = &Url::parse("https://icp0.io").unwrap(); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider1, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp0.io/" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider2, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp0.io/" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
assert_eq!( | ||||||||||||||||||||||||||||||
format_frontend_url(provider3, "ryjl3-tyaaa-aaaaa-aaaba-cai").as_str(), | ||||||||||||||||||||||||||||||
"https://ryjl3-tyaaa-aaaaa-aaaba-cai.icp0.io/" | ||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,110 @@ | ||||||||||||||||||||||||||||||||||
use crate::lib::canister_info::CanisterInfo; | ||||||||||||||||||||||||||||||||||
use crate::lib::error::DfxResult; | ||||||||||||||||||||||||||||||||||
use crate::lib::network::network_opt::NetworkOpt; | ||||||||||||||||||||||||||||||||||
use crate::lib::{environment::Environment, named_canister}; | ||||||||||||||||||||||||||||||||||
use anyhow::Context; | ||||||||||||||||||||||||||||||||||
use candid::Principal; | ||||||||||||||||||||||||||||||||||
use clap::Parser; | ||||||||||||||||||||||||||||||||||
use dfx_core::canister::url::{ | ||||||||||||||||||||||||||||||||||
format_frontend_url, format_ui_canister_url_custom, format_ui_canister_url_ic, | ||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||
use dfx_core::config::model::canister_id_store::CanisterIdStore; | ||||||||||||||||||||||||||||||||||
use dfx_core::config::model::network_descriptor::NetworkDescriptor; | ||||||||||||||||||||||||||||||||||
use dfx_core::network::provider::{create_network_descriptor, LocalBindDetermination}; | ||||||||||||||||||||||||||||||||||
use fn_error_context::context; | ||||||||||||||||||||||||||||||||||
use url::Url; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
/// Prints the URL of a canister. | ||||||||||||||||||||||||||||||||||
#[derive(Parser)] | ||||||||||||||||||||||||||||||||||
pub struct CanisterURLOpts { | ||||||||||||||||||||||||||||||||||
/// Specifies the name of the canister. | ||||||||||||||||||||||||||||||||||
canister: String, | ||||||||||||||||||||||||||||||||||
#[command(flatten)] | ||||||||||||||||||||||||||||||||||
network: NetworkOpt, | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[context("Failed to construct frontend url for canister {} on network '{}'.", canister_id, network.name)] | ||||||||||||||||||||||||||||||||||
pub fn construct_frontend_url( | ||||||||||||||||||||||||||||||||||
network: &NetworkDescriptor, | ||||||||||||||||||||||||||||||||||
canister_id: &Principal, | ||||||||||||||||||||||||||||||||||
) -> DfxResult<Url> { | ||||||||||||||||||||||||||||||||||
let url = Url::parse(&network.providers[0]).with_context(|| { | ||||||||||||||||||||||||||||||||||
format!( | ||||||||||||||||||||||||||||||||||
"Failed to parse url for network provider {}.", | ||||||||||||||||||||||||||||||||||
&network.providers[0] | ||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||
})?; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
Ok(format_frontend_url(&url, &canister_id.to_string())) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
#[context("Failed to construct ui canister url for {} on network '{}'.", canister_id, network.name)] | ||||||||||||||||||||||||||||||||||
pub fn construct_ui_canister_url( | ||||||||||||||||||||||||||||||||||
network: &NetworkDescriptor, | ||||||||||||||||||||||||||||||||||
canister_id: &Principal, | ||||||||||||||||||||||||||||||||||
ui_canister_id: Option<Principal>, | ||||||||||||||||||||||||||||||||||
) -> DfxResult<Url> { | ||||||||||||||||||||||||||||||||||
let provider = Url::parse(&network.providers[0]).with_context(|| { | ||||||||||||||||||||||||||||||||||
format!( | ||||||||||||||||||||||||||||||||||
"Failed to parse url for network provider {}.", | ||||||||||||||||||||||||||||||||||
&network.providers[0] | ||||||||||||||||||||||||||||||||||
) | ||||||||||||||||||||||||||||||||||
})?; | ||||||||||||||||||||||||||||||||||
if network.is_ic { | ||||||||||||||||||||||||||||||||||
let formatted_url = format_ui_canister_url_ic(&canister_id.to_string())?; | ||||||||||||||||||||||||||||||||||
return Ok(formatted_url); | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
if let Some(ui_canister_id) = ui_canister_id { | ||||||||||||||||||||||||||||||||||
let formatted_url = format_ui_canister_url_custom( | ||||||||||||||||||||||||||||||||||
&canister_id.to_string(), | ||||||||||||||||||||||||||||||||||
&provider, | ||||||||||||||||||||||||||||||||||
&ui_canister_id.to_string().as_str(), | ||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||
return Ok(formatted_url); | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
return Err(anyhow::anyhow!( | ||||||||||||||||||||||||||||||||||
"Network {} does not have a ui canister id", | ||||||||||||||||||||||||||||||||||
network.name | ||||||||||||||||||||||||||||||||||
)); | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
pub fn exec(env: &dyn Environment, opts: CanisterURLOpts) -> DfxResult { | ||||||||||||||||||||||||||||||||||
env.get_config_or_anyhow()?; | ||||||||||||||||||||||||||||||||||
let network_descriptor = create_network_descriptor( | ||||||||||||||||||||||||||||||||||
env.get_config(), | ||||||||||||||||||||||||||||||||||
env.get_networks_config(), | ||||||||||||||||||||||||||||||||||
opts.network.to_network_name(), | ||||||||||||||||||||||||||||||||||
None, | ||||||||||||||||||||||||||||||||||
LocalBindDetermination::AsConfigured, | ||||||||||||||||||||||||||||||||||
)?; | ||||||||||||||||||||||||||||||||||
let canister_name = opts.canister.as_str(); | ||||||||||||||||||||||||||||||||||
let canister_id_store = | ||||||||||||||||||||||||||||||||||
CanisterIdStore::new(env.get_logger(), &network_descriptor, env.get_config())?; | ||||||||||||||||||||||||||||||||||
let canister_id = | ||||||||||||||||||||||||||||||||||
Principal::from_text(canister_name).or_else(|_| canister_id_store.get(canister_name))?; | ||||||||||||||||||||||||||||||||||
let config = env.get_config_or_anyhow()?; | ||||||||||||||||||||||||||||||||||
let canister_info = CanisterInfo::load(&config, canister_name, Some(canister_id))?; | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
let ui_canister_id = named_canister::get_ui_canister_id(&canister_id_store); | ||||||||||||||||||||||||||||||||||
// If the canister is an assets canister or has a frontend section, we can display a frontend url. | ||||||||||||||||||||||||||||||||||
if let Some(canisters) = &config.get_config().canisters { | ||||||||||||||||||||||||||||||||||
let canister_config = canisters.get(canister_name).unwrap(); | ||||||||||||||||||||||||||||||||||
Comment on lines
+92
to
+93
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never Nesting is more of an aspiration, but we can flatten this a bit, put the error messages next to the error conditions, and eliminate an unwrap:
Suggested change
|
||||||||||||||||||||||||||||||||||
let is_assets = canister_info.is_assets() || canister_config.frontend.is_some(); | ||||||||||||||||||||||||||||||||||
if is_assets { | ||||||||||||||||||||||||||||||||||
let url = construct_frontend_url(&network_descriptor, &canister_id)?; | ||||||||||||||||||||||||||||||||||
println!("{}", url.as_str()); | ||||||||||||||||||||||||||||||||||
Ok(()) | ||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
let url = construct_ui_canister_url(&network_descriptor, &canister_id, ui_canister_id)?; | ||||||||||||||||||||||||||||||||||
println!("{}", url.as_str()); | ||||||||||||||||||||||||||||||||||
Ok(()) | ||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
Comment on lines
+95
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||
} else { | ||||||||||||||||||||||||||||||||||
Err(anyhow::anyhow!( | ||||||||||||||||||||||||||||||||||
"Canister {} does not have a frontend section", | ||||||||||||||||||||||||||||||||||
canister_name | ||||||||||||||||||||||||||||||||||
)) | ||||||||||||||||||||||||||||||||||
Comment on lines
+105
to
+108
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this error message correct? It looks like the condition for getting here is if dfx.json does not define any canisters. |
||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.