1use std::{
2 collections::{BTreeSet, HashMap},
3 path::{Path, PathBuf},
4 str::FromStr,
5 sync::{Arc, mpsc::Sender},
6 time::Duration,
7};
8
9use anyhow::{Context, Result, bail};
10use bytes::Bytes;
11use clap::Parser;
12use tokio::runtime::Handle;
13use url::Url;
14use virtual_fs::{DeviceFile, FileSystem, PassthruFileSystem, RootFileSystemBuilder};
15use virtual_net::ruleset::Ruleset;
16use wasmer::{Engine, Function, Instance, Memory32, Memory64, Module, RuntimeError, Store, Value};
17use wasmer_config::package::PackageSource as PackageSpecifier;
18use wasmer_types::ModuleHash;
19#[cfg(feature = "journal")]
20use wasmer_wasix::journal::{LogFileJournal, SnapshotTrigger};
21use wasmer_wasix::{
22 PluggableRuntime, RewindState, Runtime, WasiEnv, WasiEnvBuilder, WasiError, WasiFunctionEnv,
23 WasiVersion,
24 bin_factory::BinaryPackage,
25 capabilities::Capabilities,
26 default_fs_backing, get_wasi_versions,
27 http::HttpClient,
28 journal::{CompactingLogFileJournal, DynJournal, DynReadableJournal},
29 os::{TtyBridge, tty_sys::SysTty},
30 rewind_ext,
31 runners::MAPPED_CURRENT_DIR_DEFAULT_PATH,
32 runners::{MappedCommand, MappedDirectory},
33 runtime::{
34 module_cache::{FileSystemCache, ModuleCache},
35 package_loader::{BuiltinPackageLoader, PackageLoader},
36 resolver::{
37 BackendSource, FileSystemSource, InMemorySource, MultiSource, Source, WebSource,
38 },
39 task_manager::{
40 VirtualTaskManagerExt,
41 tokio::{RuntimeOrHandle, TokioTaskManager},
42 },
43 },
44 types::__WASI_STDIN_FILENO,
45 wasmer_wasix_types::wasi::Errno,
46};
47
48use crate::{
49 config::{UserRegistry, WasmerEnv},
50 utils::{parse_envvar, parse_mapdir},
51};
52
53use super::{
54 CliPackageSource, ExecutableTarget,
55 capabilities::{self, PkgCapabilityCache},
56};
57
58const WAPM_SOURCE_CACHE_TIMEOUT: Duration = Duration::from_secs(10 * 60);
59
60#[derive(Debug, Parser, Clone, Default)]
61pub struct Wasi {
63 #[clap(long = "dir", name = "DIR", group = "wasi")]
65 pub(crate) pre_opened_directories: Vec<PathBuf>,
66
67 #[clap(
69 long = "mapdir",
70 name = "GUEST_DIR:HOST_DIR",
71 value_parser=parse_mapdir,
72 )]
73 pub(crate) mapped_dirs: Vec<MappedDirectory>,
74
75 #[clap(long = "cwd")]
78 pub(crate) cwd: Option<PathBuf>,
79
80 #[clap(
82 long = "env",
83 name = "KEY=VALUE",
84 value_parser=parse_envvar,
85 )]
86 pub(crate) env_vars: Vec<(String, String)>,
87
88 #[clap(long, env)]
90 pub(crate) forward_host_env: bool,
91
92 #[clap(long = "use", name = "USE")]
94 pub(crate) uses: Vec<String>,
95
96 #[clap(long = "include-webc", name = "WEBC")]
99 pub(super) include_webcs: Vec<PathBuf>,
100
101 #[clap(long = "map-command", name = "MAPCMD")]
103 pub(super) map_commands: Vec<String>,
104
105 #[clap(long = "net", require_equals = true)]
124 pub networking: Option<Option<String>>,
127
128 #[clap(long = "no-tty")]
130 pub no_tty: bool,
131
132 #[clap(long = "enable-async-threads")]
134 pub enable_async_threads: bool,
135
136 #[clap(long = "enable-cpu-backoff")]
141 pub enable_cpu_backoff: Option<u64>,
142
143 #[cfg(feature = "journal")]
149 #[clap(long = "journal")]
150 pub read_only_journals: Vec<PathBuf>,
151
152 #[cfg(feature = "journal")]
162 #[clap(long = "journal-writable")]
163 pub writable_journals: Vec<PathBuf>,
164
165 #[cfg(feature = "journal")]
168 #[clap(long = "enable-compaction")]
169 pub enable_compaction: bool,
170
171 #[cfg(feature = "journal")]
173 #[clap(long = "without-compact-on-drop")]
174 pub without_compact_on_drop: bool,
175
176 #[cfg(feature = "journal")]
182 #[clap(long = "with-compact-on-growth", default_value = "0.15")]
183 pub with_compact_on_growth: f32,
184
185 #[cfg(feature = "journal")]
196 #[clap(long = "snapshot-on")]
197 pub snapshot_on: Vec<SnapshotTrigger>,
198
199 #[cfg(feature = "journal")]
203 #[clap(long = "snapshot-period")]
204 pub snapshot_interval: Option<u64>,
205
206 #[cfg(feature = "journal")]
209 #[clap(long = "stop-after-snapshot")]
210 pub stop_after_snapshot: bool,
211
212 #[cfg(feature = "journal")]
214 #[clap(long = "skip-journal-stdio")]
215 pub skip_stdio_during_bootstrap: bool,
216
217 #[clap(long)]
221 pub http_client: bool,
222
223 #[clap(long = "deny-multiple-wasi-versions")]
225 pub deny_multiple_wasi_versions: bool,
226
227 #[clap(long = "disable-cache")]
232 disable_cache: bool,
233}
234
235pub struct RunProperties {
236 pub ctx: WasiFunctionEnv,
237 pub path: PathBuf,
238 pub invoke: Option<String>,
239 pub args: Vec<String>,
240}
241
242fn endpoint_to_folder(url: &Url) -> String {
243 url.to_string()
244 .replace("registry.wasmer.io", "wasmer.io")
245 .replace("registry.wasmer.wtf", "wasmer.wtf")
246 .replace(|c| "/:?&=#%\\".contains(c), "_")
247}
248
249#[allow(dead_code)]
250impl Wasi {
251 pub fn map_dir(&mut self, alias: &str, target_on_disk: PathBuf) {
252 self.mapped_dirs.push(MappedDirectory {
253 guest: alias.to_string(),
254 host: target_on_disk,
255 });
256 }
257
258 pub fn set_env(&mut self, key: &str, value: &str) {
259 self.env_vars.push((key.to_string(), value.to_string()));
260 }
261
262 pub fn get_versions(module: &Module) -> Option<BTreeSet<WasiVersion>> {
264 get_wasi_versions(module, false)
269 }
270
271 pub fn has_wasi_imports(module: &Module) -> bool {
273 get_wasi_versions(module, false).is_some()
276 }
277
278 pub fn prepare(
279 &self,
280 module: &Module,
281 program_name: String,
282 args: Vec<String>,
283 rt: Arc<dyn Runtime + Send + Sync>,
284 ) -> Result<WasiEnvBuilder> {
285 let args = args.into_iter().map(|arg| arg.into_bytes());
286
287 let map_commands = self
288 .map_commands
289 .iter()
290 .map(|map| map.split_once('=').unwrap())
291 .map(|(a, b)| (a.to_string(), b.to_string()))
292 .collect::<HashMap<_, _>>();
293
294 let mut uses = Vec::new();
295 for name in &self.uses {
296 let specifier = PackageSpecifier::from_str(name)
297 .with_context(|| format!("Unable to parse \"{name}\" as a package specifier"))?;
298 let pkg = {
299 let inner_rt = rt.clone();
300 rt.task_manager()
301 .spawn_and_block_on(async move {
302 BinaryPackage::from_registry(&specifier, &*inner_rt).await
303 })
304 .with_context(|| format!("Unable to load \"{name}\""))??
305 };
306 uses.push(pkg);
307 }
308
309 let mut builder = WasiEnv::builder(program_name)
310 .runtime(Arc::clone(&rt))
311 .args(args)
312 .envs(self.env_vars.clone())
313 .uses(uses)
314 .map_commands(map_commands);
315
316 let mut builder = {
317 let root_fs = RootFileSystemBuilder::new()
319 .with_tty(Box::new(DeviceFile::new(__WASI_STDIN_FILENO)))
320 .build();
321
322 let mut mapped_dirs = Vec::new();
323
324 let mut have_current_dir = false;
326 for dir in &self.pre_opened_directories {
327 let mapping = if dir == Path::new(".") {
328 if have_current_dir {
329 bail!(
330 "Cannot pre-open the current directory twice: --dir=. must only be specified once"
331 );
332 }
333 have_current_dir = true;
334
335 let current_dir =
336 std::env::current_dir().context("could not determine current directory")?;
337
338 MappedDirectory {
339 host: current_dir,
340 guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
341 }
342 } else {
343 let resolved = dir.canonicalize().with_context(|| {
344 format!(
345 "could not canonicalize path for argument '--dir {}'",
346 dir.display()
347 )
348 })?;
349
350 if &resolved != dir {
351 bail!(
352 "Invalid argument '--dir {}': path must either be absolute, or '.'",
353 dir.display(),
354 );
355 }
356
357 let guest = resolved
358 .to_str()
359 .with_context(|| {
360 format!(
361 "invalid argument '--dir {}': path must be valid utf-8",
362 dir.display(),
363 )
364 })?
365 .to_string();
366
367 MappedDirectory {
368 host: resolved,
369 guest,
370 }
371 };
372
373 mapped_dirs.push(mapping);
374 }
375
376 for MappedDirectory { host, guest } in &self.mapped_dirs {
377 let resolved_host = host.canonicalize().with_context(|| {
378 format!(
379 "could not canonicalize path for argument '--mapdir {}:{}'",
380 host.display(),
381 guest,
382 )
383 })?;
384
385 let mapping = if guest == "." {
386 if have_current_dir {
387 bail!(
388 "Cannot pre-open the current directory twice: '--mapdir=?:.' / '--dir=.' must only be specified once"
389 );
390 }
391 have_current_dir = true;
392
393 MappedDirectory {
394 host: resolved_host,
395 guest: MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string(),
396 }
397 } else {
398 MappedDirectory {
399 host: resolved_host,
400 guest: guest.clone(),
401 }
402 };
403 mapped_dirs.push(mapping);
404 }
405
406 if !mapped_dirs.is_empty() {
407 let fs_backing: Arc<dyn FileSystem + Send + Sync> =
409 Arc::new(PassthruFileSystem::new_arc(default_fs_backing()));
410 for MappedDirectory { host, guest } in self.mapped_dirs.clone() {
411 let host = if !host.is_absolute() {
412 Path::new("/").join(host)
413 } else {
414 host
415 };
416 root_fs.mount(guest.into(), &fs_backing, host)?;
417 }
418 }
419
420 if let Some(cwd) = self.cwd.as_ref() {
421 if !cwd.starts_with("/") {
422 bail!("The argument to --cwd must be an absolute path");
423 }
424 builder = builder.current_dir(cwd.clone());
425 }
426
427 builder = builder
429 .sandbox_fs(root_fs)
430 .preopen_dir(Path::new("/"))
431 .unwrap();
432
433 if have_current_dir {
434 builder.map_dir(".", MAPPED_CURRENT_DIR_DEFAULT_PATH)?
435 } else {
436 builder.map_dir(".", "/")?
437 }
438 };
439
440 *builder.capabilities_mut() = self.capabilities();
441
442 #[cfg(feature = "journal")]
443 {
444 for trigger in self.snapshot_on.iter().cloned() {
445 builder.add_snapshot_trigger(trigger);
446 }
447 if let Some(interval) = self.snapshot_interval {
448 builder.with_snapshot_interval(std::time::Duration::from_millis(interval));
449 }
450 if self.stop_after_snapshot {
451 builder.with_stop_running_after_snapshot(true);
452 }
453 let (r, w) = self.build_journals()?;
454 for journal in r {
455 builder.add_read_only_journal(journal);
456 }
457 for journal in w {
458 builder.add_writable_journal(journal);
459 }
460 builder.with_skip_stdio_during_bootstrap(self.skip_stdio_during_bootstrap);
461 }
462
463 Ok(builder)
464 }
465
466 #[cfg(feature = "journal")]
467 #[allow(clippy::type_complexity)]
468 pub fn build_journals(
469 &self,
470 ) -> anyhow::Result<(Vec<Arc<DynReadableJournal>>, Vec<Arc<DynJournal>>)> {
471 let mut readable = Vec::new();
472 for journal in self.read_only_journals.clone() {
473 if matches!(std::fs::metadata(&journal), Err(e) if e.kind() == std::io::ErrorKind::NotFound)
474 {
475 bail!("Read-only journal file does not exist: {journal:?}");
476 }
477
478 readable
479 .push(Arc::new(LogFileJournal::new_readonly(journal)?) as Arc<DynReadableJournal>);
480 }
481
482 let mut writable = Vec::new();
483 for journal in self.writable_journals.clone() {
484 if self.enable_compaction {
485 let mut journal = CompactingLogFileJournal::new(journal)?;
486 if !self.without_compact_on_drop {
487 journal = journal.with_compact_on_drop()
488 }
489 if self.with_compact_on_growth.is_normal() && self.with_compact_on_growth != 0f32 {
490 journal = journal.with_compact_on_factor_size(self.with_compact_on_growth);
491 }
492 writable.push(Arc::new(journal) as Arc<DynJournal>);
493 } else {
494 writable.push(Arc::new(LogFileJournal::new(journal)?));
495 }
496 }
497 Ok((readable, writable))
498 }
499
500 #[cfg(not(feature = "journal"))]
501 pub fn build_journals(&self) -> anyhow::Result<Vec<Arc<DynJournal>>> {
502 Ok(Vec::new())
503 }
504
505 pub fn build_mapped_directories(
506 &self,
507 is_wasix: bool,
508 ) -> Result<(bool, Vec<MappedDirectory>), anyhow::Error> {
509 let mut mapped_dirs = Vec::new();
510
511 let mut have_current_dir = false;
513 for dir in &self.pre_opened_directories {
514 let mapping = if dir == Path::new(".") {
515 if have_current_dir {
516 bail!(
517 "Cannot pre-open the current directory twice: --dir=. must only be specified once"
518 );
519 }
520 have_current_dir = true;
521
522 let current_dir =
523 std::env::current_dir().context("could not determine current directory")?;
524
525 MappedDirectory {
526 host: current_dir,
527 guest: if is_wasix {
528 MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string()
529 } else {
530 "/".to_string()
531 },
532 }
533 } else {
534 if dir == Path::new("/") && is_wasix {
535 tracing::warn!(
536 "Pre-opening the root directory with --dir=/ breaks WASIX modules' filesystems"
537 );
538 }
539
540 let resolved = dir.canonicalize().with_context(|| {
541 format!(
542 "could not canonicalize path for argument '--dir {}'",
543 dir.display()
544 )
545 })?;
546
547 if &resolved != dir {
548 bail!(
549 "Invalid argument '--dir {}': path must either be absolute, or '.'",
550 dir.display(),
551 );
552 }
553
554 let guest = resolved
555 .to_str()
556 .with_context(|| {
557 format!(
558 "invalid argument '--dir {}': path must be valid utf-8",
559 dir.display(),
560 )
561 })?
562 .to_string();
563
564 MappedDirectory {
565 host: resolved,
566 guest,
567 }
568 };
569
570 mapped_dirs.push(mapping);
571 }
572
573 for MappedDirectory { host, guest } in &self.mapped_dirs {
574 if guest == "/" && is_wasix {
575 tracing::warn!(
578 "Mounting on the guest's virtual root with --mapdir /:<HOST_PATH> breaks WASIX modules' filesystems"
579 );
580 }
581 let resolved_host = host.canonicalize().with_context(|| {
582 format!(
583 "could not canonicalize path for argument '--mapdir {}:{}'",
584 guest,
585 host.display(),
586 )
587 })?;
588
589 let mapping = if guest == "." {
590 if have_current_dir {
591 bail!(
592 "Cannot pre-open the current directory twice: '--mapdir=?:.' / '--dir=.' must only be specified once"
593 );
594 }
595 have_current_dir = true;
596
597 MappedDirectory {
598 host: resolved_host,
599 guest: if is_wasix {
600 MAPPED_CURRENT_DIR_DEFAULT_PATH.to_string()
601 } else {
602 "/".to_string()
603 },
604 }
605 } else {
606 MappedDirectory {
607 host: resolved_host,
608 guest: guest.clone(),
609 }
610 };
611 mapped_dirs.push(mapping);
612 }
613
614 Ok((have_current_dir, mapped_dirs))
615 }
616
617 pub fn build_mapped_commands(&self) -> Result<Vec<MappedCommand>, anyhow::Error> {
618 self.map_commands
619 .iter()
620 .map(|item| {
621 let (a, b) = item.split_once('=').with_context(|| {
622 format!(
623 "Invalid --map-command flag: expected <ALIAS>=<HOST_PATH>, got '{item}'"
624 )
625 })?;
626
627 let a = a.trim();
628 let b = b.trim();
629
630 if a.is_empty() {
631 bail!("Invalid --map-command flag - alias cannot be empty: '{item}'");
632 }
633 if b.is_empty() {
635 bail!("Invalid --map-command flag - host path cannot be empty: '{item}'");
636 }
637
638 Ok(MappedCommand {
639 alias: a.to_string(),
640 target: b.to_string(),
641 })
642 })
643 .collect::<Result<Vec<_>, anyhow::Error>>()
644 }
645
646 pub fn capabilities(&self) -> Capabilities {
647 let mut caps = Capabilities::default();
648
649 if self.http_client {
650 caps.http_client = wasmer_wasix::http::HttpClientCapabilityV1::new_allow_all();
651 }
652
653 caps.threading.enable_asynchronous_threading = self.enable_async_threads;
654 caps.threading.enable_exponential_cpu_backoff =
655 self.enable_cpu_backoff.map(Duration::from_millis);
656
657 caps
658 }
659
660 pub fn prepare_runtime<I>(
661 &self,
662 engine: Engine,
663 env: &WasmerEnv,
664 pkg_cache_path: &Path,
665 rt_or_handle: I,
666 preferred_webc_version: webc::Version,
667 ) -> Result<impl Runtime + Send + Sync + use<I>>
668 where
669 I: Into<RuntimeOrHandle>,
670 {
671 let tokio_task_manager = Arc::new(TokioTaskManager::new(rt_or_handle.into()));
672 let mut rt = PluggableRuntime::new(tokio_task_manager.clone());
673
674 let has_networking = self.networking.is_some()
675 || capabilities::get_cached_capability(pkg_cache_path)
676 .ok()
677 .is_some_and(|v| v.enable_networking);
678
679 let ruleset = self
680 .networking
681 .clone()
682 .flatten()
683 .map(|ruleset| Ruleset::from_str(&ruleset))
684 .transpose()?;
685
686 let network = if let Some(ruleset) = ruleset {
687 virtual_net::host::LocalNetworking::with_ruleset(ruleset)
688 } else {
689 virtual_net::host::LocalNetworking::default()
690 };
691
692 if has_networking {
693 rt.set_networking_implementation(network);
694 } else {
695 let net = super::capabilities::net::AskingNetworking::new(
696 pkg_cache_path.to_path_buf(),
697 Arc::new(network),
698 );
699
700 rt.set_networking_implementation(net);
701 }
702
703 #[cfg(feature = "journal")]
704 {
705 let (r, w) = self.build_journals()?;
706 for journal in r {
707 rt.add_read_only_journal(journal);
708 }
709 for journal in w {
710 rt.add_writable_journal(journal);
711 }
712 }
713
714 if !self.no_tty {
715 let tty = Arc::new(SysTty);
716 tty.reset();
717 rt.set_tty(tty);
718 }
719
720 let client =
721 wasmer_wasix::http::default_http_client().context("No HTTP client available")?;
722 let client = Arc::new(client);
723
724 let package_loader = self
725 .prepare_package_loader(env, client.clone())
726 .context("Unable to prepare the package loader")?;
727
728 let registry = self.prepare_source(env, client, preferred_webc_version)?;
729
730 if !self.disable_cache {
731 let cache_dir = env.cache_dir().join("compiled");
732 let module_cache = wasmer_wasix::runtime::module_cache::in_memory()
733 .with_fallback(FileSystemCache::new(cache_dir, tokio_task_manager));
734 rt.set_module_cache(module_cache);
735 }
736
737 rt.set_package_loader(package_loader)
738 .set_source(registry)
739 .set_engine(engine);
740
741 Ok(rt)
742 }
743
744 pub fn instantiate(
746 &self,
747 module: &Module,
748 module_hash: ModuleHash,
749 program_name: String,
750 args: Vec<String>,
751 runtime: Arc<dyn Runtime + Send + Sync>,
752 store: &mut Store,
753 ) -> Result<(WasiFunctionEnv, Instance)> {
754 let builder = self.prepare(module, program_name, args, runtime)?;
755 let (instance, wasi_env) = builder.instantiate_ext(module.clone(), module_hash, store)?;
756
757 Ok((wasi_env, instance))
758 }
759
760 pub fn for_binfmt_interpreter() -> Result<Self> {
761 let dir = std::env::var_os("WASMER_BINFMT_MISC_PREOPEN")
762 .map(Into::into)
763 .unwrap_or_else(|| PathBuf::from("."));
764 Ok(Self {
765 deny_multiple_wasi_versions: true,
766 env_vars: std::env::vars().collect(),
767 pre_opened_directories: vec![dir],
768 ..Self::default()
769 })
770 }
771
772 fn prepare_package_loader(
773 &self,
774 env: &WasmerEnv,
775 client: Arc<dyn HttpClient + Send + Sync>,
776 ) -> Result<BuiltinPackageLoader> {
777 let checkout_dir = env.cache_dir().join("checkouts");
778 let tokens = tokens_by_authority(env)?;
779
780 let loader = BuiltinPackageLoader::new()
781 .with_cache_dir(checkout_dir)
782 .with_shared_http_client(client)
783 .with_tokens(tokens);
784
785 Ok(loader)
786 }
787
788 fn prepare_source(
789 &self,
790 env: &WasmerEnv,
791 client: Arc<dyn HttpClient + Send + Sync>,
792 preferred_webc_version: webc::Version,
793 ) -> Result<MultiSource> {
794 let mut source = MultiSource::default();
795
796 let mut preloaded = InMemorySource::new();
799 for path in &self.include_webcs {
800 preloaded
801 .add_webc(path)
802 .with_context(|| format!("Unable to load \"{}\"", path.display()))?;
803 }
804 source.add_source(preloaded);
805
806 let graphql_endpoint = self.graphql_endpoint(env)?;
807 let cache_dir = env
808 .cache_dir()
809 .join("queries")
810 .join(endpoint_to_folder(&graphql_endpoint));
811 let mut wapm_source = BackendSource::new(graphql_endpoint, Arc::clone(&client))
812 .with_local_cache(cache_dir, WAPM_SOURCE_CACHE_TIMEOUT)
813 .with_preferred_webc_version(preferred_webc_version);
814 if let Some(token) = env
815 .config()?
816 .registry
817 .get_login_token_for_registry(wapm_source.registry_endpoint().as_str())
818 {
819 wapm_source = wapm_source.with_auth_token(token);
820 }
821 source.add_source(wapm_source);
822
823 let cache_dir = env.cache_dir().join("downloads");
824 source.add_source(WebSource::new(cache_dir, client));
825
826 source.add_source(FileSystemSource::default());
827
828 Ok(source)
829 }
830
831 fn graphql_endpoint(&self, env: &WasmerEnv) -> Result<Url> {
832 if let Ok(endpoint) = env.registry_endpoint() {
833 return Ok(endpoint);
834 }
835
836 let config = env.config()?;
837 let graphql_endpoint = config.registry.get_graphql_url();
838 let graphql_endpoint = graphql_endpoint
839 .parse()
840 .with_context(|| format!("Unable to parse \"{graphql_endpoint}\" as a URL"))?;
841
842 Ok(graphql_endpoint)
843 }
844}
845
846fn parse_registry(r: &str) -> Result<Url> {
847 UserRegistry::from(r).graphql_endpoint()
848}
849
850fn tokens_by_authority(env: &WasmerEnv) -> Result<HashMap<String, String>> {
851 let mut tokens = HashMap::new();
852 let config = env.config()?;
853
854 for credentials in config.registry.tokens {
855 if let Ok(url) = Url::parse(&credentials.registry)
856 && url.has_authority()
857 {
858 tokens.insert(url.authority().to_string(), credentials.token);
859 }
860 }
861
862 if let (Ok(current_registry), Some(token)) = (env.registry_endpoint(), env.token())
863 && current_registry.has_authority()
864 {
865 tokens.insert(current_registry.authority().to_string(), token);
866 }
867
868 let mut frontend_tokens = HashMap::new();
881 for (hostname, token) in &tokens {
882 if let Some(frontend_url) = hostname.strip_prefix("registry.")
883 && !tokens.contains_key(frontend_url)
884 {
885 frontend_tokens.insert(frontend_url.to_string(), token.clone());
886 }
887 }
888 tokens.extend(frontend_tokens);
889
890 Ok(tokens)
891}