Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions crates/loader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ dirs = "4.0"
dunce = "1.0"
futures = "0.3.17"
glob = "0.3.0"
indexmap = { version = "1" }
itertools = "0.10.3"
lazy_static = "1.4.0"
mime_guess = { version = "2.0" }
Expand Down
41 changes: 41 additions & 0 deletions crates/loader/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ pub async fn from_file(
loader.load_file(path).await
}

/// Load a Spin locked app from a standalone Wasm file.
pub async fn from_wasm_file(wasm_path: impl AsRef<Path>) -> Result<LockedApp> {
let app_root = std::env::current_dir()?;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would feel slightly better if we used a tempdir here. It shouldn't make any difference as the feature currently works, but as features are added I wouldn't want to accidentally expose content from the cwd.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I believe you already have working_dir where this function is called; could probably just pass that through.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the directory relative to which to resolve the Wasm file reference (and, perhaps, in future, other paths). If we use working_dir then we have to copy the Wasm file first (before copying it again), and any file-related errors will show unexpected locations.

The alternative is for app_root to be the directory containing the Wasm file, and to override wasm_path to be only the name of the Wasm file (so that the relative reference resolves correctly), but I'm not sure that's going to be helpful with hypothetical future features where the user will expect paths entered on the CLI to be resolved relative to the current directory. Of course it's hard to know because we haven't defined those features yet!

We could also do a more invasive change that threads multiple "app roots" through the loader, or redefines the app root, or absolutises everything earlier. Or we can have a separate loader path for much longer than this PR does it, e.g. doing any copies needed then jumping straight to building the lockfile instead of leaning on the existing manifestful path.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds complicated. I retract my feelings.

let manifest = single_file_manifest(wasm_path)?;
let loader = LocalLoader::new(&app_root, FilesMountStrategy::Direct, None).await?;
loader.load_manifest(manifest).await
}

/// The strategy to use for mounting WASI files into a guest.
#[derive(Debug)]
pub enum FilesMountStrategy {
Expand All @@ -50,3 +58,36 @@ pub enum FilesMountStrategy {
/// patterns, and `exclude_files` are not supported.
Direct,
}

fn single_file_manifest(
wasm_path: impl AsRef<Path>,
) -> anyhow::Result<spin_manifest::schema::v2::AppManifest> {
use serde::Deserialize;

let wasm_path_str = wasm_path
.as_ref()
.to_str()
.context("Failed to stringise Wasm file path")?
.to_owned();
let app_name = wasm_path
.as_ref()
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("wasm-file")
.to_owned();

let manifest = toml::toml!(
spin_manifest_version = 2

[application]
name = app_name

[[trigger.http]]
route = "/..."
component = { source = wasm_path_str }
);

let manifest = spin_manifest::schema::v2::AppManifest::deserialize(manifest)?;

Ok(manifest)
}
2 changes: 1 addition & 1 deletion crates/loader/src/local.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ impl LocalLoader {
}

// Load the given manifest into a LockedApp, ready for execution.
async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
pub(crate) async fn load_manifest(&self, mut manifest: AppManifest) -> Result<LockedApp> {
spin_manifest::normalize::normalize_manifest(&mut manifest);

let AppManifest {
Expand Down
4 changes: 2 additions & 2 deletions crates/manifest/src/schema/v2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,10 @@ pub struct Component {
pub exclude_files: Vec<String>,
/// `allowed_http_hosts = ["example.com"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) allowed_http_hosts: Vec<String>,
pub allowed_http_hosts: Vec<String>,
/// `allowed_outbound_hosts = ["redis://myredishost.com:6379"]`
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) allowed_outbound_hosts: Vec<String>,
pub allowed_outbound_hosts: Vec<String>,
/// `key_value_stores = ["default", "my-store"]`
#[serde(
default,
Expand Down
1 change: 1 addition & 0 deletions examples/spin-timer/Cargo.lock

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

8 changes: 8 additions & 0 deletions src/commands/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ impl UpCommand {
.await?;
ResolvedAppSource::OciRegistry { locked_app }
}
AppSource::BareWasm(path) => ResolvedAppSource::BareWasm {
wasm_path: path.clone(),
},
AppSource::Unresolvable(err) => bail!("{err}"),
AppSource::None => bail!("Internal error - should have shown help"),
})
Expand Down Expand Up @@ -463,6 +466,11 @@ impl UpCommand {
})
}
ResolvedAppSource::OciRegistry { locked_app } => Ok(locked_app),
ResolvedAppSource::BareWasm { wasm_path } => spin_loader::from_wasm_file(&wasm_path)
.await
.with_context(|| {
format!("Failed to load component from {}", quoted_path(&wasm_path))
}),
}
}

Expand Down
31 changes: 25 additions & 6 deletions src/commands/up/app_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use spin_manifest::schema::v2::AppManifest;
pub enum AppSource {
File(PathBuf),
OciRegistry(String),
BareWasm(PathBuf),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add an explicit CLI arg (e.g. --from-wasm) for this path like we have for the other app sources?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I had wondered about this. I'm concerned it would make the docs a bit confusing, given that --from-file (in the current proposal) also accepts Wasm files (because my lawyers inform me that Wasm files are files within the meaning of the Act). I suppose we could deprecate --from-file and define a new --from-manifest flag that forces the file to be interpreted as TOML, but this feels excessive. Although maybe I'll feel differently if I understand what #2529 is about.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to instead have an orthogonal option to differentiate between manifestful and manifestless sources. Anyway, that ought to be backward compatible with this so definitely not a blocker here.

Unresolvable(String),
None,
}
Expand All @@ -31,7 +32,13 @@ impl AppSource {

pub fn infer_file_source(path: impl Into<PathBuf>) -> Self {
match spin_common::paths::resolve_manifest_file_path(path.into()) {
Ok(file) => Self::File(file),
Ok(file) => {
if is_wasm_file(&file) {
Self::BareWasm(file)
} else {
Self::File(file)
}
}
Err(e) => Self::Unresolvable(e.to_string()),
}
}
Expand All @@ -58,11 +65,17 @@ impl AppSource {
}
}

fn is_wasm_file(path: &Path) -> bool {
let extn = path.extension().and_then(std::ffi::OsStr::to_str);
extn.is_some_and(|e| e == "wasm" || e == "wat")
}

impl std::fmt::Display for AppSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::File(path) => write!(f, "local app {}", quoted_path(path)),
Self::OciRegistry(reference) => write!(f, "remote app {reference:?}"),
Self::BareWasm(path) => write!(f, "Wasm file {}", quoted_path(path)),
Self::Unresolvable(s) => write!(f, "unknown app source: {s:?}"),
Self::None => write!(f, "<no source>"),
}
Expand All @@ -77,6 +90,9 @@ pub enum ResolvedAppSource {
manifest_path: PathBuf,
manifest: AppManifest,
},
BareWasm {
wasm_path: PathBuf,
},
OciRegistry {
locked_app: LockedApp,
},
Expand All @@ -85,17 +101,20 @@ pub enum ResolvedAppSource {
impl ResolvedAppSource {
pub fn trigger_types(&self) -> anyhow::Result<Vec<&str>> {
let types = match self {
ResolvedAppSource::File { manifest, .. } => {
manifest.triggers.keys().collect::<HashSet<_>>()
}
ResolvedAppSource::File { manifest, .. } => manifest
.triggers
.keys()
.map(|s| s.as_str())
.collect::<HashSet<_>>(),
ResolvedAppSource::OciRegistry { locked_app } => locked_app
.triggers
.iter()
.map(|t| &t.trigger_type)
.map(|t| t.trigger_type.as_str())
.collect::<HashSet<_>>(),
ResolvedAppSource::BareWasm { .. } => ["http"].into_iter().collect(),
};

ensure!(!types.is_empty(), "no triggers in app");
Ok(types.into_iter().map(|t| t.as_str()).collect())
Ok(types.into_iter().collect())
}
}