Skip to content

Instantly share code, notes, and snippets.

@itowlson
Created June 21, 2023 21:18
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itowlson/f3573c35cd385aadc47211c71e07953e to your computer and use it in GitHub Desktop.
Save itowlson/f3573c35cd385aadc47211c71e07953e to your computer and use it in GitHub Desktop.
Code for minimal Spin embedding sample
[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"
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