Created
June 21, 2023 21:18
-
-
Save itowlson/f3573c35cd385aadc47211c71e07953e to your computer and use it in GitHub Desktop.
Code for minimal Spin embedding sample
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "spinhost" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
# Spin runtime | |
spin-app = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" } | |
spin-core = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" } | |
spin-oci = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" } | |
spin-trigger = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" } | |
spin-trigger-http = { git = "https://github.com/fermyon/spin", tag = "v1.3.0" } | |
# Utility packages | |
anyhow = "1.0" | |
futures = "0.3.28" | |
serde = { version = "1.0", features = ["derive"] } | |
serde_json = "1.0.82" | |
tempfile = "3.3.0" | |
tokio = { version = "1.23", features = ["full"] } | |
url = "2.4.0" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pub struct App { | |
pub reference: String, // registry reference | |
pub address: std::net::SocketAddr, // for HTTP server to listen on | |
pub state_dir: String, // where storage like key-value and SQLite lives | |
} | |
#[tokio::main] | |
async fn main() -> anyhow::Result<()> { | |
let apps = [ | |
App { | |
reference: "ghcr.io/itowlson/dioxus-test:v1".to_owned(), | |
address: "127.0.0.1:4001".parse()?, | |
state_dir: "app1".to_owned(), | |
}, | |
App { | |
reference: "ghcr.io/itowlson/dioxus-test:v2".to_owned(), | |
address: "127.0.0.1:4002".parse()?, | |
state_dir: "app2".to_owned(), | |
}, | |
]; | |
let mut running_apps = vec![]; | |
for app in &apps { | |
running_apps.push(run(app).await?); | |
} | |
let results = futures::future::join_all(running_apps).await; | |
dump_errors(&results); | |
Ok(()) | |
} | |
fn dump_errors(results: &[Result<anyhow::Result<()>, tokio::task::JoinError>]) { | |
for r in results { | |
if let Err(e) = r { | |
println!("{e:#}"); | |
} | |
if let Ok(Err(e)) = r { | |
println!("{e:#}"); | |
} | |
} | |
} | |
pub async fn run(app: &App) -> anyhow::Result<tokio::task::JoinHandle<anyhow::Result<()>>> { | |
let working_dir = tempfile::tempdir()?; | |
let locked_app = prepare_app_from_oci(&app.reference, working_dir.path()).await?; | |
let locked_url = write_locked_app(&locked_app, working_dir.path()).await?; | |
// `trigger.run` needs the trigger configuration. In the Spin CLI this is loaded | |
// as `self.run_config`; here we have to construct it from the App object. | |
let http_run_config = spin_trigger_http::CliArgs { | |
address: app.address.clone(), tls_cert: None, tls_key: None | |
}; | |
// `build` needs to know if there is any initialization for host services | |
// like key-value storage and SQLite. In the Spin CLI these are loaded as | |
// `self.key_values` and `self.sqlite_statements`; here we skip providing | |
// for initialization. | |
let init_data = spin_trigger::HostComponentInitData::default(); | |
// And now back to your regularly scheduled copy-and-pasting. | |
let loader = spin_trigger::loader::TriggerLoader::new(working_dir.path(), false); | |
let trigger = build_executor(&app, loader, locked_url, init_data).await?; | |
let run_fut = trigger.run(http_run_config); | |
let join_handle = tokio::task::spawn(async move { | |
let _wd = working_dir; // Keep the TempDir in scope! Letting it drop would delete the directory | |
run_fut.await | |
}); | |
Ok(join_handle) | |
} | |
// Plan part 1: from registry reference to LockedApp | |
use anyhow::{anyhow, Context, Result}; | |
use spin_app::locked::LockedApp; | |
use spin_oci::OciLoader; | |
use std::path::Path; | |
use url::Url; | |
async fn prepare_app_from_oci(reference: &str, working_dir: &Path) -> Result<LockedApp> { | |
let mut client = spin_oci::Client::new(false, None) | |
.await | |
.context("cannot create registry client")?; | |
OciLoader::new(working_dir) | |
.load_app(&mut client, reference) | |
.await | |
} | |
async fn write_locked_app( | |
locked_app: &LockedApp, | |
working_dir: &Path, | |
) -> Result<String, anyhow::Error> { | |
let locked_path = working_dir.join("spin.lock"); | |
let locked_app_contents = | |
serde_json::to_vec_pretty(&locked_app).context("failed to serialize locked app")?; | |
tokio::fs::write(&locked_path, locked_app_contents) | |
.await | |
.with_context(|| format!("failed to write {:?}", locked_path))?; | |
let locked_url = Url::from_file_path(&locked_path) | |
.map_err(|_| anyhow!("cannot convert to file URL: {locked_path:?}"))? | |
.to_string(); | |
Ok(locked_url) | |
} | |
// Plan part 2: from LockedApp to running server | |
use spin_app::Loader; | |
use spin_trigger::{ | |
HostComponentInitData, RuntimeConfig, TriggerExecutorBuilder, TriggerExecutor | |
}; | |
use spin_trigger_http::HttpTrigger; | |
async fn build_executor( | |
app: &App, | |
loader: impl Loader + Send + Sync + 'static, | |
locked_url: String, | |
init_data: HostComponentInitData, | |
) -> Result<HttpTrigger> { | |
let runtime_config = build_runtime_config(&app.state_dir)?; | |
let mut builder = TriggerExecutorBuilder::new(loader); | |
builder.wasmtime_config_mut().cache_config_load_default()?; | |
builder.build(locked_url, runtime_config, init_data).await | |
} | |
fn build_runtime_config(state_dir: impl Into<String>) -> Result<RuntimeConfig> { | |
let mut config = RuntimeConfig::new(None); | |
config.set_state_dir(state_dir); | |
Ok(config) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment