Skip to content

Commit 8feaa12

Browse files
committed
feat: add component naming and workload update lifecycle management
Signed-off-by: Aditya <aditya.salunkh919@gmail.com>
1 parent 4df9bbc commit 8feaa12

18 files changed

+1523
-87
lines changed

crates/wash-runtime/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ async fn main() -> anyhow::Result<()> {
6565
host_interfaces: vec![],
6666
volumes: vec![],
6767
},
68+
component_ids: None,
6869
};
6970

7071
host.workload_start(req).await?;

crates/wash-runtime/src/engine/mod.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,72 @@ impl Engine {
242242
))
243243
}
244244

245+
/// Initialize specific components from a workload spec by their indices.
246+
/// This is used for component-specific restart operations.
247+
///
248+
/// # Arguments
249+
/// * `workload_id` - The workload identifier
250+
/// * `workload` - The full workload specification
251+
/// * `component_indices` - Indices of components to initialize from the workload spec
252+
///
253+
/// # Returns
254+
/// A vector of initialized WorkloadComponents corresponding to the specified indices
255+
pub fn initialize_specific_components(
256+
&self,
257+
workload_id: impl AsRef<str>,
258+
workload: &Workload,
259+
component_indices: &[usize],
260+
) -> anyhow::Result<Vec<WorkloadComponent>> {
261+
// Process and validate volumes first
262+
let mut validated_volumes = std::collections::HashMap::new();
263+
264+
for v in &workload.volumes {
265+
let host_path = match &v.volume_type {
266+
VolumeType::HostPath(HostPathVolume { local_path }) => {
267+
let path = PathBuf::from(local_path);
268+
if !path.is_dir() {
269+
anyhow::bail!(
270+
"HostPath volume '{local_path}' does not exist or is not a directory",
271+
);
272+
}
273+
path
274+
}
275+
VolumeType::EmptyDir(EmptyDirVolume {}) => {
276+
let temp_dir = tempfile::tempdir()
277+
.context("failed to create temp dir for empty dir volume")?;
278+
tracing::debug!(path = ?temp_dir.path(), "created temp dir for empty dir volume");
279+
temp_dir.keep()
280+
}
281+
};
282+
283+
validated_volumes.insert(v.name.clone(), host_path);
284+
}
285+
286+
// Initialize only the specified components
287+
let mut initialized_components = Vec::new();
288+
for &idx in component_indices {
289+
if idx >= workload.components.len() {
290+
bail!(
291+
"component index {idx} out of bounds (workload has {} components)",
292+
workload.components.len()
293+
);
294+
}
295+
296+
let component = &workload.components[idx];
297+
let workload_component = self.initialize_workload_component(
298+
workload_id.as_ref(),
299+
&workload.name,
300+
&workload.namespace,
301+
component.clone(),
302+
&validated_volumes,
303+
)?;
304+
305+
initialized_components.push(workload_component);
306+
}
307+
308+
Ok(initialized_components)
309+
}
310+
245311
/// Initialize a component that is a part of a workload, add wasi@0.2 interfaces (and
246312
/// wasi:http if the `http` feature is enabled) to the linker.
247313
fn initialize_workload_component(
@@ -287,6 +353,7 @@ impl Engine {
287353
workload_id.as_ref(),
288354
workload_name.as_ref(),
289355
workload_namespace.as_ref(),
356+
component.name.clone(),
290357
wasmtime_component,
291358
linker,
292359
component_volume_mounts,

crates/wash-runtime/src/engine/workload.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ pub struct WorkloadMetadata {
4545
workload_name: Arc<str>,
4646
/// The namespace of the workload this component belongs to
4747
workload_namespace: Arc<str>,
48+
/// Optional user-provided name for this component (from spec)
49+
component_name: Option<String>,
4850
/// The actual wasmtime [`Component`] that can be instantiated
4951
component: Component,
5052
/// The wasmtime [`Linker`] used to instantiate the component
@@ -78,6 +80,11 @@ impl WorkloadMetadata {
7880
&self.workload_namespace
7981
}
8082

83+
/// Returns the optional user-provided name for this component.
84+
pub fn component_name(&self) -> Option<&str> {
85+
self.component_name.as_deref()
86+
}
87+
8188
/// Returns a reference to the wasmtime engine used to compile this component.
8289
pub fn engine(&self) -> &wasmtime::Engine {
8390
self.component.engine()
@@ -235,6 +242,7 @@ impl WorkloadService {
235242
workload_id: workload_id.into(),
236243
workload_name: workload_name.into(),
237244
workload_namespace: workload_namespace.into(),
245+
component_name: None, // Services don't have names yet
238246
component,
239247
linker,
240248
volume_mounts,
@@ -285,6 +293,7 @@ impl WorkloadComponent {
285293
workload_id: impl Into<Arc<str>>,
286294
workload_name: impl Into<Arc<str>>,
287295
workload_namespace: impl Into<Arc<str>>,
296+
component_name: Option<String>,
288297
component: Component,
289298
linker: Linker<Ctx>,
290299
volume_mounts: Vec<(PathBuf, VolumeMount)>,
@@ -296,6 +305,7 @@ impl WorkloadComponent {
296305
workload_id: workload_id.into(),
297306
workload_name: workload_name.into(),
298307
workload_namespace: workload_namespace.into(),
308+
component_name,
299309
component,
300310
linker,
301311
volume_mounts,
@@ -480,6 +490,122 @@ impl ResolvedWorkload {
480490
&self.host_interfaces
481491
}
482492

493+
/// Bind a single component to plugins and add/replace it in the resolved workload.
494+
/// This is used for component-specific restart operations.
495+
///
496+
/// # Arguments
497+
/// * `component` - The initialized WorkloadComponent to bind and add
498+
/// * `target_component_id` - The component ID to use (for replacing existing components)
499+
/// * `plugins` - Available host plugins for binding
500+
///
501+
/// # Returns
502+
/// The component ID that was bound
503+
pub async fn bind_and_add_component(
504+
&self,
505+
mut component: WorkloadComponent,
506+
target_component_id: String,
507+
plugins: &HashMap<&'static str, Arc<dyn HostPlugin + 'static>>,
508+
host_interfaces: &[WitInterface],
509+
) -> anyhow::Result<String> {
510+
// Override the component ID to match the target (for replacement)
511+
component.metadata.id = Arc::from(target_component_id.as_str());
512+
513+
// Override the component ID to match the target (for replacement)
514+
component.metadata.id = Arc::from(target_component_id.as_str());
515+
516+
// Determine which interfaces this component needs
517+
let world = component.world();
518+
let http_iface = WitInterface::from("wasi:http/incoming-handler,outgoing-handler");
519+
let required_interfaces: HashSet<WitInterface> = host_interfaces
520+
.iter()
521+
.filter(|wit_interface| !http_iface.contains(wit_interface))
522+
.filter(|wit_interface| world.includes_bidirectional(wit_interface))
523+
.cloned()
524+
.collect();
525+
526+
if required_interfaces.is_empty() {
527+
// No plugins needed, just add the component
528+
// Set component state to Running first
529+
component
530+
.set_state(crate::types::ComponentState::Running)
531+
.await;
532+
533+
self.components
534+
.write()
535+
.await
536+
.insert(Arc::from(target_component_id.as_str()), component);
537+
return Ok(target_component_id);
538+
}
539+
540+
// Bind to matching plugins
541+
for (_plugin_id, plugin) in plugins.iter() {
542+
let plugin_world = plugin.world();
543+
let plugin_matched_interfaces: HashSet<WitInterface> = required_interfaces
544+
.iter()
545+
.filter(|interface| plugin_world.includes_bidirectional(interface))
546+
.cloned()
547+
.collect();
548+
549+
if plugin_matched_interfaces.is_empty() {
550+
continue;
551+
}
552+
553+
// Note: We skip on_workload_bind for individual component restarts
554+
// as the workload is already bound. We only need on_component_bind.
555+
556+
// Bind plugin to component
557+
let matching_interfaces: HashSet<WitInterface> = plugin_matched_interfaces
558+
.iter()
559+
.filter(|interface| world.includes_bidirectional(interface))
560+
.cloned()
561+
.collect();
562+
563+
if !matching_interfaces.is_empty() {
564+
if let Err(e) = plugin
565+
.on_component_bind(&mut component, matching_interfaces.clone())
566+
.await
567+
{
568+
warn!(
569+
plugin_id = plugin.id(),
570+
component_id = target_component_id,
571+
error = ?e,
572+
"failed to bind component to plugin during component restart"
573+
);
574+
bail!(e);
575+
}
576+
577+
component.add_plugin(plugin.id(), plugin.clone());
578+
}
579+
580+
// Notify plugin of resolution
581+
if let Err(e) = plugin
582+
.on_workload_resolved(self, &target_component_id)
583+
.await
584+
{
585+
warn!(
586+
plugin_id = plugin.id(),
587+
component_id = target_component_id,
588+
error = ?e,
589+
"failed to notify plugin of resolved workload during component restart"
590+
);
591+
bail!(e);
592+
}
593+
}
594+
595+
// Set component state to Running
596+
component
597+
.set_state(crate::types::ComponentState::Running)
598+
.await;
599+
600+
// Add/replace the component in the workload
601+
self.components
602+
.write()
603+
.await
604+
.insert(Arc::from(target_component_id.as_str()), component);
605+
606+
Ok(target_component_id)
607+
}
608+
483609
async fn link_components(&mut self) -> anyhow::Result<()> {
484610
// A map from component ID to its exported interfaces
485611
let mut interface_map: HashMap<String, Arc<str>> = HashMap::new();
@@ -1697,6 +1823,7 @@ mod tests {
16971823
format!("workload-{id}"),
16981824
format!("test-workload-{id}"),
16991825
"test-namespace".to_string(),
1826+
None, // No component name for test components
17001827
component,
17011828
linker,
17021829
Vec::new(),

0 commit comments

Comments
 (0)