Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bcccdd5
feat(task): add function_name field to task struct
JeffMboya Mar 24, 2026
c582200
feat(storage): add function_name column to postgres and sqlite task r…
JeffMboya Mar 24, 2026
9305bac
feat(manager): include function_name in task dispatch payload
JeffMboya Mar 24, 2026
8b8155f
feat(proplet): support custom component exports via function_name
JeffMboya Mar 24, 2026
a976de7
feat(task): change inputs from []uint64 to []string for WAVE encoding
JeffMboya Mar 24, 2026
1bd3a80
feat(proplet): change inputs to Vec<String> for WAVE-encoded arguments
JeffMboya Mar 24, 2026
eb549a6
feat(proplet): use WAVE encoding for component export arguments
JeffMboya Mar 24, 2026
6a5010b
feat(proplet): support WAVE invoke format for component exports in ho…
JeffMboya Mar 24, 2026
b5d6d27
test(proplet): fix StartRequest struct literals and add function_name…
JeffMboya Mar 24, 2026
38862bd
fix(proplet): detect both wasm-tools component layer bytes and add tests
JeffMboya Mar 24, 2026
1e45177
style(proplet): fix rustfmt violations in wasmtime_runtime and service
JeffMboya Mar 24, 2026
c69465e
refactor: remove function_name field and use name as the function ide…
JeffMboya Mar 25, 2026
3e38762
style: fix gci formatting in postgres and sqlite init files
JeffMboya Mar 25, 2026
96c8e65
test(proplet): update validate_empty_name assertion to match renamed …
JeffMboya Mar 25, 2026
3fb8b4a
feat(proplet): route http/https image_url to HTTP fetch instead of re…
JeffMboya Mar 25, 2026
f11fc7e
fix(proplet): cargo fmt formatting
JeffMboya Mar 25, 2026
484a31c
revert: remove http/https image_url routing (belongs in #169)
JeffMboya Mar 26, 2026
6bb8cca
feat(task): add FlexStrings type to accept both JSON numbers and stri…
JeffMboya Mar 26, 2026
182869a
style(task): remove comment from FlexStrings type
JeffMboya Mar 26, 2026
96b7990
feat(examples): add greet-component wasm example with string input
JeffMboya Mar 26, 2026
240ecd9
fix(ci): fix nlreturn lint in FlexStrings and serialize env-var tests
JeffMboya Mar 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
442 changes: 442 additions & 0 deletions examples/greet-component/Cargo.lock

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions examples/greet-component/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "greet-component"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wit-bindgen = "0.43.0"
11 changes: 11 additions & 0 deletions examples/greet-component/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
wit_bindgen::generate!({ world: "greet-world" });

struct Component;

impl Guest for Component {
fn greet(name: String) -> String {
format!("Hello, {}!", name)
}
}

export!(Component);
5 changes: 5 additions & 0 deletions examples/greet-component/wit/world.wit
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package propeller:greet-component;

world greet-world {
export greet: func(name: string) -> string;
}
29 changes: 28 additions & 1 deletion pkg/task/task.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,38 @@
package task

import (
"encoding/json"
"fmt"
"time"

"github.com/absmach/propeller/pkg/proplet"
)

type FlexStrings []string

func (f *FlexStrings) UnmarshalJSON(data []byte) error {
var raw []json.RawMessage
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
*f = make(FlexStrings, len(raw))
for i, r := range raw {
var s string
if err := json.Unmarshal(r, &s); err == nil {
(*f)[i] = s

continue
}
var n json.Number
if err := json.Unmarshal(r, &n); err != nil {
return fmt.Errorf("inputs[%d]: expected string or number", i)
}
(*f)[i] = n.String()
}

return nil
}

type State uint8

const (
Expand Down Expand Up @@ -70,7 +97,7 @@ type Task struct {
ImageURL string `json:"image_url,omitempty"`
File []byte `json:"file,omitempty"`
CLIArgs []string `json:"cli_args"`
Inputs []uint64 `json:"inputs,omitempty"`
Inputs FlexStrings `json:"inputs,omitempty"`
Env map[string]string `json:"env,omitempty"`
Daemon bool `json:"daemon"`
Encrypted bool `json:"encrypted"`
Expand Down
52 changes: 52 additions & 0 deletions proplet/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion proplet/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ rumqttc = "0.25.1"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.9.8"
wasmtime = { version = "42.0.1", features = ["component-model", "async"] }
wasmtime = { version = "42.0.1", features = ["component-model", "async", "wave"] }
wasmtime-wasi = "42.0.1"
wasmtime-wasi-http = "42.0.1"
hyper = { version = "1.7", features = ["full"] }
Expand Down Expand Up @@ -41,6 +41,7 @@ futures-util = { version = "0.3" }

# ELASTIC TEE HAL — hardware abstraction layer for TEE workloads
elastic-tee-hal = { git = "https://github.com/elasticproject-eu/wasmhal", default-features = false, features = ["amd-sev"] }
wasm-wave = "0.244.0"

[features]
default = []
Expand Down
21 changes: 21 additions & 0 deletions proplet/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,13 @@ impl PropletConfig {
mod tests {
use super::*;
use std::env;
use std::sync::{Mutex, OnceLock};

static ENV_MUTEX: OnceLock<Mutex<()>> = OnceLock::new();

fn env_lock() -> std::sync::MutexGuard<'static, ()> {
ENV_MUTEX.get_or_init(|| Mutex::new(())).lock().unwrap()
}

#[test]
fn test_proplet_config_default() {
Expand Down Expand Up @@ -469,6 +476,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_log_level() {
let _lock = env_lock();
env::set_var("PROPLET_LOG_LEVEL", "debug");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_LOG_LEVEL");
Expand All @@ -478,6 +486,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_mqtt_address() {
let _lock = env_lock();
env::set_var("PROPLET_MQTT_ADDRESS", "tcp://mqtt.example.com:1883");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MQTT_ADDRESS");
Expand All @@ -487,6 +496,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_mqtt_timeout() {
let _lock = env_lock();
env::set_var("PROPLET_MQTT_TIMEOUT", "120");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MQTT_TIMEOUT");
Expand All @@ -496,6 +506,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_mqtt_qos() {
let _lock = env_lock();
env::set_var("PROPLET_MQTT_QOS", "1");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MQTT_QOS");
Expand All @@ -505,6 +516,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_liveliness_interval() {
let _lock = env_lock();
env::set_var("PROPLET_LIVELINESS_INTERVAL", "20");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_LIVELINESS_INTERVAL");
Expand All @@ -514,6 +526,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_domain_id() {
let _lock = env_lock();
env::set_var("PROPLET_DOMAIN_ID", "domain-123");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_DOMAIN_ID");
Expand All @@ -523,6 +536,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_channel_id() {
let _lock = env_lock();
env::set_var("PROPLET_CHANNEL_ID", "channel-456");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_CHANNEL_ID");
Expand All @@ -532,6 +546,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_client_id() {
let _lock = env_lock();
env::set_var("PROPLET_CLIENT_ID", "client-789");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_CLIENT_ID");
Expand All @@ -541,6 +556,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_client_key() {
let _lock = env_lock();
env::set_var("PROPLET_CLIENT_KEY", "secret-key");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_CLIENT_KEY");
Expand All @@ -550,6 +566,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_k8s_namespace() {
let _lock = env_lock();
env::set_var("PROPLET_MANAGER_K8S_NAMESPACE", "production");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MANAGER_K8S_NAMESPACE");
Expand All @@ -559,6 +576,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_external_runtime() {
let _lock = env_lock();
env::set_var("PROPLET_EXTERNAL_WASM_RUNTIME", "/usr/local/bin/wasmtime");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_EXTERNAL_WASM_RUNTIME");
Expand All @@ -571,6 +589,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_mqtt_timeout_invalid() {
let _lock = env_lock();
env::set_var("PROPLET_MQTT_TIMEOUT", "not-a-number");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MQTT_TIMEOUT");
Expand All @@ -580,6 +599,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_mqtt_qos_invalid() {
let _lock = env_lock();
env::set_var("PROPLET_MQTT_QOS", "invalid");
let config = PropletConfig::from_env();
env::remove_var("PROPLET_MQTT_QOS");
Expand All @@ -589,6 +609,7 @@ mod tests {

#[test]
fn test_proplet_config_from_env_no_env_vars() {
let _lock = env_lock();
let vars_to_clear = vec![
"PROPLET_LOG_LEVEL",
"PROPLET_MQTT_ADDRESS",
Expand Down
52 changes: 46 additions & 6 deletions proplet/src/runtime/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@ impl HostRuntime {
}
Ok(())
}

fn is_wasm_component(binary: &[u8]) -> bool {
// Component model binary layer marker: 0x0a (older wasm-tools) or 0x0d (wasm-tools >= 0.200)
binary.len() >= 8
&& (binary[4..8] == [0x0a, 0x00, 0x01, 0x00]
|| binary[4..8] == [0x0d, 0x00, 0x01, 0x00])
}
}

#[async_trait]
Expand All @@ -73,13 +80,20 @@ impl Runtime for HostRuntime {

cmd.arg("run");

let is_component = Self::is_wasm_component(&config.wasm_binary);
let cli_args_has_invoke = config.cli_args.iter().any(|a| a == "--invoke");
if !config.function_name.is_empty()
let has_custom_export = !config.function_name.is_empty()
&& config.function_name != "_start"
&& !config.function_name.starts_with("fl-round-")
&& !cli_args_has_invoke
{
cmd.arg("--invoke").arg(&config.function_name);
&& !cli_args_has_invoke;

if has_custom_export {
if is_component {
let wave_call = format!("{}({})", config.function_name, config.args.join(", "));
cmd.arg("--invoke").arg(&wave_call);
} else {
cmd.arg("--invoke").arg(&config.function_name);
}
}

for arg in &config.cli_args {
Expand All @@ -103,8 +117,10 @@ impl Runtime for HostRuntime {

cmd.arg(&temp_file);

for arg in &config.args {
cmd.arg(arg.to_string());
if !is_component || !has_custom_export {
for arg in &config.args {
cmd.arg(arg);
}
}

cmd.envs(&config.env);
Expand Down Expand Up @@ -391,4 +407,28 @@ mod tests {

runtime.cleanup_temp_file(file_path).await.unwrap();
}

#[test]
fn test_is_wasm_component_old_format() {
let binary = [0x00, 0x61, 0x73, 0x6d, 0x0a, 0x00, 0x01, 0x00, 0x00];
assert!(HostRuntime::is_wasm_component(&binary));
}

#[test]
fn test_is_wasm_component_new_format() {
let binary = [0x00, 0x61, 0x73, 0x6d, 0x0d, 0x00, 0x01, 0x00, 0x00];
assert!(HostRuntime::is_wasm_component(&binary));
}

#[test]
fn test_is_wasm_component_rejects_core_module() {
let binary = [0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x00];
assert!(!HostRuntime::is_wasm_component(&binary));
}

#[test]
fn test_is_wasm_component_rejects_too_short() {
let binary = [0x00, 0x61, 0x73, 0x6d];
assert!(!HostRuntime::is_wasm_component(&binary));
}
}
2 changes: 1 addition & 1 deletion proplet/src/runtime/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pub struct StartConfig {
pub wasm_binary: Vec<u8>,
pub cli_args: Vec<String>,
pub env: HashMap<String, String>,
pub args: Vec<u64>,
pub args: Vec<String>,
#[allow(dead_code)]
pub mode: Option<String>,
}
Expand Down
Loading