diff --git a/Cargo.lock b/Cargo.lock index ac7a0a983f..a530a00dab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -23,18 +23,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher 0.3.0", - "cpufeatures", - "opaque-debug", -] - [[package]] name = "aes" version = "0.8.4" @@ -42,7 +30,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", - "cipher 0.4.4", + "cipher", "cpufeatures", ] @@ -770,29 +758,31 @@ dependencies = [ ] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "blake2" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" dependencies = [ - "generic-array", + "digest", ] [[package]] -name = "block-modes" -version = "0.8.1" +name = "block-buffer" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ - "block-padding", - "cipher 0.3.0", + "generic-array", ] [[package]] name = "block-padding" -version = "0.2.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] [[package]] name = "blocking" @@ -1220,6 +1210,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + [[package]] name = "cc" version = "1.0.90" @@ -1266,15 +1265,6 @@ dependencies = [ "windows-targets 0.52.4", ] -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - [[package]] name = "cipher" version = "0.4.4" @@ -1576,8 +1566,7 @@ dependencies = [ [[package]] name = "cranelift-bforest" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b6b33d7e757a887989eb18b35712b2a67d96171ec3149d1bfb657b29b7b367c" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-entity", ] @@ -1585,8 +1574,7 @@ dependencies = [ [[package]] name = "cranelift-codegen" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9acf15cb22be42d07c3b57d7856329cb228b7315d385346149df2566ad5e4aa" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "bumpalo", "cranelift-bforest", @@ -1607,8 +1595,7 @@ dependencies = [ [[package]] name = "cranelift-codegen-meta" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e934d301392b73b3f8b0540391fb82465a0f179a3cee7c726482ac4727efcc97" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-codegen-shared", ] @@ -1616,14 +1603,12 @@ dependencies = [ [[package]] name = "cranelift-codegen-shared" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb2a2566b3d54b854dfb288b3b187f6d3d17d6f762c92898207eba302931da" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" [[package]] name = "cranelift-control" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0100f33b704cdacd01ad66ff41f8c5030d57cbff078e2a4e49ab1822591299fa" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "arbitrary", ] @@ -1631,8 +1616,7 @@ dependencies = [ [[package]] name = "cranelift-entity" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8cfdc315e5d18997093e040a8d234bea1ac1e118a716d3e30f40d449e78207b" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "serde 1.0.197", "serde_derive", @@ -1641,8 +1625,7 @@ dependencies = [ [[package]] name = "cranelift-frontend" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f74b84f16af2e982b0c0c72233503d9d55cbfe3865dbe807ca28dc6642a28b5" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-codegen", "log", @@ -1653,14 +1636,12 @@ dependencies = [ [[package]] name = "cranelift-isle" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adf306d3dde705fb94bd48082f01d38c4ededc74293a4c007805f610bf08bc6e" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" [[package]] name = "cranelift-native" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea0ebdef7aff4a79bcbc8b6495f31315f16b3bf311152f472eaa8d679352581" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-codegen", "libc", @@ -1670,8 +1651,7 @@ dependencies = [ [[package]] name = "cranelift-wasm" version = "0.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d549108a1942065cdbac3bb96c2952afa0e1b9a3beff4b08c4308ac72257576d" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-codegen", "cranelift-entity", @@ -2417,6 +2397,20 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "expander" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e83c02035136f1592a47964ea60c05a50e4ed8b5892cfac197063850898d4d" +dependencies = [ + "blake2", + "fs-err", + "prettier-please", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "fallible-iterator" version = "0.2.0" @@ -2482,6 +2476,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "field-offset" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f" +dependencies = [ + "memoffset 0.9.1", + "rustc_version", +] + [[package]] name = "filetime" version = "0.2.23" @@ -2557,6 +2561,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "fs-set-times" version = "0.20.1" @@ -3289,12 +3302,12 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" dependencies = [ "bytes", - "futures-core", + "futures-util", "http 1.1.0", "http-body 1.0.0", "pin-project-lite", @@ -3657,6 +3670,7 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" dependencies = [ + "block-padding", "generic-array", ] @@ -5067,12 +5081,6 @@ version = "11.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - [[package]] name = "openssl" version = "0.10.64" @@ -5811,6 +5819,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettier-please" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3" +dependencies = [ + "proc-macro2", + "syn 2.0.58", +] + [[package]] name = "prettyplease" version = "0.2.17" @@ -6827,12 +6845,12 @@ dependencies = [ [[package]] name = "secret-service" -version = "3.0.1" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5da1a5ad4d28c03536f82f77d9f36603f5e37d8869ac98f0a750d5b5686d8d95" +checksum = "b5204d39df37f06d1944935232fd2dfe05008def7ca599bf28c0800366c8a8f9" dependencies = [ - "aes 0.7.5", - "block-modes", + "aes", + "cbc", "futures-util", "generic-array", "hkdf", @@ -7540,6 +7558,103 @@ dependencies = [ "toml 0.5.11", ] +[[package]] +name = "spin-factor-key-value" +version = "2.6.0-pre0" +dependencies = [ + "anyhow", + "serde 1.0.197", + "spin-factors", + "spin-key-value", + "spin-world", + "toml 0.8.12", +] + +[[package]] +name = "spin-factor-outbound-http" +version = "2.6.0-pre0" +dependencies = [ + "anyhow", + "http 1.1.0", + "spin-factor-outbound-networking", + "spin-factor-wasi", + "spin-factors", + "spin-world", + "tracing", + "wasmtime-wasi-http", +] + +[[package]] +name = "spin-factor-outbound-networking" +version = "2.6.0-pre0" +dependencies = [ + "futures-util", + "ipnet", + "spin-factor-variables", + "spin-factor-wasi", + "spin-factors", + "spin-outbound-networking", + "tracing", +] + +[[package]] +name = "spin-factor-variables" +version = "2.6.0-pre0" +dependencies = [ + "serde 1.0.197", + "spin-expressions", + "spin-factors", + "spin-world", + "toml 0.8.12", +] + +[[package]] +name = "spin-factor-wasi" +version = "2.6.0-pre0" +dependencies = [ + "cap-primitives 3.0.0", + "spin-factors", + "wasmtime-wasi", +] + +[[package]] +name = "spin-factors" +version = "2.6.0-pre0" +dependencies = [ + "anyhow", + "field-offset", + "http 1.1.0", + "http-body-util", + "serde 1.0.197", + "serde_json", + "spin-app", + "spin-componentize", + "spin-factor-key-value", + "spin-factor-outbound-http", + "spin-factor-outbound-networking", + "spin-factor-variables", + "spin-factor-wasi", + "spin-factors-derive", + "spin-key-value-sqlite", + "spin-loader", + "thiserror", + "tokio", + "toml 0.8.12", + "tracing", + "wasmtime", + "wasmtime-wasi-http", +] + +[[package]] +name = "spin-factors-derive" +version = "2.6.0-pre0" +dependencies = [ + "expander", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "spin-http" version = "2.7.0-pre0" @@ -8075,6 +8190,7 @@ dependencies = [ name = "spin-world" version = "2.7.0-pre0" dependencies = [ + "async-trait", "wasmtime", ] @@ -9407,8 +9523,7 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasi-common" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86fd41e1e26ff6af9451c6a332a5ce5f5283ca51e87d875cdd9a05305598ee3" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "bitflags 2.5.0", @@ -9741,8 +9856,7 @@ dependencies = [ [[package]] name = "wasmtime" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "786d8b5e7a4d54917c5ebe555b9667337e5f93383f49bddaaeec2eba68093b45" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "addr2line", "anyhow", @@ -9797,8 +9911,7 @@ dependencies = [ [[package]] name = "wasmtime-asm-macros" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d697d99c341d4a9ffb72f3af7a02124d233eeb59aee010f36d88e97cca553d5e" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cfg-if", ] @@ -9806,8 +9919,7 @@ dependencies = [ [[package]] name = "wasmtime-cache" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916610f9ae9a6c22deb25bba2e6247ba9f00b093d30620875203b91328a1adfa" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "base64 0.21.7", @@ -9826,8 +9938,7 @@ dependencies = [ [[package]] name = "wasmtime-component-macro" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b29b462b068e73b5b27fae092a27f47e5937cabf6b26be2779c978698a52feca" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "proc-macro2", @@ -9841,14 +9952,12 @@ dependencies = [ [[package]] name = "wasmtime-component-util" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d2912c53d9054984b380dfbd7579f9c3681b2a73b903a56bd71a1c4f175f1e" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" [[package]] name = "wasmtime-cranelift" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3975deafea000457ba84355c7c0fce0372937204f77026510b7b454f28a3a65" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cfg-if", @@ -9871,8 +9980,7 @@ dependencies = [ [[package]] name = "wasmtime-environ" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f444e900e848b884d8a8a2949b6f5b92af642a3e663ff8fbe78731143a55be61" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cpp_demangle", @@ -9896,8 +10004,7 @@ dependencies = [ [[package]] name = "wasmtime-fiber" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ded58eb2d1bf0dcd2182d0ccd7055c4b10b50d711514f1d73f61515d0fa829d" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cc", @@ -9911,8 +10018,7 @@ dependencies = [ [[package]] name = "wasmtime-jit-debug" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bc54198c6720f098210a85efb3ba8c078d1de4d373cdb6778850a66ae088d11" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "object 0.36.0", "once_cell", @@ -9923,8 +10029,7 @@ dependencies = [ [[package]] name = "wasmtime-jit-icache-coherence" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5afe2f0499542f9a4bcfa1b55bfdda803b6ade4e7c93c6b99e0f39dba44b0a91" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cfg-if", @@ -9935,14 +10040,12 @@ dependencies = [ [[package]] name = "wasmtime-slab" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7de1f2bec5bbb35d532e61c85c049dc84ae671df60492f90b954ecf21169e7" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" [[package]] name = "wasmtime-types" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "412463e9000e14cf6856be48628d2213c20c153e29ffc22b036980c892ea6964" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "cranelift-entity", "serde 1.0.197", @@ -9954,8 +10057,7 @@ dependencies = [ [[package]] name = "wasmtime-versioned-export-macros" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de5a9bc4f44ceeb168e9e8e3be4e0b4beb9095b468479663a9e24c667e36826f" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "proc-macro2", "quote", @@ -9965,8 +10067,7 @@ dependencies = [ [[package]] name = "wasmtime-wasi" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8abb1301089ed8e0b4840f539cba316a73ac382090f1b25d22d8c8eed8df49c7" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "async-trait", @@ -9996,8 +10097,7 @@ dependencies = [ [[package]] name = "wasmtime-wasi-http" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "315cadc284b808cfbd6be9295da4009144c106723f09b421ce6c6d89275cfdb7" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "async-trait", @@ -10019,8 +10119,7 @@ dependencies = [ [[package]] name = "wasmtime-winch" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed4db238a0241df2d15f79ad17b3a37a27f2ea6cb885894d81b42ae107544466" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cranelift-codegen", @@ -10036,8 +10135,7 @@ dependencies = [ [[package]] name = "wasmtime-wit-bindgen" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc077306b38288262e5ba01d4b21532a6987416cdc0aedf04bb06c22a68fdc" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "heck 0.4.1", @@ -10216,8 +10314,7 @@ dependencies = [ [[package]] name = "wiggle" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29830e5d01c182d24b94092c697aa7ab0ee97d22e78a2bf40ca91eae6ebca5c2" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "async-trait", @@ -10231,8 +10328,7 @@ dependencies = [ [[package]] name = "wiggle-generate" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "557567f2793508760cd855f7659b7a0b9dc4dbc451f53f1415d6943a15311ade" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "heck 0.4.1", @@ -10246,8 +10342,7 @@ dependencies = [ [[package]] name = "wiggle-macro" version = "22.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc26129a8aea20b62c961d1b9ab4a3c3b56b10042ed85d004f8678af0f21ba6e" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "proc-macro2", "quote", @@ -10289,8 +10384,7 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "winch-codegen" version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c6915884e731b2db0d8cf08cb64474cb69221a161675fd3c135f91febc3daa" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=69a78fc1359769f320b5968042cb9d988336dd54#69a78fc1359769f320b5968042cb9d988336dd54" dependencies = [ "anyhow", "cranelift-codegen", @@ -10838,7 +10932,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ - "aes 0.8.4", + "aes", "byteorder", "bzip2", "constant_time_eq", diff --git a/crates/app/src/lib.rs b/crates/app/src/lib.rs index 70d6a59b6b..97077032db 100644 --- a/crates/app/src/lib.rs +++ b/crates/app/src/lib.rs @@ -296,6 +296,14 @@ impl<'a, L> AppComponent<'a, L> { &self.locked.source } + /// Returns an iterator of environment variable (key, value) pairs. + pub fn environment(&self) -> impl IntoIterator { + self.locked + .env + .iter() + .map(|(k, v)| (k.as_str(), v.as_str())) + } + /// Returns an iterator of [`ContentPath`]s for this component's configured /// "directory mounts". pub fn files(&self) -> std::slice::Iter { diff --git a/crates/expressions/src/lib.rs b/crates/expressions/src/lib.rs index 612c0696bb..811c0e864d 100644 --- a/crates/expressions/src/lib.rs +++ b/crates/expressions/src/lib.rs @@ -5,6 +5,8 @@ use std::{borrow::Cow, collections::HashMap, fmt::Debug}; use spin_locked_app::Variable; +pub use async_trait; + pub use provider::Provider; use template::Part; pub use template::Template; @@ -251,6 +253,14 @@ impl<'a> Key<'a> { } } +impl<'a> TryFrom<&'a str> for Key<'a> { + type Error = Error; + + fn try_from(value: &'a str) -> std::prelude::v1::Result { + Self::new(value) + } +} + impl<'a> AsRef for Key<'a> { fn as_ref(&self) -> &str { self.0 diff --git a/crates/factor-key-value/Cargo.toml b/crates/factor-key-value/Cargo.toml new file mode 100644 index 0000000000..89e060e463 --- /dev/null +++ b/crates/factor-key-value/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "spin-factor-key-value" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = "1.0" +serde = { version = "1.0", features = ["rc"] } +spin-factors = { path = "../factors" } +# TODO: merge with this crate +spin-key-value = { path = "../key-value" } +spin-world = { path = "../world" } +toml = "0.8" + +[lints] +workspace = true diff --git a/crates/factor-key-value/src/lib.rs b/crates/factor-key-value/src/lib.rs new file mode 100644 index 0000000000..efca63a162 --- /dev/null +++ b/crates/factor-key-value/src/lib.rs @@ -0,0 +1,164 @@ +mod store; + +use std::{ + collections::{HashMap, HashSet}, + sync::Arc, +}; + +use anyhow::{bail, ensure}; +use serde::Deserialize; +use spin_factors::{ + anyhow::{self, Context}, + ConfigureAppContext, Factor, FactorInstanceBuilder, FactorRuntimeConfig, InitContext, + InstanceBuilders, PrepareContext, RuntimeFactors, +}; +use spin_key_value::{ + CachingStoreManager, DelegatingStoreManager, KeyValueDispatch, StoreManager, + KEY_VALUE_STORES_KEY, +}; +use store::{store_from_toml_fn, StoreFromToml}; + +pub use store::MakeKeyValueStore; + +#[derive(Default)] +pub struct KeyValueFactor { + store_types: HashMap<&'static str, StoreFromToml>, +} + +impl KeyValueFactor { + pub fn add_store_type(&mut self, store_type: T) -> anyhow::Result<()> { + if self + .store_types + .insert(T::RUNTIME_CONFIG_TYPE, store_from_toml_fn(store_type)) + .is_some() + { + bail!( + "duplicate key value store type {:?}", + T::RUNTIME_CONFIG_TYPE + ); + } + Ok(()) + } +} + +impl Factor for KeyValueFactor { + type RuntimeConfig = RuntimeConfig; + type AppState = AppState; + type InstanceBuilder = InstanceBuilder; + + fn init( + &mut self, + mut ctx: InitContext, + ) -> anyhow::Result<()> { + ctx.link_bindings(spin_world::v1::key_value::add_to_linker)?; + ctx.link_bindings(spin_world::v2::key_value::add_to_linker)?; + Ok(()) + } + + fn configure_app( + &self, + mut ctx: ConfigureAppContext, + ) -> anyhow::Result { + // Build StoreManager from runtime config + let mut stores = HashMap::new(); + if let Some(runtime_config) = ctx.take_runtime_config() { + for (label, StoreConfig { type_, config }) in runtime_config.store_configs { + let store_maker = self + .store_types + .get(type_.as_str()) + .with_context(|| format!("unknown key value store type {type_:?}"))?; + let store = store_maker(config)?; + stores.insert(label, store); + } + } + let delegating_manager = DelegatingStoreManager::new(stores); + let caching_manager = CachingStoreManager::new(delegating_manager); + let store_manager = Arc::new(caching_manager); + + // Build component -> allowed stores map + let mut component_allowed_stores = HashMap::new(); + for component in ctx.app().components() { + let component_id = component.id().to_string(); + let key_value_stores = component + .get_metadata(KEY_VALUE_STORES_KEY)? + .unwrap_or_default() + .into_iter() + .collect::>(); + for label in &key_value_stores { + // TODO: port nicer errors from KeyValueComponent (via error type?) + ensure!( + store_manager.is_defined(label), + "unknown key_value_stores label {label:?} for component {component_id:?}" + ); + } + component_allowed_stores.insert(component_id, key_value_stores); + // TODO: warn (?) on unused store? + } + + Ok(AppState { + store_manager, + component_allowed_stores, + }) + } + + fn prepare( + &self, + ctx: PrepareContext, + _builders: &mut InstanceBuilders, + ) -> anyhow::Result { + let app_state = ctx.app_state(); + let allowed_stores = app_state + .component_allowed_stores + .get(ctx.app_component().id()) + .expect("component should be in component_stores") + .clone(); + Ok(InstanceBuilder { + store_manager: app_state.store_manager.clone(), + allowed_stores, + }) + } +} + +#[derive(Deserialize)] +#[serde(transparent)] +pub struct RuntimeConfig { + store_configs: HashMap, +} + +impl FactorRuntimeConfig for RuntimeConfig { + const KEY: &'static str = "key_value_store"; +} + +#[derive(Deserialize)] +struct StoreConfig { + #[serde(rename = "type")] + type_: String, + #[serde(flatten)] + config: toml::Table, +} + +type AppStoreManager = CachingStoreManager; + +pub struct AppState { + store_manager: Arc, + component_allowed_stores: HashMap>, +} + +pub struct InstanceBuilder { + store_manager: Arc, + allowed_stores: HashSet, +} + +impl FactorInstanceBuilder for InstanceBuilder { + type InstanceState = KeyValueDispatch; + + fn build(self) -> anyhow::Result { + let Self { + store_manager, + allowed_stores, + } = self; + let mut dispatch = KeyValueDispatch::new_with_capacity(u32::MAX); + dispatch.init(allowed_stores, store_manager); + Ok(dispatch) + } +} diff --git a/crates/factor-key-value/src/store.rs b/crates/factor-key-value/src/store.rs new file mode 100644 index 0000000000..5b0c956584 --- /dev/null +++ b/crates/factor-key-value/src/store.rs @@ -0,0 +1,24 @@ +use std::sync::Arc; + +use serde::de::DeserializeOwned; +use spin_key_value::StoreManager; + +pub trait MakeKeyValueStore: 'static { + const RUNTIME_CONFIG_TYPE: &'static str; + + type RuntimeConfig: DeserializeOwned; + type StoreManager: StoreManager; + + fn make_store(&self, runtime_config: Self::RuntimeConfig) + -> anyhow::Result; +} + +pub(crate) type StoreFromToml = Box anyhow::Result>>; + +pub(crate) fn store_from_toml_fn(provider_type: T) -> StoreFromToml { + Box::new(move |table| { + let runtime_config: T::RuntimeConfig = table.try_into()?; + let provider = provider_type.make_store(runtime_config)?; + Ok(Arc::new(provider)) + }) +} diff --git a/crates/factor-outbound-http/Cargo.toml b/crates/factor-outbound-http/Cargo.toml new file mode 100644 index 0000000000..e922255e35 --- /dev/null +++ b/crates/factor-outbound-http/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spin-factor-outbound-http" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = "1.0" +http = "1.1.0" +spin-factor-outbound-networking = { path = "../factor-outbound-networking" } +spin-factor-wasi = { path = "../factor-wasi" } +spin-factors = { path = "../factors" } +spin-world = { path = "../world" } +tracing = { workspace = true } +wasmtime-wasi-http = { workspace = true } + +[lints] +workspace = true diff --git a/crates/factor-outbound-http/src/lib.rs b/crates/factor-outbound-http/src/lib.rs new file mode 100644 index 0000000000..36000239b9 --- /dev/null +++ b/crates/factor-outbound-http/src/lib.rs @@ -0,0 +1,56 @@ +mod spin; +mod wasi; + +use spin_factor_outbound_networking::{OutboundAllowedHosts, OutboundNetworkingFactor}; +use spin_factors::{ + anyhow, ConfigureAppContext, Factor, InstanceBuilders, PrepareContext, RuntimeFactors, + SelfInstanceBuilder, +}; +use wasmtime_wasi_http::WasiHttpCtx; + +pub use wasi::get_wasi_http_view; + +pub struct OutboundHttpFactor; + +impl Factor for OutboundHttpFactor { + type RuntimeConfig = (); + type AppState = (); + type InstanceBuilder = InstanceState; + + fn init( + &mut self, + mut ctx: spin_factors::InitContext, + ) -> anyhow::Result<()> { + ctx.link_bindings(spin_world::v1::http::add_to_linker)?; + wasi::add_to_linker::(ctx.linker())?; + Ok(()) + } + + fn configure_app( + &self, + _ctx: ConfigureAppContext, + ) -> anyhow::Result { + Ok(()) + } + + fn prepare( + &self, + _ctx: PrepareContext, + builders: &mut InstanceBuilders, + ) -> anyhow::Result { + let allowed_hosts = builders + .get_mut::()? + .allowed_hosts(); + Ok(InstanceState { + allowed_hosts, + wasi_http_ctx: WasiHttpCtx::new(), + }) + } +} + +pub struct InstanceState { + allowed_hosts: OutboundAllowedHosts, + wasi_http_ctx: WasiHttpCtx, +} + +impl SelfInstanceBuilder for InstanceState {} diff --git a/crates/factor-outbound-http/src/spin.rs b/crates/factor-outbound-http/src/spin.rs new file mode 100644 index 0000000000..22ea723f2f --- /dev/null +++ b/crates/factor-outbound-http/src/spin.rs @@ -0,0 +1,31 @@ +use spin_factor_outbound_networking::OutboundUrl; +use spin_world::{ + async_trait, + v1::http, + v1::http_types::{self, HttpError, Request, Response}, +}; + +#[async_trait] +impl http::Host for crate::InstanceState { + async fn send_request(&mut self, req: Request) -> Result { + // FIXME(lann): This is all just a stub to test allowed_outbound_hosts + let outbound_url = OutboundUrl::parse(&req.uri, "https").or(Err(HttpError::InvalidUrl))?; + match self.allowed_hosts.allows(&outbound_url).await { + Ok(true) => (), + _ => { + return Err(HttpError::DestinationNotAllowed); + } + } + Ok(Response { + status: 200, + headers: None, + body: Some(b"test response".into()), + }) + } +} + +impl http_types::Host for crate::InstanceState { + fn convert_http_error(&mut self, err: HttpError) -> anyhow::Result { + Ok(err) + } +} diff --git a/crates/factor-outbound-http/src/wasi.rs b/crates/factor-outbound-http/src/wasi.rs new file mode 100644 index 0000000000..daec51fd0b --- /dev/null +++ b/crates/factor-outbound-http/src/wasi.rs @@ -0,0 +1,51 @@ +use anyhow::Context; +use spin_factors::{Linker, RuntimeFactors}; +use wasmtime_wasi_http::{WasiHttpImpl, WasiHttpView}; + +pub(crate) fn add_to_linker(linker: &mut Linker) -> anyhow::Result<()> { + fn type_annotate(f: F) -> F + where + F: Fn(&mut T) -> WasiHttpImpl, + { + f + } + let wasi_and_http_getter = + T::instance_state_getter2::() + .context("failed to get WasiFactor")?; + let host_getter = type_annotate(move |data| { + let (wasi, http) = wasi_and_http_getter.get_states(data); + WasiHttpImpl(MutStates { http, wasi }) + }); + wasmtime_wasi_http::bindings::http::outgoing_handler::add_to_linker_get_host( + linker, + host_getter, + )?; + wasmtime_wasi_http::bindings::http::types::add_to_linker_get_host(linker, host_getter)?; + Ok(()) +} + +struct MutStates<'a> { + http: &'a mut crate::InstanceState, + wasi: &'a mut spin_factor_wasi::InstanceState, +} + +impl<'a> WasiHttpView for MutStates<'a> { + fn ctx(&mut self) -> &mut wasmtime_wasi_http::WasiHttpCtx { + &mut self.http.wasi_http_ctx + } + + fn table(&mut self) -> &mut spin_factors::wasmtime::component::ResourceTable { + self.wasi.table() + } +} + +// TODO: This is a little weird, organizationally +pub fn get_wasi_http_view( + instance_state: &mut T::InstanceState, +) -> anyhow::Result { + let wasi_and_http_getter = + T::instance_state_getter2::() + .context("failed to get WasiFactor")?; + let (wasi, http) = wasi_and_http_getter.get_states(instance_state); + Ok(MutStates { http, wasi }) +} diff --git a/crates/factor-outbound-networking/Cargo.toml b/crates/factor-outbound-networking/Cargo.toml new file mode 100644 index 0000000000..13dd49c9e3 --- /dev/null +++ b/crates/factor-outbound-networking/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "spin-factor-outbound-networking" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +futures-util = "0.3" +ipnet = "2.9.0" +spin-factor-variables = { path = "../factor-variables" } +spin-factor-wasi = { path = "../factor-wasi" } +spin-factors = { path = "../factors" } +# TODO: merge with this crate +spin-outbound-networking = { path = "../outbound-networking" } +tracing = { workspace = true } + +[lints] +workspace = true diff --git a/crates/factor-outbound-networking/src/lib.rs b/crates/factor-outbound-networking/src/lib.rs new file mode 100644 index 0000000000..66b5e3732d --- /dev/null +++ b/crates/factor-outbound-networking/src/lib.rs @@ -0,0 +1,150 @@ +use std::{collections::HashMap, sync::Arc}; + +use futures_util::{ + future::{BoxFuture, Shared}, + FutureExt, +}; +use spin_factor_variables::VariablesFactor; +use spin_factor_wasi::WasiFactor; +use spin_factors::{ + anyhow::{self, Context}, + ConfigureAppContext, Factor, FactorInstanceBuilder, InstanceBuilders, PrepareContext, + RuntimeFactors, +}; +use spin_outbound_networking::{AllowedHostsConfig, ALLOWED_HOSTS_KEY}; + +pub use spin_outbound_networking::OutboundUrl; + +pub type SharedFutureResult = Shared, Arc>>>; + +pub struct OutboundNetworkingFactor; + +impl Factor for OutboundNetworkingFactor { + type RuntimeConfig = (); + type AppState = AppState; + type InstanceBuilder = InstanceBuilder; + + fn configure_app( + &self, + ctx: ConfigureAppContext, + ) -> anyhow::Result { + // Extract allowed_outbound_hosts for all components + let component_allowed_hosts = ctx + .app() + .components() + .map(|component| { + Ok(( + component.id().to_string(), + component + .get_metadata(ALLOWED_HOSTS_KEY)? + .unwrap_or_default() + .into_boxed_slice() + .into(), + )) + }) + .collect::>()?; + Ok(AppState { + component_allowed_hosts, + }) + } + + fn prepare( + &self, + ctx: PrepareContext, + builders: &mut InstanceBuilders, + ) -> anyhow::Result { + let hosts = ctx + .app_state() + .component_allowed_hosts + .get(ctx.app_component().id()) + .cloned() + .context("missing component allowed hosts")?; + let resolver = builders.get_mut::()?.resolver().clone(); + let allowed_hosts_future = async move { + let prepared = resolver.prepare().await?; + AllowedHostsConfig::parse(&hosts, &prepared) + } + .map(|res| res.map(Arc::new).map_err(Arc::new)) + .boxed() + .shared(); + // let prepared_resolver = resolver.prepare().await?; + // let allowed_hosts = AllowedHostsConfig::parse( + // .context("missing component allowed hosts")?, + // &prepared_resolver, + // )?; + + // Update Wasi socket allowed ports + let wasi_preparer = builders.get_mut::()?; + let hosts_future = allowed_hosts_future.clone(); + wasi_preparer.outbound_socket_addr_check(move |addr| { + let hosts_future = hosts_future.clone(); + async move { + match hosts_future.await { + Ok(allowed_hosts) => { + // TODO: verify this actually works... + spin_outbound_networking::check_url(&addr.to_string(), "*", &allowed_hosts) + } + Err(err) => { + // TODO: should this trap (somehow)? + tracing::error!(%err, "allowed_outbound_hosts variable resolution failed"); + false + } + } + } + }); + Ok(InstanceBuilder { + allowed_hosts_future, + }) + } +} + +pub struct AppState { + component_allowed_hosts: HashMap>, +} + +pub struct InstanceBuilder { + allowed_hosts_future: SharedFutureResult, +} + +impl InstanceBuilder { + pub fn allowed_hosts(&self) -> OutboundAllowedHosts { + OutboundAllowedHosts { + allowed_hosts_future: self.allowed_hosts_future.clone(), + } + } +} + +impl FactorInstanceBuilder for InstanceBuilder { + type InstanceState = (); + + fn build(self) -> anyhow::Result { + Ok(()) + } +} + +// TODO: Refactor w/ spin-outbound-networking crate to simplify +pub struct OutboundAllowedHosts { + allowed_hosts_future: SharedFutureResult, +} + +impl OutboundAllowedHosts { + pub async fn allows(&self, url: &OutboundUrl) -> anyhow::Result { + Ok(self.resolve().await?.allows(url)) + } + + pub async fn check_url(&self, url: &str, scheme: &str) -> anyhow::Result { + let allowed_hosts = self.resolve().await?; + Ok(spin_outbound_networking::check_url( + url, + scheme, + &allowed_hosts, + )) + } + + async fn resolve(&self) -> anyhow::Result> { + self.allowed_hosts_future.clone().await.map_err(|err| { + // TODO: better way to handle this? + anyhow::Error::msg(err) + }) + } +} diff --git a/crates/factor-variables/Cargo.toml b/crates/factor-variables/Cargo.toml new file mode 100644 index 0000000000..5e5cf55438 --- /dev/null +++ b/crates/factor-variables/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "spin-factor-variables" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +serde = { version = "1.0", features = ["rc"] } +spin-expressions = { path = "../expressions" } +spin-factors = { path = "../factors" } +spin-world = { path = "../world" } +toml = "0.8" + +[lints] +workspace = true diff --git a/crates/factor-variables/src/lib.rs b/crates/factor-variables/src/lib.rs new file mode 100644 index 0000000000..498e96c890 --- /dev/null +++ b/crates/factor-variables/src/lib.rs @@ -0,0 +1,172 @@ +mod provider; + +use std::{collections::HashMap, sync::Arc}; + +use provider::{provider_from_toml_fn, ProviderFromToml}; +use serde::Deserialize; +use spin_expressions::ProviderResolver; +use spin_factors::{ + anyhow::{self, bail, Context}, + ConfigureAppContext, Factor, FactorRuntimeConfig, InitContext, InstanceBuilders, + PrepareContext, RuntimeFactors, SelfInstanceBuilder, +}; +use spin_world::{async_trait, v1, v2::variables}; + +pub use provider::{MakeVariablesProvider, StaticVariables}; + +#[derive(Default)] +pub struct VariablesFactor { + provider_types: HashMap<&'static str, ProviderFromToml>, +} + +impl VariablesFactor { + pub fn add_provider_type( + &mut self, + provider_type: T, + ) -> anyhow::Result<()> { + if self + .provider_types + .insert(T::RUNTIME_CONFIG_TYPE, provider_from_toml_fn(provider_type)) + .is_some() + { + bail!("duplicate provider type {:?}", T::RUNTIME_CONFIG_TYPE); + } + Ok(()) + } +} + +impl Factor for VariablesFactor { + type RuntimeConfig = RuntimeConfig; + type AppState = AppState; + type InstanceBuilder = InstanceState; + + fn init( + &mut self, + mut ctx: InitContext, + ) -> anyhow::Result<()> { + ctx.link_bindings(spin_world::v1::config::add_to_linker)?; + ctx.link_bindings(spin_world::v2::variables::add_to_linker)?; + Ok(()) + } + + fn configure_app( + &self, + mut ctx: ConfigureAppContext, + ) -> anyhow::Result { + let app = ctx.app(); + let mut resolver = + ProviderResolver::new(app.variables().map(|(key, val)| (key.clone(), val.clone())))?; + + for component in app.components() { + resolver.add_component_variables( + component.id(), + component.config().map(|(k, v)| (k.into(), v.into())), + )?; + } + + if let Some(runtime_config) = ctx.take_runtime_config() { + for ProviderConfig { type_, config } in runtime_config.provider_configs { + let provider_maker = self + .provider_types + .get(type_.as_str()) + .with_context(|| format!("unknown variables provider type {type_:?}"))?; + let provider = provider_maker(config)?; + resolver.add_provider(provider); + } + } + + Ok(AppState { + resolver: Arc::new(resolver), + }) + } + + fn prepare( + &self, + ctx: PrepareContext, + _builders: &mut InstanceBuilders, + ) -> anyhow::Result { + let component_id = ctx.app_component().id().to_string(); + let resolver = ctx.app_state().resolver.clone(); + Ok(InstanceState { + component_id, + resolver, + }) + } +} + +#[derive(Deserialize)] +#[serde(transparent)] +pub struct RuntimeConfig { + provider_configs: Vec, +} + +impl FactorRuntimeConfig for RuntimeConfig { + const KEY: &'static str = "variable_provider"; +} + +#[derive(Deserialize)] +struct ProviderConfig { + #[serde(rename = "type")] + type_: String, + #[serde(flatten)] + config: toml::Table, +} + +pub struct AppState { + resolver: Arc, +} + +pub struct InstanceState { + component_id: String, + resolver: Arc, +} + +impl InstanceState { + pub fn resolver(&self) -> &Arc { + &self.resolver + } +} + +impl SelfInstanceBuilder for InstanceState {} + +#[async_trait] +impl variables::Host for InstanceState { + async fn get(&mut self, key: String) -> Result { + let key = spin_expressions::Key::new(&key).map_err(expressions_to_variables_err)?; + self.resolver + .resolve(&self.component_id, key) + .await + .map_err(expressions_to_variables_err) + } + + fn convert_error(&mut self, error: variables::Error) -> anyhow::Result { + Ok(error) + } +} + +#[async_trait] +impl v1::config::Host for InstanceState { + async fn get_config(&mut self, key: String) -> Result { + ::get(self, key) + .await + .map_err(|err| match err { + variables::Error::InvalidName(msg) => v1::config::Error::InvalidKey(msg), + variables::Error::Undefined(msg) => v1::config::Error::Provider(msg), + other => v1::config::Error::Other(format!("{other}")), + }) + } + + fn convert_error(&mut self, err: v1::config::Error) -> anyhow::Result { + Ok(err) + } +} + +fn expressions_to_variables_err(err: spin_expressions::Error) -> variables::Error { + use spin_expressions::Error; + match err { + Error::InvalidName(msg) => variables::Error::InvalidName(msg), + Error::Undefined(msg) => variables::Error::Undefined(msg), + Error::Provider(err) => variables::Error::Provider(err.to_string()), + other => variables::Error::Other(format!("{other}")), + } +} diff --git a/crates/factor-variables/src/provider.rs b/crates/factor-variables/src/provider.rs new file mode 100644 index 0000000000..f3ee948118 --- /dev/null +++ b/crates/factor-variables/src/provider.rs @@ -0,0 +1,51 @@ +use std::{collections::HashMap, sync::Arc}; + +use serde::{de::DeserializeOwned, Deserialize}; +use spin_expressions::{async_trait::async_trait, Key, Provider}; +use spin_factors::anyhow; + +pub trait MakeVariablesProvider: 'static { + const RUNTIME_CONFIG_TYPE: &'static str; + + type RuntimeConfig: DeserializeOwned; + type Provider: Provider; + + fn make_provider(&self, runtime_config: Self::RuntimeConfig) -> anyhow::Result; +} + +pub(crate) type ProviderFromToml = Box anyhow::Result>>; + +pub(crate) fn provider_from_toml_fn( + provider_type: T, +) -> ProviderFromToml { + Box::new(move |table| { + let runtime_config: T::RuntimeConfig = table.try_into()?; + let provider = provider_type.make_provider(runtime_config)?; + Ok(Box::new(provider)) + }) +} + +pub struct StaticVariables; + +impl MakeVariablesProvider for StaticVariables { + const RUNTIME_CONFIG_TYPE: &'static str = "static"; + + type RuntimeConfig = StaticVariablesProvider; + type Provider = StaticVariablesProvider; + + fn make_provider(&self, runtime_config: Self::RuntimeConfig) -> anyhow::Result { + Ok(runtime_config) + } +} + +#[derive(Debug, Deserialize)] +pub struct StaticVariablesProvider { + values: Arc>, +} + +#[async_trait] +impl Provider for StaticVariablesProvider { + async fn get(&self, key: &Key) -> anyhow::Result> { + Ok(self.values.get(key.as_str()).cloned()) + } +} diff --git a/crates/factor-wasi/Cargo.toml b/crates/factor-wasi/Cargo.toml new file mode 100644 index 0000000000..d1572e555a --- /dev/null +++ b/crates/factor-wasi/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "spin-factor-wasi" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +cap-primitives = "3.0.0" +spin-factors = { path = "../factors" } +wasmtime-wasi = { workspace = true } + +[lints] +workspace = true diff --git a/crates/factor-wasi/src/lib.rs b/crates/factor-wasi/src/lib.rs new file mode 100644 index 0000000000..9d80131b60 --- /dev/null +++ b/crates/factor-wasi/src/lib.rs @@ -0,0 +1,203 @@ +use std::{future::Future, net::SocketAddr, path::Path}; + +use spin_factors::{ + anyhow, AppComponent, Factor, FactorInstanceBuilder, InitContext, InstanceBuilders, + PrepareContext, RuntimeFactors, +}; +use wasmtime_wasi::{ResourceTable, WasiCtx, WasiCtxBuilder, WasiImpl, WasiView}; + +pub struct WasiFactor { + files_mounter: Box, +} + +impl WasiFactor { + pub fn new(files_mounter: impl FilesMounter + 'static) -> Self { + Self { + files_mounter: Box::new(files_mounter), + } + } +} + +impl Factor for WasiFactor { + type RuntimeConfig = (); + type AppState = (); + type InstanceBuilder = InstanceBuilder; + + fn init( + &mut self, + mut ctx: InitContext, + ) -> anyhow::Result<()> { + fn type_annotate(f: F) -> F + where + F: Fn(&mut T) -> WasiImpl<&mut U>, + { + f + } + let get_data = ctx.get_data_fn(); + let closure = type_annotate(move |data| WasiImpl(get_data(data))); + let linker = ctx.linker(); + use wasmtime_wasi::bindings; + bindings::clocks::wall_clock::add_to_linker_get_host(linker, closure)?; + bindings::clocks::monotonic_clock::add_to_linker_get_host(linker, closure)?; + bindings::filesystem::types::add_to_linker_get_host(linker, closure)?; + bindings::filesystem::preopens::add_to_linker_get_host(linker, closure)?; + bindings::io::error::add_to_linker_get_host(linker, closure)?; + bindings::io::poll::add_to_linker_get_host(linker, closure)?; + bindings::io::streams::add_to_linker_get_host(linker, closure)?; + bindings::random::random::add_to_linker_get_host(linker, closure)?; + bindings::random::insecure::add_to_linker_get_host(linker, closure)?; + bindings::random::insecure_seed::add_to_linker_get_host(linker, closure)?; + bindings::cli::exit::add_to_linker_get_host(linker, closure)?; + bindings::cli::environment::add_to_linker_get_host(linker, closure)?; + bindings::cli::stdin::add_to_linker_get_host(linker, closure)?; + bindings::cli::stdout::add_to_linker_get_host(linker, closure)?; + bindings::cli::stderr::add_to_linker_get_host(linker, closure)?; + bindings::cli::terminal_input::add_to_linker_get_host(linker, closure)?; + bindings::cli::terminal_output::add_to_linker_get_host(linker, closure)?; + bindings::cli::terminal_stdin::add_to_linker_get_host(linker, closure)?; + bindings::cli::terminal_stdout::add_to_linker_get_host(linker, closure)?; + bindings::cli::terminal_stderr::add_to_linker_get_host(linker, closure)?; + bindings::sockets::tcp::add_to_linker_get_host(linker, closure)?; + bindings::sockets::tcp_create_socket::add_to_linker_get_host(linker, closure)?; + bindings::sockets::udp::add_to_linker_get_host(linker, closure)?; + bindings::sockets::udp_create_socket::add_to_linker_get_host(linker, closure)?; + bindings::sockets::instance_network::add_to_linker_get_host(linker, closure)?; + bindings::sockets::network::add_to_linker_get_host(linker, closure)?; + bindings::sockets::ip_name_lookup::add_to_linker_get_host(linker, closure)?; + Ok(()) + } + + fn configure_app( + &self, + _ctx: spin_factors::ConfigureAppContext, + ) -> anyhow::Result { + Ok(()) + } + + fn prepare( + &self, + ctx: PrepareContext, + _builders: &mut InstanceBuilders, + ) -> anyhow::Result { + let mut wasi_ctx = WasiCtxBuilder::new(); + + // Apply environment variables + for (key, val) in ctx.app_component().environment() { + wasi_ctx.env(key, val); + } + + // Mount files + let mount_ctx = MountFilesContext { + wasi_ctx: &mut wasi_ctx, + }; + self.files_mounter + .mount_files(ctx.app_component(), mount_ctx)?; + + Ok(InstanceBuilder { wasi_ctx }) + } +} + +pub trait FilesMounter { + fn mount_files( + &self, + app_component: &AppComponent, + ctx: MountFilesContext, + ) -> anyhow::Result<()>; +} + +pub struct DummyFilesMounter; + +impl FilesMounter for DummyFilesMounter { + fn mount_files( + &self, + app_component: &AppComponent, + _ctx: MountFilesContext, + ) -> anyhow::Result<()> { + anyhow::ensure!( + app_component.files().next().is_none(), + "DummyFilesMounter can't actually mount files" + ); + Ok(()) + } +} + +pub struct MountFilesContext<'a> { + wasi_ctx: &'a mut WasiCtxBuilder, +} + +impl<'a> MountFilesContext<'a> { + pub fn preopened_dir( + &mut self, + host_path: impl AsRef, + guest_path: impl AsRef, + writable: bool, + ) -> anyhow::Result<()> { + use wasmtime_wasi::{DirPerms, FilePerms}; + let (dir_perms, file_perms) = if writable { + (DirPerms::all(), FilePerms::all()) + } else { + (DirPerms::READ, FilePerms::READ) + }; + self.wasi_ctx + .preopened_dir(host_path, guest_path, dir_perms, file_perms)?; + Ok(()) + } +} + +pub struct InstanceBuilder { + wasi_ctx: WasiCtxBuilder, +} + +impl FactorInstanceBuilder for InstanceBuilder { + type InstanceState = InstanceState; + + fn build(self) -> anyhow::Result { + let InstanceBuilder { mut wasi_ctx } = self; + Ok(InstanceState { + ctx: wasi_ctx.build(), + table: Default::default(), + }) + } +} + +impl InstanceBuilder { + pub fn outbound_socket_addr_check(&mut self, check: F) + where + F: Fn(SocketAddr) -> Fut + Send + Sync + Clone + 'static, + Fut: Future + Send + Sync, + { + self.wasi_ctx.socket_addr_check(move |addr, addr_use| { + let check = check.clone(); + Box::pin(async move { + match addr_use { + wasmtime_wasi::SocketAddrUse::TcpBind => false, + wasmtime_wasi::SocketAddrUse::TcpConnect + | wasmtime_wasi::SocketAddrUse::UdpBind + | wasmtime_wasi::SocketAddrUse::UdpConnect + | wasmtime_wasi::SocketAddrUse::UdpOutgoingDatagram => check(addr).await, + } + }) + }); + } +} + +pub struct InstanceState { + ctx: WasiCtx, + table: ResourceTable, +} + +impl InstanceState { + pub fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} + +impl WasiView for InstanceState { + fn ctx(&mut self) -> &mut WasiCtx { + &mut self.ctx + } + + fn table(&mut self) -> &mut ResourceTable { + &mut self.table + } +} diff --git a/crates/factors-derive/Cargo.toml b/crates/factors-derive/Cargo.toml new file mode 100644 index 0000000000..859a3c3290 --- /dev/null +++ b/crates/factors-derive/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "spin-factors-derive" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[lib] +proc-macro = true + +[features] +expander = ["dep:expander"] + +[dependencies] +expander = { version = "2.1.0", optional = true } +proc-macro2 = "1.0.79" +quote = "1.0.35" +syn = "2.0.52" + +[lints] +workspace = true diff --git a/crates/factors-derive/src/lib.rs b/crates/factors-derive/src/lib.rs new file mode 100644 index 0000000000..0b980570f6 --- /dev/null +++ b/crates/factors-derive/src/lib.rs @@ -0,0 +1,211 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{parse_macro_input, Data, DeriveInput, Error}; + +#[proc_macro_derive(RuntimeFactors)] +pub fn derive_factors(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = parse_macro_input!(input as DeriveInput); + let expanded = expand_factors(&input).unwrap_or_else(|err| err.into_compile_error()); + + #[cfg(feature = "expander")] + let expanded = expander::Expander::new("factors") + .write_to_out_dir(expanded) + .unwrap(); + + expanded.into() +} + +#[allow(non_snake_case)] +fn expand_factors(input: &DeriveInput) -> syn::Result { + let name = &input.ident; + let vis = &input.vis; + + let app_state_name = format_ident!("{name}AppState"); + let builders_name = format_ident!("{name}InstanceBuilders"); + let state_name = format_ident!("{name}InstanceState"); + + if !input.generics.params.is_empty() { + return Err(Error::new_spanned( + input, + "cannot derive Factors for generic structs", + )); + } + + // Get struct fields + let fields = match &input.data { + Data::Struct(struct_data) => &struct_data.fields, + _ => { + return Err(Error::new_spanned( + input, + "can only derive Factors for structs", + )) + } + }; + let mut factor_names = Vec::with_capacity(fields.len()); + let mut factor_types = Vec::with_capacity(fields.len()); + for field in fields.iter() { + factor_names.push( + field + .ident + .as_ref() + .ok_or_else(|| Error::new_spanned(input, "tuple structs are not supported"))?, + ); + factor_types.push(&field.ty); + } + + let Any = quote!(::std::any::Any); + let TypeId = quote!(::std::any::TypeId); + let factors_crate = format_ident!("spin_factors"); + let factors_path = quote!(::#factors_crate); + let field_offset = quote!(#factors_path::__internal::field_offset); + let wasmtime = quote!(#factors_path::wasmtime); + let Result = quote!(#factors_path::Result); + let Factor = quote!(#factors_path::Factor); + let ConfiguredApp = quote!(#factors_path::ConfiguredApp); + let RuntimeConfigTracker = quote!(#factors_path::__internal::RuntimeConfigTracker); + let FactorInstanceBuilder = quote!(#factors_path::FactorInstanceBuilder); + + Ok(quote! { + impl #name { + #[allow(clippy::needless_option_as_deref)] + pub fn init( + &mut self, + linker: &mut #wasmtime::component::Linker<#state_name>, + ) -> #Result<()> { + #( + #Factor::init::( + &mut self.#factor_names, + #factors_path::InitContext::::new( + linker, + |state| &mut state.#factor_names, + ) + )?; + )* + Ok(()) + } + + pub fn configure_app( + &self, + app: #factors_path::App, + runtime_config: impl #factors_path::RuntimeConfigSource + ) -> #Result<#ConfiguredApp> { + let mut app_state = #app_state_name { + #( #factor_names: None, )* + }; + let mut runtime_config_tracker = #RuntimeConfigTracker::new(runtime_config); + #( + app_state.#factor_names = Some( + #Factor::configure_app( + &self.#factor_names, + #factors_path::ConfigureAppContext::::new( + &app, + &app_state, + &mut runtime_config_tracker, + )?, + )? + ); + )* + runtime_config_tracker.validate_all_keys_used()?; + Ok(#ConfiguredApp::new(app, app_state)) + } + + pub fn build_store_data(&self, configured_app: &#ConfiguredApp, component_id: &str) -> #Result<#state_name> { + let app_component = configured_app.app().get_component(component_id).ok_or_else(|| { + #wasmtime::Error::msg("unknown component") + })?; + let mut builders = #builders_name { + #( #factor_names: None, )* + }; + #( + builders.#factor_names = Some( + #Factor::prepare::( + &self.#factor_names, + #factors_path::PrepareContext::new( + configured_app.app_state::<#factor_types>().unwrap(), + &app_component, + ), + &mut #factors_path::InstanceBuilders::new(&mut builders), + )? + ); + )* + Ok(#state_name { + #( + #factor_names: #FactorInstanceBuilder::build( + builders.#factor_names.unwrap(), + )?, + )* + }) + } + + } + + impl #factors_path::RuntimeFactors for #name { + type AppState = #app_state_name; + type InstanceBuilders = #builders_name; + type InstanceState = #state_name; + + fn app_state(app_state: &Self::AppState) -> Option<&F::AppState> { + #( + if let Some(state) = &app_state.#factor_names { + if let Some(state) = ::downcast_ref(state) { + return Some(state) + } + } + )* + None + } + + fn instance_builder_mut( + builders: &mut Self::InstanceBuilders, + ) -> Option> { + let type_id = #TypeId::of::(); + #( + if type_id == #TypeId::of::<#factor_types>() { + return Some( + builders.#factor_names.as_mut().map(|builder| { + ::downcast_mut(builder).unwrap() + }) + ); + } + )* + None + } + + fn instance_state_offset() -> Option< + #field_offset::FieldOffset< + Self::InstanceState, + <::InstanceBuilder as #FactorInstanceBuilder>::InstanceState, + > + > { + let type_id = #TypeId::of::(); + #( + if type_id == #TypeId::of::<#factor_types>() { + let offset = #field_offset::offset_of!(Self::InstanceState => #factor_names); + return Some( + unsafe { ::std::mem::transmute(offset) } + ); + } + )* + None + } + } + + #vis struct #app_state_name { + #( + pub #factor_names: Option<<#factor_types as #Factor>::AppState>, + )* + } + + #vis struct #builders_name { + #( + pub #factor_names: Option<<#factor_types as #Factor>::InstanceBuilder>, + )* + } + + #vis struct #state_name { + #( + pub #factor_names: <<#factor_types as #Factor>::InstanceBuilder as #FactorInstanceBuilder>::InstanceState, + )* + } + }) +} diff --git a/crates/factors/Cargo.toml b/crates/factors/Cargo.toml new file mode 100644 index 0000000000..7a2e5f1d7f --- /dev/null +++ b/crates/factors/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "spin-factors" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } + +[dependencies] +anyhow = "1.0" +field-offset = "0.3.6" +serde = "1.0" +spin-app = { path = "../app" } +spin-factors-derive = { path = "../factors-derive" } +thiserror = "1.0" +tracing = { workspace = true } +wasmtime = { workspace = true } + +[dev-dependencies] +http = "1.1.0" +http-body-util = "0.1.2" +serde_json = "1.0" +spin-componentize = { path = "../componentize" } +spin-factors-derive = { path = "../factors-derive", features = ["expander"] } +spin-factor-key-value = { path = "../factor-key-value" } +spin-factor-outbound-http = { path = "../factor-outbound-http" } +spin-factor-outbound-networking = { path = "../factor-outbound-networking" } +spin-factor-variables = { path = "../factor-variables" } +spin-factor-wasi = { path = "../factor-wasi" } +spin-key-value-sqlite = { path = "../key-value-sqlite" } +spin-loader = { path = "../loader" } +tokio = { version = "1", features = ["macros", "rt", "sync"] } +toml = "0.8" +wasmtime-wasi-http = { workspace = true } + +[lints] +workspace = true diff --git a/crates/factors/src/factor.rs b/crates/factors/src/factor.rs new file mode 100644 index 0000000000..a3d0747951 --- /dev/null +++ b/crates/factors/src/factor.rs @@ -0,0 +1,154 @@ +use std::{any::Any, collections::HashMap}; + +use anyhow::Context; + +use crate::{ + prepare::FactorInstanceBuilder, runtime_config::RuntimeConfigTracker, App, FactorRuntimeConfig, + InstanceBuilders, Linker, PrepareContext, RuntimeConfigSource, RuntimeFactors, +}; + +pub trait Factor: Any + Sized { + type RuntimeConfig: FactorRuntimeConfig; + + type AppState; + + type InstanceBuilder: FactorInstanceBuilder; + + /// Initializes this Factor for a runtime. This will be called at most once, + /// before any call to [`FactorInstancePreparer::new`] + fn init(&mut self, mut ctx: InitContext) -> anyhow::Result<()> { + // TODO: Should `ctx` always be immut? Rename this param/type? + _ = &mut ctx; + Ok(()) + } + + /// Performs factor-specific validation and configuration for the given + /// [`App`]. + /// + /// A runtime may - but is not required to - reuse the returned config + /// across multiple instances. Note that this may be called without any call + /// to `init` in cases where only validation is needed. + fn configure_app( + &self, + ctx: ConfigureAppContext, + ) -> anyhow::Result; + + /// Prepares an instance builder for this factor. + /// + /// This method is given access to the app component being instantiated and + /// to any other factors' instance builders that have already been prepared. + fn prepare( + &self, + ctx: PrepareContext, + _builders: &mut InstanceBuilders, + ) -> anyhow::Result; + + /// Returns [JSON Schema](https://json-schema.org/) for this factor's + /// runtime config. + /// + /// Note that this represents only a fragment of an entire JSON document (a + /// "child instance" in JSON Schema terms), so `$schema` isn't needed. + /// + /// The default implementation returns an empty schema, which accepts any + /// configuration. + fn runtime_config_json_schema(&self) -> impl serde::Serialize { + HashMap::<(), ()>::new() + } +} + +pub(crate) type FactorInstanceState = + <::InstanceBuilder as FactorInstanceBuilder>::InstanceState; + +pub(crate) type GetDataFn = + fn(&mut ::InstanceState) -> &mut FactorInstanceState; + +/// An InitContext is passed to [`Factor::init`], giving access to the global +/// common [`wasmtime::component::Linker`]. +pub struct InitContext<'a, T: RuntimeFactors, F: Factor> { + pub(crate) linker: &'a mut Linker, + pub(crate) get_data: GetDataFn, +} + +impl<'a, T: RuntimeFactors, F: Factor> InitContext<'a, T, F> { + #[doc(hidden)] + pub fn new(linker: &'a mut Linker, get_data: GetDataFn) -> Self { + Self { linker, get_data } + } + + pub fn linker(&mut self) -> &mut Linker { + self.linker + } + + pub fn get_data_fn(&self) -> GetDataFn { + self.get_data + } + + pub fn link_bindings( + &mut self, + add_to_linker: impl Fn( + &mut Linker, + fn(&mut T::InstanceState) -> &mut FactorInstanceState, + ) -> anyhow::Result<()>, + ) -> anyhow::Result<()> +where { + add_to_linker(self.linker, self.get_data) + } +} + +pub struct ConfigureAppContext<'a, T: RuntimeFactors, F: Factor> { + app: &'a App, + app_state: &'a T::AppState, + runtime_config: Option, +} + +impl<'a, T: RuntimeFactors, F: Factor> ConfigureAppContext<'a, T, F> { + #[doc(hidden)] + pub fn new( + app: &'a App, + app_state: &'a T::AppState, + runtime_config_tracker: &mut RuntimeConfigTracker, + ) -> anyhow::Result { + let runtime_config = runtime_config_tracker.get_config::()?; + Ok(Self { + app, + app_state, + runtime_config, + }) + } + + pub fn app(&self) -> &App { + self.app + } + + pub fn app_state(&self) -> crate::Result<&U::AppState> { + T::app_state::(self.app_state).context("no such factor") + } + + pub fn runtime_config(&self) -> Option<&F::RuntimeConfig> { + self.runtime_config.as_ref() + } + + pub fn take_runtime_config(&mut self) -> Option { + self.runtime_config.take() + } +} + +pub struct ConfiguredApp { + app: App, + app_state: T::AppState, +} + +impl ConfiguredApp { + #[doc(hidden)] + pub fn new(app: App, app_state: T::AppState) -> Self { + Self { app, app_state } + } + + pub fn app(&self) -> &App { + &self.app + } + + pub fn app_state(&self) -> crate::Result<&F::AppState> { + T::app_state::(&self.app_state).context("no such factor") + } +} diff --git a/crates/factors/src/lib.rs b/crates/factors/src/lib.rs new file mode 100644 index 0000000000..bfdbf0f54e --- /dev/null +++ b/crates/factors/src/lib.rs @@ -0,0 +1,31 @@ +mod factor; +mod prepare; +mod runtime_config; +mod runtime_factors; + +pub use anyhow; +pub use wasmtime; + +pub use spin_factors_derive::RuntimeFactors; + +pub use crate::{ + factor::{ConfigureAppContext, ConfiguredApp, Factor, InitContext}, + prepare::{FactorInstanceBuilder, InstanceBuilders, PrepareContext, SelfInstanceBuilder}, + runtime_config::{FactorRuntimeConfig, RuntimeConfigSource}, + runtime_factors::RuntimeFactors, +}; + +pub type Linker = wasmtime::component::Linker<::InstanceState>; + +// Temporary wrappers while refactoring +pub type App = spin_app::App<'static, spin_app::InertLoader>; +pub type AppComponent<'a> = spin_app::AppComponent<'a, spin_app::InertLoader>; + +// TODO: Add a real Error type +pub type Result = wasmtime::Result; + +#[doc(hidden)] +pub mod __internal { + pub use crate::runtime_config::RuntimeConfigTracker; + pub use field_offset; +} diff --git a/crates/factors/src/prepare.rs b/crates/factors/src/prepare.rs new file mode 100644 index 0000000000..7e3c7f6d6e --- /dev/null +++ b/crates/factors/src/prepare.rs @@ -0,0 +1,77 @@ +use std::any::Any; + +use anyhow::Context; + +use crate::{AppComponent, Factor, RuntimeFactors}; + +pub trait FactorInstanceBuilder: Any { + type InstanceState: Send + 'static; + + fn build(self) -> anyhow::Result; +} + +impl FactorInstanceBuilder for () { + type InstanceState = (); + + fn build(self) -> anyhow::Result { + Ok(()) + } +} + +pub trait SelfInstanceBuilder: Send + 'static {} + +impl FactorInstanceBuilder for T { + type InstanceState = Self; + + fn build(self) -> anyhow::Result { + Ok(self) + } +} + +/// A PrepareContext is passed to [`Factor::prepare`], giving access to any +/// already-initialized [`FactorInstanceBuilder`]s, allowing for +/// inter-[`Factor`] dependencies. +pub struct PrepareContext<'a, F: Factor> { + pub(crate) app_state: &'a F::AppState, + pub(crate) app_component: &'a AppComponent<'a>, +} + +impl<'a, F: Factor> PrepareContext<'a, F> { + #[doc(hidden)] + pub fn new(app_state: &'a F::AppState, app_component: &'a AppComponent) -> Self { + Self { + app_state, + app_component, + } + } + + pub fn app_state(&self) -> &F::AppState { + self.app_state + } + + pub fn app_component(&self) -> &AppComponent { + self.app_component + } +} + +pub struct InstanceBuilders<'a, T: RuntimeFactors> { + pub(crate) inner: &'a mut T::InstanceBuilders, +} + +impl<'a, T: RuntimeFactors> InstanceBuilders<'a, T> { + #[doc(hidden)] + pub fn new(inner: &'a mut T::InstanceBuilders) -> Self { + Self { inner } + } + + /// Returns the prepared [`FactorInstanceBuilder`] for the given [`Factor`]. + /// + /// Fails if the current [`RuntimeFactors`] does not include the given + /// [`Factor`] or if the given [`Factor`]'s builder has not been prepared + /// yet (because it is sequenced after this factor). + pub fn get_mut(&mut self) -> crate::Result<&mut F::InstanceBuilder> { + T::instance_builder_mut::(self.inner) + .context("no such factor")? + .context("builder not prepared") + } +} diff --git a/crates/factors/src/runtime_config.rs b/crates/factors/src/runtime_config.rs new file mode 100644 index 0000000000..372c9ffe69 --- /dev/null +++ b/crates/factors/src/runtime_config.rs @@ -0,0 +1,101 @@ +use std::collections::HashSet; + +use anyhow::bail; +use serde::de::DeserializeOwned; + +use crate::Factor; + +pub const NO_RUNTIME_CONFIG: &str = ""; + +/// FactorRuntimeConfig represents an application's runtime configuration. +/// +/// Runtime configuration is partitioned, with each partition being the +/// responsibility of exactly one [`crate::Factor`]. If configuration needs +/// to be shared between Factors, one Factor can be selected as the owner +/// and the others will have a dependency relationship with that owner. +pub trait FactorRuntimeConfig: DeserializeOwned { + const KEY: &'static str; +} + +impl FactorRuntimeConfig for () { + const KEY: &'static str = NO_RUNTIME_CONFIG; +} + +pub trait RuntimeConfigSource { + /// Returns an iterator of factor config keys available in this source. + /// + /// Should only include keys that have been positively provided and that + /// haven't already been parsed by the runtime. A runtime may treat + /// unrecognized keys as a warning or error. + fn factor_config_keys(&self) -> impl IntoIterator; + + /// Returns deserialized runtime config of the given type for the given + /// factor config key. + /// + /// Returns Ok(None) if no configuration is available for the given key. + /// Returns Err if configuration is available but deserialization fails. + fn get_factor_config(&self, key: &str) -> anyhow::Result>; +} + +impl RuntimeConfigSource for () { + fn get_factor_config( + &self, + _factor_config_key: &str, + ) -> anyhow::Result> { + Ok(None) + } + + fn factor_config_keys(&self) -> impl IntoIterator { + std::iter::empty() + } +} + +pub struct RuntimeConfigTracker { + source: S, + used_keys: HashSet<&'static str>, + unused_keys: HashSet, +} + +impl RuntimeConfigTracker { + #[doc(hidden)] + pub fn new(source: S) -> Self { + let unused_keys = source + .factor_config_keys() + .into_iter() + .map(ToOwned::to_owned) + .collect(); + Self { + source, + used_keys: Default::default(), + unused_keys, + } + } + + #[doc(hidden)] + pub fn validate_all_keys_used(self) -> anyhow::Result<()> { + if !self.unused_keys.is_empty() { + bail!( + "unused runtime config key(s): {keys}", + keys = self + .unused_keys + .iter() + .map(|key| format!("{key:?}")) + .collect::>() + .join(", ") + ); + } + Ok(()) + } + + pub fn get_config(&mut self) -> anyhow::Result> { + let key = F::RuntimeConfig::KEY; + if key == NO_RUNTIME_CONFIG { + return Ok(None); + } + if !self.used_keys.insert(key) { + bail!("already used runtime config key {key:?}"); + } + self.unused_keys.remove(key); + self.source.get_factor_config::(key) + } +} diff --git a/crates/factors/src/runtime_factors.rs b/crates/factors/src/runtime_factors.rs new file mode 100644 index 0000000000..73f746039b --- /dev/null +++ b/crates/factors/src/runtime_factors.rs @@ -0,0 +1,101 @@ +use field_offset::FieldOffset; + +use crate::{factor::FactorInstanceState, Factor}; + +/// Implemented by `#[derive(RuntimeFactors)]` +pub trait RuntimeFactors: Sized + 'static { + type AppState; + type InstanceBuilders; + type InstanceState: Send + 'static; + + fn app_state(app_state: &Self::AppState) -> Option<&F::AppState>; + + fn instance_builder_mut( + builders: &mut Self::InstanceBuilders, + ) -> Option>; + + #[doc(hidden)] + fn instance_state_offset( + ) -> Option>>; + + fn instance_state_getter() -> Option> { + StateGetter::new() + } + + fn instance_state_getter2() -> Option> { + StateGetter2::new() + } +} + +pub struct StateGetter { + offset: FieldOffset>, +} + +impl StateGetter { + fn new() -> Option { + Some(Self { + offset: T::instance_state_offset::()?, + }) + } + + pub fn get_state<'a>( + &self, + instance_state: &'a mut T::InstanceState, + ) -> &'a mut FactorInstanceState { + self.offset.apply_mut(instance_state) + } +} + +impl Clone for StateGetter { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StateGetter {} + +pub struct StateGetter2 { + // Invariant: offsets must point at non-overlapping objects + offset1: FieldOffset>, + offset2: FieldOffset>, +} + +impl StateGetter2 { + fn new() -> Option { + let offset1 = T::instance_state_offset::()?; + let offset2 = T::instance_state_offset::()?; + // Make sure the two states don't point to the same field + if offset1.get_byte_offset() == offset2.get_byte_offset() { + return None; + } + Some(StateGetter2 { offset1, offset2 }) + } + + pub fn get_states<'a>( + &self, + instance_state: &'a mut T::InstanceState, + ) -> ( + &'a mut FactorInstanceState, + &'a mut FactorInstanceState, + ) { + let ptr = instance_state as *mut T::InstanceState; + unsafe { + ( + &mut *(self.offset1.apply_ptr_mut(ptr) as *mut FactorInstanceState), + &mut *(self.offset2.apply_ptr_mut(ptr) as *mut FactorInstanceState), + ) + } + } +} + +impl Clone for StateGetter2 { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for StateGetter2 {} + +// TODO: This seems fine, but then again I don't understand why `FieldOffset`'s +// own `Sync`ness depends on `U`... +unsafe impl Sync for StateGetter2 {} diff --git a/crates/factors/tests/smoke-app/.gitignore b/crates/factors/tests/smoke-app/.gitignore new file mode 100644 index 0000000000..386474fa59 --- /dev/null +++ b/crates/factors/tests/smoke-app/.gitignore @@ -0,0 +1,2 @@ +target/ +.spin/ diff --git a/crates/factors/tests/smoke-app/Cargo.toml b/crates/factors/tests/smoke-app/Cargo.toml new file mode 100644 index 0000000000..d4fc36af1b --- /dev/null +++ b/crates/factors/tests/smoke-app/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "smoke-app" +authors = ["Lann Martin "] +description = "" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +anyhow = "1" +spin-sdk = "3.0.1" + +[workspace] diff --git a/crates/factors/tests/smoke-app/spin.toml b/crates/factors/tests/smoke-app/spin.toml new file mode 100644 index 0000000000..a800b49fe9 --- /dev/null +++ b/crates/factors/tests/smoke-app/spin.toml @@ -0,0 +1,25 @@ +spin_manifest_version = 2 + +[application] +name = "smoke-app" +version = "0.1.0" +authors = ["Lann Martin "] +description = "" + +[variables] +host = { required = true } +other = { default = "other value" } + +[[trigger.http]] +route = "/..." +component = "smoke-app" + +[component.smoke-app] +source = "target/wasm32-wasi/release/smoke_app.wasm" +allowed_outbound_hosts = ["https://{{ host }}"] +key_value_stores = ["default"] +variables = { "other" = "<{{ other }}>"} + +[component.smoke-app.build] +command = "cargo build --target wasm32-wasi --release" +watch = ["src/**/*.rs", "Cargo.toml"] diff --git a/crates/factors/tests/smoke-app/src/lib.rs b/crates/factors/tests/smoke-app/src/lib.rs new file mode 100644 index 0000000000..214373d293 --- /dev/null +++ b/crates/factors/tests/smoke-app/src/lib.rs @@ -0,0 +1,19 @@ +use spin_sdk::http::{IntoResponse, Request, Response}; +use spin_sdk::http_component; + +/// A simple Spin HTTP component. +#[http_component] +fn handle_smoke_app(_req: Request) -> anyhow::Result { + let var_val = spin_sdk::variables::get("other")?; + let kv_val = { + let store = spin_sdk::key_value::Store::open_default()?; + store.set("k", b"v")?; + store.get("k")? + }; + let body = format!("Test response\nVariable: {var_val}\nKV: {kv_val:?}"); + Ok(Response::builder() + .status(200) + .header("content-type", "text/plain") + .body(body) + .build()) +} diff --git a/crates/factors/tests/smoke.rs b/crates/factors/tests/smoke.rs new file mode 100644 index 0000000000..dcbc043f20 --- /dev/null +++ b/crates/factors/tests/smoke.rs @@ -0,0 +1,162 @@ +use std::path::PathBuf; + +use anyhow::bail; +use http_body_util::BodyExt; +use serde::Deserialize; +use spin_app::App; +use spin_factor_key_value::{KeyValueFactor, MakeKeyValueStore}; +use spin_factor_outbound_http::OutboundHttpFactor; +use spin_factor_outbound_networking::OutboundNetworkingFactor; +use spin_factor_variables::{StaticVariables, VariablesFactor}; +use spin_factor_wasi::{DummyFilesMounter, WasiFactor}; +use spin_factors::{FactorRuntimeConfig, RuntimeConfigSource, RuntimeFactors}; +use spin_key_value_sqlite::{DatabaseLocation, KeyValueSqlite}; +use wasmtime_wasi_http::WasiHttpView; + +#[derive(RuntimeFactors)] +struct Factors { + wasi: WasiFactor, + variables: VariablesFactor, + outbound_networking: OutboundNetworkingFactor, + outbound_http: OutboundHttpFactor, + key_value: KeyValueFactor, +} + +#[tokio::test(flavor = "multi_thread")] +async fn main() -> anyhow::Result<()> { + let mut factors = Factors { + wasi: WasiFactor::new(DummyFilesMounter), + variables: VariablesFactor::default(), + outbound_networking: OutboundNetworkingFactor, + outbound_http: OutboundHttpFactor, + key_value: KeyValueFactor::default(), + }; + + factors.variables.add_provider_type(StaticVariables)?; + + factors.key_value.add_store_type(TestSpinKeyValueStore)?; + + let locked = spin_loader::from_file( + "tests/smoke-app/spin.toml", + spin_loader::FilesMountStrategy::Direct, + None, + ) + .await?; + let app = App::inert(locked); + + let engine = wasmtime::Engine::new(wasmtime::Config::new().async_support(true))?; + let mut linker = wasmtime::component::Linker::new(&engine); + + factors.init(&mut linker).unwrap(); + + let configured_app = factors.configure_app(app, TestSource)?; + let data = factors.build_store_data(&configured_app, "smoke-app")?; + + assert_eq!( + data.variables + .resolver() + .resolve("smoke-app", "other".try_into().unwrap()) + .await + .unwrap(), + "" + ); + + let mut store = wasmtime::Store::new(&engine, data); + + let component = configured_app.app().components().next().unwrap(); + let wasm_path = component + .source() + .content + .source + .as_deref() + .unwrap() + .strip_prefix("file://") + .unwrap(); + let wasm_bytes = std::fs::read(wasm_path)?; + let component_bytes = spin_componentize::componentize_if_necessary(&wasm_bytes)?; + let component = wasmtime::component::Component::new(&engine, component_bytes)?; + let instance = linker.instantiate_async(&mut store, &component).await?; + + // Invoke handler + let req = http::Request::get("/").body(Default::default()).unwrap(); + let mut wasi_http_view = + spin_factor_outbound_http::get_wasi_http_view::(store.data_mut())?; + let request = wasi_http_view.new_incoming_request(req)?; + let (response_tx, response_rx) = tokio::sync::oneshot::channel(); + let response = wasi_http_view.new_response_outparam(response_tx)?; + drop(wasi_http_view); + + let guest = wasmtime_wasi_http::proxy::Proxy::new(&mut store, &instance)?; + let call_task = tokio::spawn(async move { + guest + .wasi_http_incoming_handler() + .call_handle(&mut store, request, response) + .await + }); + let resp_task = tokio::spawn(async { + let resp = response_rx.await.unwrap().unwrap(); + let body = resp.into_body().collect().await.unwrap().to_bytes(); + eprintln!("Response: {body:?}"); + }); + let (call_res, resp_res) = tokio::join!(call_task, resp_task); + let _ = call_res?; + resp_res?; + Ok(()) +} + +struct TestSource; + +impl RuntimeConfigSource for TestSource { + fn factor_config_keys(&self) -> impl IntoIterator { + [spin_factor_variables::RuntimeConfig::KEY] + } + + fn get_factor_config( + &self, + key: &str, + ) -> anyhow::Result> { + let Some(table) = toml::toml! { + [[variable_provider]] + type = "static" + [variable_provider.values] + foo = "bar" + + [key_value_store.default] + type = "spin" + } + .remove(key) else { + return Ok(None); + }; + let config = table.try_into()?; + Ok(Some(config)) + } +} + +struct TestSpinKeyValueStore; + +impl MakeKeyValueStore for TestSpinKeyValueStore { + const RUNTIME_CONFIG_TYPE: &'static str = "spin"; + + type RuntimeConfig = TestSpinKeyValueRuntimeConfig; + + type StoreManager = KeyValueSqlite; + + fn make_store( + &self, + runtime_config: Self::RuntimeConfig, + ) -> anyhow::Result { + let location = match runtime_config.path { + Some(_) => { + // TODO(lann): need state_dir to derive default store path + bail!("spin key value runtime config not implemented") + } + None => DatabaseLocation::InMemory, + }; + Ok(KeyValueSqlite::new(location)) + } +} + +#[derive(Deserialize)] +struct TestSpinKeyValueRuntimeConfig { + path: Option, +} diff --git a/crates/world/Cargo.toml b/crates/world/Cargo.toml index 3c51d5b247..270b46a174 100644 --- a/crates/world/Cargo.toml +++ b/crates/world/Cargo.toml @@ -5,4 +5,5 @@ authors = { workspace = true } edition = { workspace = true } [dependencies] +async-trait = "0.1" wasmtime = { workspace = true } diff --git a/crates/world/src/lib.rs b/crates/world/src/lib.rs index 42c4d35451..d7adeb19f0 100644 --- a/crates/world/src/lib.rs +++ b/crates/world/src/lib.rs @@ -1,5 +1,7 @@ #![allow(missing_docs)] +pub use async_trait::async_trait; + wasmtime::component::bindgen!({ inline: r#" package fermyon:runtime;