use anyhow::{Context, bail};
use wasmtime::PoolingAllocationConfig;
use wasmtime::component::{Component, Linker};
use crate::engine::ctx::Ctx;
use crate::engine::workload::{UnresolvedWorkload, WorkloadComponent, WorkloadService};
use crate::types::{EmptyDirVolume, HostPathVolume, VolumeType, Workload};
use std::path::PathBuf;
pub mod ctx;
mod value;
pub mod workload;
#[derive(Debug, Clone)]
pub struct Engine {
pub(crate) inner: wasmtime::Engine,
}
impl Engine {
pub fn builder() -> EngineBuilder {
EngineBuilder::default()
}
pub fn inner(&self) -> &wasmtime::Engine {
&self.inner
}
pub fn initialize_workload(
&self,
id: impl AsRef<str>,
workload: Workload,
) -> anyhow::Result<UnresolvedWorkload> {
let Workload {
namespace,
name,
components,
service,
volumes,
host_interfaces,
..
} = workload;
let mut validated_volumes = std::collections::HashMap::new();
for v in volumes {
let host_path = match v.volume_type {
VolumeType::HostPath(HostPathVolume { local_path }) => {
let path = PathBuf::from(&local_path);
if !path.is_dir() {
anyhow::bail!(
"HostPath volume '{local_path}' does not exist or is not a directory",
);
}
path
}
VolumeType::EmptyDir(EmptyDirVolume {}) => {
let temp_dir = tempfile::tempdir()
.context("failed to create temp dir for empty dir volume")?;
tracing::debug!(path = ?temp_dir.path(), "created temp dir for empty dir volume");
temp_dir.keep()
}
};
validated_volumes.insert(v.name.clone(), host_path);
}
let service = if let Some(svc) = service {
match self.initialize_service(id.as_ref(), &name, &namespace, svc, &validated_volumes) {
Ok(handle) => {
tracing::debug!("successfully initialized service component");
Some(handle)
}
Err(e) => {
tracing::error!(err = ?e, "failed to initialize service component");
bail!(e);
}
}
} else {
None
};
let mut workload_components = Vec::new();
for component in components.into_iter() {
match self.initialize_workload_component(
id.as_ref(),
&name,
&namespace,
component,
&validated_volumes,
) {
Ok(handle) => {
tracing::debug!("successfully initialized workload component");
workload_components.push(handle);
}
Err(e) => {
tracing::error!(err = ?e, "failed to initialize component");
bail!(e);
}
}
}
Ok(UnresolvedWorkload::new(
id.as_ref(),
name,
namespace,
service,
workload_components,
host_interfaces,
))
}
fn initialize_service(
&self,
workload_id: impl AsRef<str>,
workload_name: impl AsRef<str>,
workload_namespace: impl AsRef<str>,
service: crate::types::Service,
validated_volumes: &std::collections::HashMap<String, PathBuf>,
) -> anyhow::Result<WorkloadService> {
let wasmtime_component = Component::new(&self.inner, service.bytes)
.context("failed to create component from bytes")?;
let mut linker: Linker<Ctx> = Linker::new(&self.inner);
wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to add WASI to linker")?;
#[cfg(feature = "wasi-http")]
wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
.context("failed to add wasi:http/types to linker")?;
let mut component_volume_mounts = Vec::new();
for vm in &service.local_resources.volume_mounts {
if let Some(host_path) = validated_volumes.get(&vm.name) {
component_volume_mounts.push((host_path.clone(), vm.clone()));
} else {
tracing::warn!(
volume = %vm.name,
"component references volume that was not found in workload volumes",
);
}
}
Ok(WorkloadService::new(
workload_id.as_ref(),
workload_name.as_ref(),
workload_namespace.as_ref(),
wasmtime_component,
linker,
component_volume_mounts,
service.local_resources,
service.max_restarts,
))
}
fn initialize_workload_component(
&self,
workload_id: impl AsRef<str>,
workload_name: impl AsRef<str>,
workload_namespace: impl AsRef<str>,
component: crate::types::Component,
validated_volumes: &std::collections::HashMap<String, PathBuf>,
) -> anyhow::Result<WorkloadComponent> {
let wasmtime_component = Component::new(&self.inner, component.bytes)
.context("failed to create component from bytes")?;
let mut linker: Linker<Ctx> = Linker::new(&self.inner);
wasmtime_wasi::add_to_linker_async(&mut linker).context("failed to add WASI to linker")?;
#[cfg(feature = "wasi-http")]
wasmtime_wasi_http::add_only_http_to_linker_async(&mut linker)
.context("failed to add wasi:http/types to linker")?;
let mut component_volume_mounts = Vec::new();
for vm in &component.local_resources.volume_mounts {
if let Some(host_path) = validated_volumes.get(&vm.name) {
component_volume_mounts.push((host_path.clone(), vm.clone()));
} else {
tracing::warn!(
volume = %vm.name,
"component references volume that was not found in workload volumes",
);
}
}
Ok(WorkloadComponent::new(
workload_id.as_ref(),
workload_name.as_ref(),
workload_namespace.as_ref(),
wasmtime_component,
linker,
component_volume_mounts,
component.local_resources,
))
}
}
#[derive(Default)]
pub struct EngineBuilder {
config: wasmtime::Config,
use_pooling_allocator: Option<bool>,
}
impl EngineBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn with_pooling_allocator(mut self, enable: bool) -> Self {
self.use_pooling_allocator = Some(enable);
self
}
pub fn with_config(mut self, config: wasmtime::Config) -> Self {
self.config = config;
self
}
}
impl EngineBuilder {
pub fn build(mut self) -> anyhow::Result<Engine> {
self.config.async_support(true);
if let Ok(true) = use_pooling_allocator_by_default(self.use_pooling_allocator) {
tracing::debug!("using pooling allocator by default");
self.config
.allocation_strategy(wasmtime::InstanceAllocationStrategy::Pooling(
PoolingAllocationConfig::default(),
));
}
let inner = wasmtime::Engine::new(&self.config)?;
Ok(Engine { inner })
}
}
fn use_pooling_allocator_by_default(enable: Option<bool>) -> anyhow::Result<bool> {
const BITS_TO_TEST: u32 = 42;
if let Some(v) = enable {
return Ok(v);
}
let mut config = wasmtime::Config::new();
config.wasm_memory64(true);
config.memory_reservation(1 << BITS_TO_TEST);
let engine = wasmtime::Engine::new(&config)?;
let mut store = wasmtime::Store::new(&engine, ());
let ty = wasmtime::MemoryType::new64(0, Some(1 << (BITS_TO_TEST - 16)));
Ok(wasmtime::Memory::new(&mut store, ty).is_ok())
}