1use std::{
4 collections::{HashMap, HashSet},
5 ops::{Deref, DerefMut},
6 path::PathBuf,
7 sync::Arc,
8};
9
10use anyhow::{Context as _, bail, ensure};
11use tokio::{sync::RwLock, task::JoinHandle};
12use tracing::{debug, info, trace, warn};
13use wasmtime::component::{
14 Component, Instance, InstancePre, Linker, ResourceAny, ResourceType, Val, types::ComponentItem,
15};
16use wasmtime_wasi::{DirPerms, FilePerms, WasiCtxBuilder, bindings::CommandPre};
17
18use crate::{
19 engine::{
20 ctx::Ctx,
21 value::{lift, lower},
22 },
23 plugin::HostPlugin,
24 types::{LocalResources, VolumeMount},
25 wit::{WitInterface, WitWorld},
26};
27
28#[derive(Clone)]
30pub struct WorkloadMetadata {
31 id: Arc<str>,
33 workload_id: Arc<str>,
35 workload_name: Arc<str>,
37 workload_namespace: Arc<str>,
39 component: Component,
41 linker: Linker<Ctx>,
43 volume_mounts: Vec<(PathBuf, VolumeMount)>,
45 local_resources: LocalResources,
47 plugins: Option<HashMap<&'static str, Arc<dyn HostPlugin + Send + Sync>>>,
49}
50
51impl WorkloadMetadata {
52 pub fn id(&self) -> &str {
54 &self.id
55 }
56
57 pub fn workload_id(&self) -> &str {
59 &self.workload_id
60 }
61
62 pub fn workload_name(&self) -> &str {
64 &self.workload_name
65 }
66
67 pub fn workload_namespace(&self) -> &str {
69 &self.workload_namespace
70 }
71
72 pub fn engine(&self) -> &wasmtime::Engine {
74 self.component.engine()
75 }
76
77 pub fn linker(&mut self) -> &mut Linker<Ctx> {
79 &mut self.linker
80 }
81
82 pub fn local_resources(&self) -> &LocalResources {
84 &self.local_resources
85 }
86
87 pub fn plugins(&self) -> &Option<HashMap<&'static str, Arc<dyn HostPlugin + Send + Sync>>> {
89 &self.plugins
90 }
91
92 pub fn add_plugin(&mut self, id: &'static str, plugin: Arc<dyn HostPlugin + Send + Sync>) {
94 if let Some(ref mut plugins) = self.plugins {
95 plugins.insert(id, plugin);
96 } else {
97 let mut plugins = HashMap::new();
98 plugins.insert(id, plugin);
99 self.plugins = Some(plugins);
100 }
101 }
102
103 pub fn with_plugins(
105 &mut self,
106 plugins: HashMap<&'static str, Arc<dyn HostPlugin + Send + Sync>>,
107 ) {
108 self.plugins = Some(plugins);
109 }
110
111 pub fn component_exports(&self) -> anyhow::Result<Vec<(String, ComponentItem)>> {
113 Ok(self
114 .component
115 .component_type()
116 .exports(self.component.engine())
117 .filter_map(|(name, item)| {
118 if matches!(item, ComponentItem::ComponentInstance(_)) {
119 Some((name.to_string(), item))
120 } else {
121 None
122 }
123 })
124 .collect::<Vec<_>>())
125 }
126
127 pub fn world(&self) -> WitWorld {
129 let mut imports = HashMap::new();
130 let mut exports = HashMap::new();
131
132 for (import_name, import_item) in self
134 .component
135 .component_type()
136 .imports(self.component.engine())
137 {
138 if let ComponentItem::ComponentInstance(_) = import_item {
139 let interface = WitInterface::from(import_name);
140 let k = interface.instance();
141 imports
142 .entry(k)
143 .and_modify(|existing: &mut WitInterface| {
144 existing.merge(&interface);
145 })
146 .or_insert(interface);
147 } else {
148 debug!(
149 import_name,
150 "imported item is not a component instance, skipping"
151 );
152 }
153 }
154
155 for (export_name, export_item) in self
157 .component
158 .component_type()
159 .exports(self.component.engine())
160 {
161 if let ComponentItem::ComponentInstance(_) = export_item {
162 let interface = WitInterface::from(export_name);
163 let k = interface.instance();
164 exports
165 .entry(k)
166 .and_modify(|existing: &mut WitInterface| {
167 existing.merge(&interface);
168 })
169 .or_insert(interface);
170 } else {
171 debug!(
172 export_name,
173 "exported item is not a component instance, skipping"
174 );
175 }
176 }
177
178 WitWorld {
179 imports: imports.into_values().collect(),
180 exports: exports.into_values().collect(),
181 }
182 }
183}
184
185#[derive(Clone)]
188pub struct WorkloadService {
189 metadata: WorkloadMetadata,
191 max_restarts: u64,
193 handle: Option<Arc<JoinHandle<()>>>,
195}
196
197impl WorkloadService {
198 #[allow(clippy::too_many_arguments)]
201 pub fn new(
202 workload_id: impl Into<Arc<str>>,
203 workload_name: impl Into<Arc<str>>,
204 workload_namespace: impl Into<Arc<str>>,
205 component: Component,
206 linker: Linker<Ctx>,
207 volume_mounts: Vec<(PathBuf, VolumeMount)>,
208 local_resources: LocalResources,
209 max_restarts: u64,
210 ) -> Self {
211 Self {
212 metadata: WorkloadMetadata {
213 id: uuid::Uuid::new_v4().to_string().into(),
214 workload_id: workload_id.into(),
215 workload_name: workload_name.into(),
216 workload_namespace: workload_namespace.into(),
217 component,
218 linker,
219 volume_mounts,
220 local_resources,
221 plugins: None,
222 },
223 handle: None,
224 max_restarts,
225 }
226 }
227
228 pub fn pre_instantiate(&mut self) -> anyhow::Result<CommandPre<Ctx>> {
230 let component = self.metadata.component.clone();
231 let pre = self.metadata.linker.instantiate_pre(&component)?;
232 let command = CommandPre::new(pre)?;
233 Ok(command)
234 }
235
236 pub fn is_running(&self) -> bool {
238 self.handle.is_some()
239 }
240}
241
242#[derive(Clone)]
249pub struct WorkloadComponent {
250 metadata: WorkloadMetadata,
252 pool_size: usize,
254 max_invocations: usize,
256}
257
258impl WorkloadComponent {
259 pub fn new(
262 workload_id: impl Into<Arc<str>>,
263 workload_name: impl Into<Arc<str>>,
264 workload_namespace: impl Into<Arc<str>>,
265 component: Component,
266 linker: Linker<Ctx>,
267 volume_mounts: Vec<(PathBuf, VolumeMount)>,
268 local_resources: LocalResources,
269 ) -> Self {
270 Self {
271 metadata: WorkloadMetadata {
272 id: uuid::Uuid::new_v4().to_string().into(),
273 workload_id: workload_id.into(),
274 workload_name: workload_name.into(),
275 workload_namespace: workload_namespace.into(),
276 component,
277 linker,
278 volume_mounts,
279 local_resources,
280 plugins: None,
281 },
282 pool_size: 0,
284 max_invocations: 0,
285 }
286 }
287
288 pub fn pre_instantiate(&mut self) -> anyhow::Result<InstancePre<Ctx>> {
290 let component = self.metadata.component.clone();
291 self.metadata.linker.instantiate_pre(&component)
292 }
293}
294
295impl std::fmt::Debug for WorkloadComponent {
296 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
297 f.debug_struct("WorkloadComponent")
298 .field("id", &self.metadata.id.as_ref())
299 .field("workload_id", &self.metadata.workload_id.as_ref())
300 .field("volume_mounts", &self.metadata.volume_mounts)
301 .field("pool_size", &self.pool_size)
302 .field("max_invocations", &self.max_invocations)
303 .finish()
304 }
305}
306
307impl std::fmt::Debug for WorkloadService {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.debug_struct("WorkloadService")
310 .field("id", &self.metadata.id.as_ref())
311 .field("workload_name", &self.metadata.workload_name.as_ref())
312 .field(
313 "workload_namespace",
314 &self.metadata.workload_namespace.as_ref(),
315 )
316 .field("workload_id", &self.metadata.workload_id.as_ref())
317 .field("volume_mounts", &self.metadata.volume_mounts)
318 .field("is_running", &self.is_running())
319 .finish()
320 }
321}
322
323impl Deref for WorkloadComponent {
324 type Target = WorkloadMetadata;
325
326 fn deref(&self) -> &Self::Target {
327 &self.metadata
328 }
329}
330
331impl DerefMut for WorkloadComponent {
332 fn deref_mut(&mut self) -> &mut Self::Target {
333 &mut self.metadata
334 }
335}
336
337impl Deref for WorkloadService {
338 type Target = WorkloadMetadata;
339
340 fn deref(&self) -> &Self::Target {
341 &self.metadata
342 }
343}
344
345impl DerefMut for WorkloadService {
346 fn deref_mut(&mut self) -> &mut Self::Target {
347 &mut self.metadata
348 }
349}
350
351#[derive(Debug, Clone)]
357pub struct ResolvedWorkload {
358 id: Arc<str>,
360 name: Arc<str>,
362 namespace: Arc<str>,
364 components: Arc<RwLock<HashMap<Arc<str>, WorkloadComponent>>>,
367 service: Option<WorkloadService>,
369}
370
371impl ResolvedWorkload {
372 pub(crate) async fn execute_service(&mut self) -> anyhow::Result<bool> {
374 let service = self
375 .service
376 .as_mut()
377 .map(|s| (s.pre_instantiate(), s.max_restarts));
378
379 if let Some((Ok(pre), mut max_restarts)) = service {
380 let mut store = self.new_service_store().await?;
381 let instance = pre.instantiate_async(&mut store).await?;
382 let handle = tokio::spawn(async move {
383 loop {
384 if let Err(e) = instance.wasi_cli_run().call_run(&mut store).await {
385 warn!(err = %e, retries = max_restarts, "service execution failed");
386 if max_restarts == 0 {
387 info!("max restarts reached, service will not be restarted");
388 break;
389 }
390 } else {
391 info!("service executed successfully");
392 break;
393 }
394 max_restarts = max_restarts.saturating_sub(1);
395 }
396 });
397
398 if let Some(s) = self.service.as_mut() {
400 s.handle = Some(Arc::new(handle));
401 }
402 Ok(true)
403 } else {
404 Ok(false)
405 }
406 }
407
408 pub(crate) fn stop_service(&self) {
410 if let Some(service) = &self.service
411 && let Some(handle) = &service.handle
412 {
413 handle.abort();
414 debug!(
415 workload_id = self.id.as_ref(),
416 "service for workload aborted"
417 );
418 }
419 }
420
421 async fn link_components(&mut self) -> anyhow::Result<()> {
422 let mut interface_map: HashMap<String, Arc<str>> = HashMap::new();
424
425 for c in self.components.read().await.values() {
427 let exported_instances = c.component_exports()?;
428 for (name, item) in exported_instances {
429 match name.split_once('@') {
431 Some(("wasmcloud:wash/plugin", _)) => {
432 trace!(name, "skipping internal plugin export");
433 continue;
434 }
435 None => {
436 if name == "wasmcloud:wash/plugin" {
437 trace!(name, "skipping internal plugin export");
438 continue;
439 }
440 }
441 _ => {}
442 }
443 if let ComponentItem::ComponentInstance(_) = item {
444 if interface_map.contains_key(&name) {
446 anyhow::bail!(
447 "another component already implements the interface '{name}'"
448 );
449 }
450 trace!(name, "registering component export for linking");
451 interface_map.insert(name.clone(), Arc::from(c.id()));
452 } else {
453 warn!(name, "exported item is not a component instance, skipping");
454 }
455 }
456 }
457
458 self.resolve_workload_imports(&interface_map).await?;
459
460 Ok(())
461 }
462
463 async fn resolve_workload_imports(
466 &mut self,
467 interface_map: &HashMap<String, Arc<str>>,
468 ) -> anyhow::Result<()> {
469 let component_ids: Vec<Arc<str>> = self.components.read().await.keys().cloned().collect();
470 for component_id in component_ids {
471 let mut workload_component = {
474 self.components
475 .write()
476 .await
477 .remove(&component_id)
478 .context("component not found during import resolution")?
479 };
480
481 let component = workload_component.metadata.component.clone();
482 let linker = &mut workload_component.metadata.linker;
483 let res = self
484 .resolve_component_imports(&component, linker, interface_map)
485 .await;
486 self.components
487 .write()
488 .await
489 .insert(workload_component.metadata.id.clone(), workload_component);
490 res?;
492 }
493
494 if let Some(mut service) = self.service.take() {
495 let component = service.metadata.component.clone();
496 let linker = &mut service.metadata.linker;
497
498 let res = self
499 .resolve_component_imports(&component, linker, interface_map)
500 .await;
501
502 self.service = Some(service);
503
504 res?;
506 }
507
508 Ok(())
509 }
510
511 async fn resolve_component_imports(
512 &self,
513 component: &wasmtime::component::Component,
514 linker: &mut Linker<Ctx>,
515 interface_map: &HashMap<String, Arc<str>>,
516 ) -> anyhow::Result<()> {
517 let ty = component.component_type();
518 let imports: Vec<_> = ty.imports(component.engine()).collect();
519
520 let instance: Arc<RwLock<Option<(String, Instance)>>> = Arc::default();
523 for (import_name, import_item) in imports.into_iter() {
524 match import_item {
525 ComponentItem::ComponentInstance(import_instance_ty) => {
526 trace!(name = import_name, "processing component instance import");
527 let mut all_components = self.components.write().await;
528 let (plugin_component, instance_idx) = {
529 let Some(exporter_component) = interface_map.get(import_name) else {
530 trace!(
533 name = import_name,
534 "import not found in component exports, skipping"
535 );
536 continue;
537 };
538 let Some(plugin_component) = all_components.get_mut(exporter_component)
539 else {
540 trace!(
541 name = import_name,
542 "exporting component not found in all components, skipping"
543 );
544 continue;
545 };
546 let Some((ComponentItem::ComponentInstance(_), idx)) = plugin_component
547 .metadata
548 .component
549 .export_index(None, import_name)
550 else {
551 trace!(name = import_name, "skipping non-instance import");
552 continue;
553 };
554 (plugin_component, idx)
555 };
556 trace!(name = import_name, index = ?instance_idx, "found import at index");
557
558 let pre = plugin_component
560 .pre_instantiate()
561 .context("failed to pre-instantiate during component linking")?;
562
563 let mut linker_instance = match linker.instance(import_name) {
564 Ok(i) => i,
565 Err(e) => {
566 trace!(name = import_name, error = %e, "error finding instance in linker, skipping");
567 continue;
568 }
569 };
570
571 for (export_name, export_ty) in
572 import_instance_ty.exports(plugin_component.metadata.component.engine())
573 {
574 match export_ty {
575 ComponentItem::ComponentFunc(_func_ty) => {
576 let (item, func_idx) = match plugin_component
577 .metadata
578 .component
579 .export_index(Some(&instance_idx), export_name)
580 {
581 Some(res) => res,
582 None => {
583 trace!(
584 name = import_name,
585 fn_name = export_name,
586 "failed to get export index, skipping"
587 );
588 continue;
589 }
590 };
591 ensure!(
592 matches!(item, ComponentItem::ComponentFunc(..)),
593 "expected function export, found other"
594 );
595 trace!(
596 name = import_name,
597 fn_name = export_name,
598 "linking function import"
599 );
600 let import_name: Arc<str> = import_name.into();
601 let export_name: Arc<str> = export_name.into();
602 let pre = pre.clone();
603 let instance = instance.clone();
604 linker_instance
605 .func_new_async(
606 &export_name.clone(),
607 move |mut store, params, results| {
608 let import_name = import_name.clone();
611 let export_name = export_name.clone();
612 let pre = pre.clone();
613 let instance = instance.clone();
614 Box::new(async move {
615 let existing_instance = instance.read().await;
616 let store_id = store.data().id.clone();
617 let instance = if let Some((id, instance)) =
618 existing_instance.clone()
619 && id == store_id
620 {
621 drop(existing_instance);
622 instance
623 } else {
624 let new_instance =
626 pre.instantiate_async(&mut store).await?;
627 drop(existing_instance);
628 *instance.write().await =
629 Some((store_id, new_instance));
630 new_instance
631 };
632
633 let func = instance
634 .get_func(&mut store, func_idx)
635 .context("function not found")?;
636 trace!(
637 name = %import_name,
638 fn_name = %export_name,
639 ?params,
640 "lowering params"
641 );
642 let mut params_buf =
643 Vec::with_capacity(params.len());
644 for v in params {
645 params_buf
646 .push(lower(&mut store, v).context(
647 "failed to lower parameter",
648 )?);
649 }
650 trace!(
651 name = %import_name,
652 fn_name = %export_name,
653 ?params_buf,
654 "invoking dynamic export"
655 );
656
657 let mut results_buf =
658 vec![Val::Bool(false); results.len()];
659 func.call_async(
662 &mut store,
663 ¶ms_buf,
664 &mut results_buf,
665 )
666 .await
667 .context("failed to call function")?;
668
669 trace!(
670 name = %import_name,
671 fn_name = %export_name,
672 ?results_buf,
673 "lifting results"
674 );
675 for (i, v) in results_buf.into_iter().enumerate() {
676 results[i] = lift(&mut store, v)
677 .context("failed to lift result")?;
678 }
679 trace!(
680 name = %import_name,
681 fn_name = %export_name,
682 ?results,
683 "invoked dynamic export"
684 );
685
686 func.post_return_async(&mut store)
687 .await
688 .context("failed to execute post-return")?;
689 Ok(())
690 })
691 },
692 )
693 .expect("failed to create async func");
694 }
695 ComponentItem::Resource(resource_ty) => {
696 let (item, _idx) = match plugin_component
697 .metadata
698 .component
699 .export_index(Some(&instance_idx), export_name)
700 {
701 Some(res) => res,
702 None => {
703 trace!(
704 name = import_name,
705 resource = export_name,
706 "failed to get resource index, skipping"
707 );
708 continue;
709 }
710 };
711 let ComponentItem::Resource(_) = item else {
712 trace!(
713 name = import_name,
714 resource = export_name,
715 "expected resource export, found non-resource, skipping"
716 );
717 continue;
718 };
719
720 if export_name == "output-stream"
722 || export_name == "input-stream"
723 || export_name == "pollable"
724 || export_name == "tcp-socket"
725 || export_name == "incoming-value-async-body"
726 {
727 trace!(
728 name = import_name,
729 resource = export_name,
730 "skipping stream link as it is a host resource type"
731 );
732 continue;
733 }
734
735 trace!(name = import_name, resource = export_name, ty = ?resource_ty, "linking resource import");
736
737 linker_instance
738 .resource(export_name, ResourceType::host::<ResourceAny>(), |_, _| Ok(()))
739 .with_context(|| {
740 format!(
741 "failed to define resource import: {import_name}.{export_name}"
742 )
743 })
744 .unwrap_or_else(|e| {
745 trace!(name = import_name, resource = export_name, error = %e, "error defining resource import, skipping");
746 });
747 }
748 _ => {
749 trace!(
750 name = import_name,
751 fn_name = export_name,
752 "skipping non-function non-resource import"
753 );
754 continue;
755 }
756 }
757 }
758 }
759 ComponentItem::Resource(resource_ty) => {
760 trace!(
761 name = import_name,
762 ty = ?resource_ty,
763 "component import is a resource, which is not supported in this context. skipping."
764 );
765 }
766 _ => continue,
767 }
768 }
769
770 Ok(())
771 }
772
773 pub fn id(&self) -> &str {
775 &self.id
776 }
777
778 pub fn name(&self) -> &str {
780 &self.name
781 }
782
783 pub fn namespace(&self) -> &str {
785 &self.namespace
786 }
787
788 pub async fn component_count(&self) -> usize {
791 self.components.read().await.len()
792 }
793
794 pub async fn new_store(&self, component_id: &str) -> anyhow::Result<wasmtime::Store<Ctx>> {
795 let components = self.components.read().await;
796 let component = components
797 .get(component_id)
798 .context("component ID not found in workload")?;
799
800 let mut wasi_ctx_builder = WasiCtxBuilder::new();
802 wasi_ctx_builder
803 .envs(
804 component
805 .metadata
806 .local_resources
807 .environment
808 .iter()
809 .map(|kv| (kv.0.as_str(), kv.1.as_str()))
810 .collect::<Vec<_>>()
811 .as_slice(),
812 )
813 .inherit_stdout()
814 .inherit_stderr();
815
816 for (host_path, mount) in &components
818 .iter()
819 .flat_map(|(_id, workload_component)| workload_component.metadata.volume_mounts.clone())
820 .collect::<Vec<_>>()
821 {
822 let dir = tokio::fs::canonicalize(host_path).await?;
824 debug!(host_path = %dir.display(), container_path = %mount.mount_path, "preopening volume mount");
825 let (dir_perms, file_perms) = match mount.read_only {
826 true => (DirPerms::READ, FilePerms::READ),
827 false => (DirPerms::all(), FilePerms::all()),
828 };
829 wasi_ctx_builder.preopened_dir(&dir, &mount.mount_path, dir_perms, file_perms)?;
830 }
831
832 let mut ctx_builder =
833 Ctx::builder(component.metadata.workload_id(), component.metadata.id())
834 .with_wasi_ctx(wasi_ctx_builder.build());
835
836 if let Some(plugins) = &component.metadata.plugins {
837 ctx_builder = ctx_builder.with_plugins(plugins.clone());
838 }
839
840 let store = wasmtime::Store::new(component.metadata.engine(), ctx_builder.build());
841
842 Ok(store)
843 }
844
845 pub async fn new_service_store(&self) -> anyhow::Result<wasmtime::Store<Ctx>> {
847 let service = self
848 .service
849 .as_ref()
850 .context("no service defined for this workload")?;
851
852 let components = self.components.read().await;
853
854 let mut wasi_ctx_builder = WasiCtxBuilder::new();
856 wasi_ctx_builder
857 .envs(
858 service
859 .metadata
860 .local_resources
861 .environment
862 .iter()
863 .map(|kv| (kv.0.as_str(), kv.1.as_str()))
864 .collect::<Vec<_>>()
865 .as_slice(),
866 )
867 .inherit_stdout()
868 .inherit_stderr();
869
870 for (host_path, mount) in &components
872 .iter()
873 .flat_map(|(_id, workload_component)| workload_component.metadata.volume_mounts.clone())
874 .collect::<Vec<_>>()
875 {
876 let dir = tokio::fs::canonicalize(host_path).await?;
878 debug!(host_path = %dir.display(), container_path = %mount.mount_path, "preopening volume mount");
879 let (dir_perms, file_perms) = match mount.read_only {
880 true => (DirPerms::READ, FilePerms::READ),
881 false => (DirPerms::all(), FilePerms::all()),
882 };
883 wasi_ctx_builder.preopened_dir(&dir, &mount.mount_path, dir_perms, file_perms)?;
884 }
885
886 let mut ctx_builder = Ctx::builder(service.metadata.workload_id(), service.metadata.id())
887 .with_wasi_ctx(wasi_ctx_builder.build());
888
889 if let Some(plugins) = &service.metadata.plugins {
890 ctx_builder = ctx_builder.with_plugins(plugins.clone());
891 }
892
893 let store = wasmtime::Store::new(service.metadata.engine(), ctx_builder.build());
894
895 Ok(store)
896 }
897
898 pub async fn instantiate_pre(
899 &self,
900 component_id: &str,
901 ) -> anyhow::Result<wasmtime::component::InstancePre<Ctx>> {
902 let mut components = self.components.write().await;
903 let component = components
904 .get_mut(component_id)
905 .context("component ID not found in workload")?;
906 let wasmtime_component = component.metadata.component.clone();
907 let linker = component.metadata.linker();
908 let pre = linker.instantiate_pre(&wasmtime_component)?;
909
910 Ok(pre)
911 }
912
913 pub async fn unbind_all_plugins(&self) -> anyhow::Result<()> {
919 trace!(
920 workload_id = self.id.as_ref(),
921 workload_name = self.name.as_ref(),
922 "unbinding all plugins from workload"
923 );
924
925 for component in self.components.read().await.values() {
926 if let Some(plugins) = component.plugins() {
927 for (plugin_id, plugin) in plugins.iter() {
928 trace!(
929 plugin_id,
930 component_id = component.id(),
931 workload_id = self.id.as_ref(),
932 "unbinding plugin from component"
933 );
934
935 let world = component.world();
937 let plugin_world = plugin.world();
938
939 let bound_interfaces = world
941 .imports
942 .iter()
943 .filter(|import| plugin_world.imports.contains(import))
944 .cloned()
945 .collect::<std::collections::HashSet<_>>();
946
947 if let Err(e) = plugin.on_workload_unbind(self, bound_interfaces).await {
948 warn!(
949 plugin_id,
950 component_id = component.id(),
951 workload_id = self.id.as_ref(),
952 error = ?e,
953 "failed to unbind plugin from workload, continuing cleanup"
954 );
955 }
956 }
957 }
958 }
959
960 Ok(())
961 }
962}
963
964pub struct UnresolvedWorkload {
986 id: Arc<str>,
988 name: Arc<str>,
990 namespace: Arc<str>,
992 host_interfaces: Vec<WitInterface>,
994 service: Option<WorkloadService>,
996 components: HashMap<Arc<str>, WorkloadComponent>,
998}
999
1000impl UnresolvedWorkload {
1001 pub fn new(
1015 id: impl Into<Arc<str>>,
1016 name: impl Into<Arc<str>>,
1017 namespace: impl Into<Arc<str>>,
1018 service: Option<WorkloadService>,
1019 components: impl IntoIterator<Item = WorkloadComponent>,
1020 host_interfaces: Vec<WitInterface>,
1021 ) -> Self {
1022 Self {
1023 id: id.into(),
1024 name: name.into(),
1025 namespace: namespace.into(),
1026 service,
1027 components: components
1028 .into_iter()
1029 .map(|c| {
1030 let id = Arc::from(c.id());
1031 (id, c)
1032 })
1033 .collect(),
1034 host_interfaces,
1035 }
1036 }
1037
1038 pub async fn bind_plugins(
1041 &mut self,
1042 plugins: &HashMap<&'static str, Arc<dyn HostPlugin + 'static>>,
1043 ) -> anyhow::Result<Vec<(Arc<dyn HostPlugin + 'static>, Vec<String>)>> {
1044 let mut bound_plugins: Vec<(Arc<dyn HostPlugin + 'static>, Vec<String>)> = Vec::new();
1045
1046 let mut unmatched_interfaces: HashMap<Arc<str>, HashSet<WitInterface>> = HashMap::new();
1049 trace!(host_interfaces = ?self.host_interfaces, "determining missing guest interfaces");
1050
1051 if let Some(service) = self.service.as_ref() {
1052 let world = service.world();
1053 trace!(?world, "comparing service world to host interfaces");
1054 let required_interfaces: HashSet<WitInterface> = self
1055 .host_interfaces
1056 .iter()
1057 .filter(|wit_interface| world.includes_bidirectional(wit_interface))
1059 .cloned()
1060 .collect();
1061
1062 if !required_interfaces.is_empty() {
1063 unmatched_interfaces.insert(Arc::from(service.id()), required_interfaces);
1064 }
1065 }
1066
1067 for (id, workload_component) in &self.components {
1068 let world = workload_component.world();
1069 trace!(?world, "comparing component world to host interfaces");
1070 let required_interfaces: HashSet<WitInterface> = self
1071 .host_interfaces
1072 .iter()
1073 .filter(|wit_interface| world.includes_bidirectional(wit_interface))
1074 .cloned()
1075 .collect();
1076
1077 if !required_interfaces.is_empty() {
1078 unmatched_interfaces.insert(id.clone(), required_interfaces);
1079 }
1080 }
1081
1082 trace!(?unmatched_interfaces, "resolving unmatched interfaces");
1083
1084 for (plugin_id, p) in plugins.iter() {
1086 let plugin_interfaces = p.world();
1087 trace!(plugin_id = plugin_id, plugin_interfaces = ?plugin_interfaces, "checking plugin interfaces");
1088
1089 let mut plugin_component_bindings = Vec::new();
1091
1092 for (component_id, required_interfaces) in unmatched_interfaces.iter() {
1094 let mut matching_interfaces = HashSet::new();
1096 for wit_interface in required_interfaces.iter() {
1097 if plugin_interfaces.includes_bidirectional(wit_interface) {
1099 matching_interfaces.insert(wit_interface.clone());
1100 }
1101 }
1102
1103 if !matching_interfaces.is_empty() {
1104 plugin_component_bindings.push((component_id.clone(), matching_interfaces));
1105 }
1106 }
1107
1108 if !plugin_component_bindings.is_empty() {
1110 let plugin_matched_interfaces: HashSet<WitInterface> = plugin_component_bindings
1112 .iter()
1113 .flat_map(|(_, interfaces)| interfaces.clone())
1114 .collect();
1115 debug!(
1116 plugin_id = plugin_id,
1117 interfaces = ?plugin_matched_interfaces,
1118 "binding plugin to workload"
1119 );
1120
1121 if let Err(e) = p.on_workload_bind(self, plugin_matched_interfaces).await {
1123 tracing::error!(
1124 plugin_id = plugin_id,
1125 err = ?e,
1126 "failed to bind plugin to workload"
1127 );
1128 bail!(e)
1129 }
1130
1131 let mut plugin_component_ids = Vec::new();
1133
1134 for (component_id, matching_interfaces) in plugin_component_bindings {
1136 let workload_component = self
1138 .components
1139 .get_mut(&component_id)
1140 .context("component not found during plugin binding")?;
1141
1142 debug!(
1143 plugin_id = plugin_id,
1144 component_id = workload_component.id(),
1145 interfaces = ?matching_interfaces,
1146 "binding plugin to workload component"
1147 );
1148
1149 if let Err(e) = p
1150 .on_component_bind(workload_component, matching_interfaces.clone())
1151 .await
1152 {
1153 tracing::error!(
1154 plugin_id = plugin_id,
1155 component_id = workload_component.id(),
1156 err = ?e,
1157 "failed to bind workload component to plugin"
1158 );
1159 bail!(e)
1160 } else {
1161 trace!(
1162 plugin_id = plugin_id,
1163 component_id = workload_component.id(),
1164 "successfully bound plugin to component"
1165 );
1166 workload_component.add_plugin(plugin_id, p.clone());
1167 plugin_component_ids.push(workload_component.id().to_string());
1168
1169 if let Some(unmatched) = unmatched_interfaces.get_mut(&component_id) {
1171 for interface in &matching_interfaces {
1172 unmatched.remove(interface);
1173 }
1174 }
1175 }
1176 }
1177
1178 bound_plugins.push((p.clone(), plugin_component_ids));
1180 }
1181 }
1182
1183 for (component_id, unmatched) in unmatched_interfaces.iter() {
1185 if !unmatched.is_empty() {
1186 tracing::error!(
1187 component_id = component_id.as_ref(),
1188 interfaces = ?unmatched,
1189 "no plugins found for requested interfaces"
1190 );
1191 bail!(
1192 "workload component {component_id} requested interfaces that are not available on this host: {unmatched:?}",
1193 )
1194 }
1195 }
1196
1197 Ok(bound_plugins)
1198 }
1199
1200 pub async fn resolve(
1224 mut self,
1225 plugins: Option<&HashMap<&'static str, Arc<dyn HostPlugin + 'static>>>,
1226 ) -> anyhow::Result<ResolvedWorkload> {
1227 let bound_plugins = if let Some(plugins) = plugins {
1229 trace!("binding plugins to workload");
1230 self.bind_plugins(plugins).await?
1231 } else {
1232 Vec::new()
1233 };
1234
1235 let mut resolved_workload = ResolvedWorkload {
1237 id: self.id.clone(),
1238 name: self.name.clone(),
1239 namespace: self.namespace.clone(),
1240 components: Arc::new(RwLock::new(self.components)),
1241 service: self.service,
1242 };
1243
1244 if let Err(e) = resolved_workload.link_components().await {
1246 warn!(
1248 error = ?e,
1249 "failed to link components, unbinding all plugins"
1250 );
1251 let _ = resolved_workload.unbind_all_plugins().await;
1252 bail!(e);
1253 }
1254
1255 for (plugin, component_ids) in bound_plugins.iter() {
1257 trace!(
1258 plugin_id = plugin.id(),
1259 component_count = component_ids.len(),
1260 "notifying plugin of resolved workload"
1261 );
1262 for component_id in component_ids {
1264 if let Err(e) = plugin
1265 .on_workload_resolved(&resolved_workload, component_id.as_str())
1266 .await
1267 {
1268 warn!(
1270 plugin_id = plugin.id(),
1271 component_id,
1272 error = ?e,
1273 "failed to notify plugin of resolved workload, unbinding all plugins"
1274 );
1275 let _ = resolved_workload.unbind_all_plugins().await;
1276 bail!(e);
1277 }
1278 }
1279 }
1280
1281 Ok(resolved_workload)
1282 }
1283
1284 pub fn id(&self) -> &str {
1286 &self.id
1287 }
1288
1289 pub fn name(&self) -> &str {
1291 &self.name
1292 }
1293
1294 pub fn namespace(&self) -> &str {
1296 &self.namespace
1297 }
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302 use super::*;
1303 use crate::plugin::HostPlugin;
1304 use crate::wit::{WitInterface, WitWorld};
1305 use async_trait::async_trait;
1306 use std::collections::{HashMap, HashSet};
1307 use std::sync::atomic::{AtomicUsize, Ordering};
1308 use std::sync::{Arc, Mutex};
1309 use wasmtime::component::{Component, Linker};
1310
1311 #[derive(Debug, Clone)]
1313 struct CallRecord {
1314 #[allow(unused)]
1315 plugin_id: String,
1316 method: String,
1317 component_id: Option<String>,
1318 #[allow(unused)]
1319 interfaces: Vec<String>,
1320 }
1321
1322 struct MockPlugin {
1325 #[allow(unused)]
1326 id: String,
1327 world: WitWorld,
1328 call_records: Arc<Mutex<Vec<CallRecord>>>,
1329 on_workload_bind_count: Arc<AtomicUsize>,
1330 on_component_bind_count: Arc<AtomicUsize>,
1331 on_workload_resolved_count: Arc<AtomicUsize>,
1332 }
1333
1334 impl MockPlugin {
1335 fn new(
1337 id: impl Into<String>,
1338 imports: Vec<WitInterface>,
1339 exports: Vec<WitInterface>,
1340 ) -> Self {
1341 Self {
1342 id: id.into(),
1343 world: WitWorld {
1344 imports: imports.into_iter().collect(),
1345 exports: exports.into_iter().collect(),
1346 },
1347 call_records: Arc::new(Mutex::new(Vec::new())),
1348 on_workload_bind_count: Arc::new(AtomicUsize::new(0)),
1349 on_component_bind_count: Arc::new(AtomicUsize::new(0)),
1350 on_workload_resolved_count: Arc::new(AtomicUsize::new(0)),
1351 }
1352 }
1353
1354 fn get_call_count(&self, method: &str) -> usize {
1356 match method {
1357 "on_workload_bind" => self.on_workload_bind_count.load(Ordering::SeqCst),
1358 "on_component_bind" => self.on_component_bind_count.load(Ordering::SeqCst),
1359 "on_workload_resolved" => self.on_workload_resolved_count.load(Ordering::SeqCst),
1360 _ => 0,
1361 }
1362 }
1363
1364 fn get_call_records(&self) -> Vec<CallRecord> {
1366 self.call_records.lock().unwrap().clone()
1367 }
1368 }
1369
1370 const ID: &str = "mock-plugin";
1371
1372 #[async_trait]
1373 impl HostPlugin for MockPlugin {
1374 fn id(&self) -> &'static str {
1375 ID
1376 }
1377
1378 fn world(&self) -> WitWorld {
1379 self.world.clone()
1380 }
1381
1382 async fn on_workload_bind(
1383 &self,
1384 _workload: &UnresolvedWorkload,
1385 interfaces: HashSet<WitInterface>,
1386 ) -> anyhow::Result<()> {
1387 self.on_workload_bind_count.fetch_add(1, Ordering::SeqCst);
1388 self.call_records.lock().unwrap().push(CallRecord {
1389 plugin_id: ID.to_string(),
1390 method: "on_workload_bind".to_string(),
1391 component_id: None,
1392 interfaces: interfaces.iter().map(|i| i.to_string()).collect(),
1393 });
1394 Ok(())
1395 }
1396
1397 async fn on_component_bind(
1398 &self,
1399 component: &mut WorkloadComponent,
1400 interfaces: HashSet<WitInterface>,
1401 ) -> anyhow::Result<()> {
1402 self.on_component_bind_count.fetch_add(1, Ordering::SeqCst);
1403 self.call_records.lock().unwrap().push(CallRecord {
1404 plugin_id: ID.to_string(),
1405 method: "on_component_bind".to_string(),
1406 component_id: Some(component.id().to_string()),
1407 interfaces: interfaces.iter().map(|i| i.to_string()).collect(),
1408 });
1409 Ok(())
1410 }
1411
1412 async fn on_workload_resolved(
1413 &self,
1414 _workload: &ResolvedWorkload,
1415 component_id: &str,
1416 ) -> anyhow::Result<()> {
1417 self.on_workload_resolved_count
1418 .fetch_add(1, Ordering::SeqCst);
1419 self.call_records.lock().unwrap().push(CallRecord {
1420 plugin_id: ID.to_string(),
1421 method: "on_workload_resolved".to_string(),
1422 component_id: Some(component_id.to_string()),
1423 interfaces: Vec::new(),
1424 });
1425 Ok(())
1426 }
1427 }
1428
1429 const HTTP_COUNTER_WASM: &[u8] = include_bytes!("../../tests/fixtures/http_counter.wasm");
1431
1432 fn create_test_component(id: &str) -> WorkloadComponent {
1435 let engine = wasmtime::Engine::default();
1436 let linker = Linker::new(&engine);
1437
1438 let component = Component::new(&engine, HTTP_COUNTER_WASM).unwrap();
1440
1441 let local_resources = LocalResources::default();
1442
1443 WorkloadComponent::new(
1444 format!("workload-{id}"),
1445 format!("test-workload-{id}"),
1446 "test-namespace".to_string(),
1447 component,
1448 linker,
1449 Vec::new(),
1450 local_resources,
1451 )
1452 }
1453
1454 #[tokio::test]
1457 async fn test_single_plugin_single_component() {
1458 let http_interface = WitInterface {
1460 namespace: "wasi".to_string(),
1461 package: "http".to_string(),
1462 interfaces: ["incoming-handler".to_string()].into_iter().collect(),
1463 version: Some(semver::Version::parse("0.2.2").unwrap()),
1464 config: std::collections::HashMap::new(),
1465 };
1466
1467 let plugin = Arc::new(MockPlugin::new(
1468 "http-plugin",
1469 vec![],
1470 vec![http_interface.clone()],
1471 ));
1472
1473 let mut plugins = HashMap::new();
1474 plugins.insert(plugin.id(), plugin.clone() as Arc<dyn HostPlugin>);
1475
1476 let components = vec![create_test_component("component1")];
1478
1479 let mut workload = UnresolvedWorkload::new(
1480 "test-workload-id".to_string(),
1481 "test-workload".to_string(),
1482 "test-namespace".to_string(),
1483 None,
1484 components,
1485 vec![http_interface.clone()],
1486 );
1487
1488 let bound_plugins = workload.bind_plugins(&plugins).await.unwrap();
1489
1490 assert_eq!(plugin.get_call_count("on_workload_bind"), 1);
1492
1493 assert_eq!(plugin.get_call_count("on_component_bind"), 1);
1495
1496 assert_eq!(bound_plugins.len(), 1);
1498 let (_bound_plugin, component_ids) = &bound_plugins[0];
1499 assert_eq!(component_ids.len(), 1);
1500
1501 let records = plugin.get_call_records();
1503 assert_eq!(records.len(), 2);
1504 assert_eq!(records[0].method, "on_workload_bind");
1505 assert_eq!(records[1].method, "on_component_bind");
1506 assert_eq!(records[1].component_id.as_ref().unwrap(), &component_ids[0]);
1507 }
1508
1509 #[tokio::test]
1512 async fn test_multiple_plugins_multiple_components() {
1513 let http_interface = WitInterface::from("wasi:http/incoming-handler@0.2.0");
1514 let blobstore_interface = WitInterface::from("wasi:blobstore/blobstore@0.2.0");
1515 let keyvalue_interface = WitInterface::from("wasi:keyvalue/store@0.2.0");
1516
1517 let http_plugin = Arc::new(MockPlugin::new(
1518 "http-plugin",
1519 vec![],
1520 vec![http_interface.clone()],
1521 ));
1522
1523 let storage_plugin = Arc::new(MockPlugin::new(
1524 "storage-plugin",
1525 vec![],
1526 vec![blobstore_interface.clone(), keyvalue_interface.clone()],
1527 ));
1528
1529 let mut plugins = HashMap::new();
1530 plugins.insert(http_plugin.id(), http_plugin.clone() as Arc<dyn HostPlugin>);
1531 plugins.insert(
1532 storage_plugin.id(),
1533 storage_plugin.clone() as Arc<dyn HostPlugin>,
1534 );
1535
1536 let components = vec![
1538 create_test_component("component1"),
1539 create_test_component("component2"),
1540 create_test_component("component3"),
1541 ];
1542
1543 let mut workload = UnresolvedWorkload::new(
1544 "test-workload-id".to_string(),
1545 "test-workload".to_string(),
1546 "test-namespace".to_string(),
1547 None,
1548 components,
1549 vec![
1550 http_interface.clone(),
1551 blobstore_interface.clone(),
1552 keyvalue_interface.clone(),
1553 ],
1554 );
1555
1556 let _bound_plugins = workload.bind_plugins(&plugins).await.unwrap();
1559
1560 for (plugin, _component_ids) in &_bound_plugins {
1562 if plugin.id() == "http-plugin" {
1564 assert_eq!(http_plugin.get_call_count("on_workload_bind"), 1);
1565 } else if plugin.id() == "storage-plugin" {
1566 assert_eq!(storage_plugin.get_call_count("on_workload_bind"), 1);
1567 }
1568 }
1569 }
1570
1571 #[tokio::test]
1574 async fn test_no_duplicate_bindings() {
1575 let http_interface = WitInterface::from("wasi:http/incoming-handler@0.2.0");
1576
1577 let plugin1 = Arc::new(MockPlugin::new(
1579 "http-plugin-1",
1580 vec![],
1581 vec![http_interface.clone()],
1582 ));
1583
1584 let plugin2 = Arc::new(MockPlugin::new(
1585 "http-plugin-2",
1586 vec![],
1587 vec![http_interface.clone()],
1588 ));
1589
1590 let mut plugins = HashMap::new();
1591 plugins.insert(plugin1.id(), plugin1.clone() as Arc<dyn HostPlugin>);
1592 plugins.insert(plugin2.id(), plugin2.clone() as Arc<dyn HostPlugin>);
1593
1594 let components = vec![create_test_component("component1")];
1595
1596 let mut workload = UnresolvedWorkload::new(
1597 "test-workload-id".to_string(),
1598 "test-workload".to_string(),
1599 "test-namespace".to_string(),
1600 None,
1601 components,
1602 vec![http_interface.clone()],
1603 );
1604
1605 let _bound_plugins = workload.bind_plugins(&plugins).await.unwrap();
1606
1607 let total_workload_binds =
1610 plugin1.get_call_count("on_workload_bind") + plugin2.get_call_count("on_workload_bind");
1611
1612 assert!(
1614 total_workload_binds <= 1,
1615 "Only one plugin should bind for a given interface"
1616 );
1617 }
1618
1619 #[tokio::test]
1622 async fn test_missing_interface_fails() {
1623 let http_interface = WitInterface::from("wasi:http/incoming-handler@0.2.0");
1624 let blobstore_interface = WitInterface::from("wasi:blobstore/blobstore@0.2.0");
1625
1626 let plugin = Arc::new(MockPlugin::new(
1628 "http-plugin",
1629 vec![],
1630 vec![http_interface.clone()],
1631 ));
1632
1633 let mut plugins = HashMap::new();
1634 plugins.insert(plugin.id(), plugin.clone() as Arc<dyn HostPlugin>);
1635
1636 let components = vec![create_test_component("component1")];
1638
1639 let mut workload = UnresolvedWorkload::new(
1642 "test-workload-id".to_string(),
1643 "test-workload".to_string(),
1644 "test-namespace".to_string(),
1645 None,
1646 components,
1647 vec![http_interface.clone(), blobstore_interface.clone()],
1648 );
1649
1650 let _result = workload.bind_plugins(&plugins).await;
1653
1654 }
1657
1658 #[tokio::test]
1661 async fn test_plugin_callback_order() {
1662 let interface1 = WitInterface::from("test:interface/handler@0.1.0");
1663
1664 let plugin = Arc::new(MockPlugin::new(
1665 "test-plugin",
1666 vec![],
1667 vec![interface1.clone()],
1668 ));
1669
1670 let mut plugins = HashMap::new();
1671 plugins.insert(plugin.id(), plugin.clone() as Arc<dyn HostPlugin>);
1672
1673 let components = vec![
1674 create_test_component("comp1"),
1675 create_test_component("comp2"),
1676 ];
1677
1678 let mut workload = UnresolvedWorkload::new(
1679 "test-workload-id".to_string(),
1680 "test-workload".to_string(),
1681 "test-namespace".to_string(),
1682 None,
1683 components,
1684 vec![interface1.clone()],
1685 );
1686
1687 let _bound_plugins = workload.bind_plugins(&plugins).await.unwrap();
1688
1689 let records = plugin.get_call_records();
1691
1692 if !records.is_empty() {
1694 assert_eq!(
1695 records[0].method, "on_workload_bind",
1696 "on_workload_bind should be called before component bindings"
1697 );
1698
1699 for record in records.iter().skip(1) {
1701 assert_eq!(
1702 record.method, "on_component_bind",
1703 "All calls after on_workload_bind should be on_component_bind"
1704 );
1705 }
1706 }
1707 }
1708
1709 #[tokio::test]
1710 async fn test_world_includes_bidirectional() {
1711 let world = WitWorld {
1712 imports: HashSet::from([WitInterface::from("wasmcloud:messaging/handler@0.1.0")]),
1713 exports: HashSet::from([WitInterface::from(
1714 "wasmcloud:messaging/consumer,types@0.1.0",
1715 )]),
1716 };
1717
1718 let interface1 = WitInterface::from("wasmcloud:messaging/handler@0.1.0");
1719 let interface2 = WitInterface::from("wasmcloud:messaging/consumer,types@0.1.0");
1720 let interface3 = WitInterface::from("wasmcloud:messaging/handler,consumer,types@0.1.0");
1721 let interface4 = WitInterface::from("wasmcloud:messaging/producer@0.1.0");
1722
1723 assert!(world.includes_bidirectional(&interface1));
1724 assert!(world.includes_bidirectional(&interface2));
1725 assert!(world.includes_bidirectional(&interface3));
1726 assert!(!world.includes_bidirectional(&interface4));
1727 assert!(!world.includes(&interface3));
1729 }
1730}