-
Notifications
You must be signed in to change notification settings - Fork 290
Merge spin locked app into spin app #3231
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
/// Type alias for a [`Result`]s with [`Error`]. | ||
pub type Result<T> = std::result::Result<T, Error>; | ||
|
||
/// Errors returned by methods in this crate. | ||
#[derive(Debug, thiserror::Error)] | ||
pub enum Error { | ||
/// An error propagated from the `spin_core` crate. | ||
#[error(transparent)] | ||
Core(anyhow::Error), | ||
/// An error from a `DynamicHostComponent`. | ||
#[error("host component error: {0:#}")] | ||
HostComponent(#[source] anyhow::Error), | ||
/// An error from a `Loader` implementation. | ||
#[error(transparent)] | ||
Loader(anyhow::Error), | ||
/// An error indicating missing or unexpected metadata. | ||
#[error("metadata error: {0}")] | ||
Metadata(String), | ||
/// An error indicating failed JSON (de)serialization. | ||
#[error("json error: {0}")] | ||
Json(#[from] serde_json::Error), | ||
/// A validation error that can be presented directly to the user. | ||
#[error(transparent)] | ||
Validation(anyhow::Error), | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,15 +11,22 @@ use std::sync::Arc; | |
|
||
use serde::Deserialize; | ||
use serde_json::Value; | ||
use spin_locked_app::MetadataExt; | ||
|
||
use locked::{ContentPath, LockedApp, LockedComponent, LockedComponentSource, LockedTrigger}; | ||
use crate::locked::{ | ||
ContentPath, LockedApp, LockedComponent, LockedComponentSource, LockedTrigger, | ||
}; | ||
|
||
pub use spin_locked_app::locked; | ||
pub use spin_locked_app::values; | ||
pub use spin_locked_app::{Error, MetadataKey, Result}; | ||
pub use crate::locked::Variable; | ||
|
||
pub use locked::Variable; | ||
use crate::error::{Error, Result}; | ||
use crate::metadata::MetadataExt as _; | ||
|
||
mod error; | ||
pub mod locked; | ||
mod metadata; | ||
pub mod values; | ||
|
||
pub use metadata::MetadataKey; | ||
|
||
/// MetadataKey for extracting the application name. | ||
pub const APP_NAME_KEY: MetadataKey = MetadataKey::new("name"); | ||
|
@@ -121,7 +128,7 @@ impl App { | |
return Ok(None); | ||
}; | ||
let metadata = T::deserialize(value).map_err(|err| { | ||
Error::MetadataError(format!( | ||
Error::Metadata(format!( | ||
"invalid metadata value for {trigger_type:?}: {err:?}" | ||
)) | ||
})?; | ||
|
@@ -186,7 +193,7 @@ impl App { | |
) -> Result<LockedApp> { | ||
self.validate_retained_components_exist(retained_components)?; | ||
for validator in validators { | ||
validator(&self, retained_components).map_err(Error::ValidationError)?; | ||
validator(&self, retained_components).map_err(Error::Validation)?; | ||
} | ||
let (component_ids, trigger_ids): (HashSet<String>, HashSet<String>) = self | ||
.triggers() | ||
|
@@ -211,7 +218,7 @@ impl App { | |
.collect::<HashSet<_>>(); | ||
for c in retained_components { | ||
if !app_components.contains(*c) { | ||
return Err(Error::ValidationError(anyhow::anyhow!( | ||
return Err(Error::Validation(anyhow::anyhow!( | ||
"Specified component \"{c}\" not found in application" | ||
))); | ||
} | ||
|
@@ -310,10 +317,10 @@ impl<'a> AppTrigger<'a> { | |
let id = &self.locked.id; | ||
let common_config: CommonTriggerConfig = self.typed_config()?; | ||
let component_id = common_config.component.ok_or_else(|| { | ||
Error::MetadataError(format!("trigger {id:?} missing 'component' config field")) | ||
Error::Metadata(format!("trigger {id:?} missing 'component' config field")) | ||
})?; | ||
self.app.get_component(&component_id).ok_or_else(|| { | ||
Error::MetadataError(format!( | ||
Error::Metadata(format!( | ||
"missing component {component_id:?} configured for trigger {id:?}" | ||
)) | ||
}) | ||
|
@@ -337,29 +344,80 @@ pub fn retain_components( | |
|
||
#[cfg(test)] | ||
mod test { | ||
use spin_factors_test::build_locked_app; | ||
use std::collections::HashSet; | ||
|
||
use spin_manifest::schema::v2::{self, ComponentSpec, Trigger}; | ||
|
||
use super::*; | ||
|
||
fn locked_trigger( | ||
trigger_type: String, | ||
mut trigger: v2::Trigger, | ||
) -> anyhow::Result<LockedTrigger> { | ||
fn reference_id(spec: v2::ComponentSpec) -> toml::Value { | ||
let v2::ComponentSpec::Reference(id) = spec else { | ||
unreachable!("should have already been normalized"); | ||
}; | ||
id.as_ref().into() | ||
} | ||
|
||
if let Some(id) = trigger.component.map(reference_id) { | ||
trigger.config.insert("component".into(), id); | ||
} | ||
|
||
Ok(LockedTrigger { | ||
id: trigger.id, | ||
trigger_type, | ||
trigger_config: trigger.config.try_into()?, | ||
}) | ||
} | ||
|
||
pub fn locked_app() -> LockedApp { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks - I think this is (unfortunately) the least worst option and I appreciate you persevering with it. I do think you can simplify the implementation a bit, e.g.:
I think this is terser and clearer -
|
||
LockedApp { | ||
spin_lock_version: Default::default(), | ||
must_understand: Default::default(), | ||
metadata: Default::default(), | ||
host_requirements: Default::default(), | ||
variables: Default::default(), | ||
triggers: vec![locked_trigger( | ||
"http".to_owned(), | ||
Trigger { | ||
id: Default::default(), | ||
component: Some(ComponentSpec::Reference( | ||
"empty".to_string().try_into().unwrap(), | ||
)), | ||
components: Default::default(), | ||
config: Default::default(), | ||
}, | ||
) | ||
.unwrap()], | ||
components: vec![LockedComponent { | ||
id: "empty".to_owned(), | ||
metadata: Default::default(), | ||
source: LockedComponentSource { | ||
content_type: "application/wasm".to_owned(), | ||
content: locked::ContentRef { | ||
source: Some("does-not-exist.wasm".to_owned()), | ||
inline: None, | ||
digest: None, | ||
}, | ||
}, | ||
env: Default::default(), | ||
files: Default::default(), | ||
config: Default::default(), | ||
dependencies: Default::default(), | ||
host_requirements: Default::default(), | ||
}], | ||
} | ||
} | ||
|
||
fn does_nothing_validator(_: &App, _: &[&str]) -> anyhow::Result<()> { | ||
Ok(()) | ||
} | ||
|
||
#[tokio::test] | ||
async fn test_retain_components_filtering_for_only_component_works() { | ||
let manifest = toml::toml! { | ||
spin_manifest_version = 2 | ||
|
||
[application] | ||
name = "test-app" | ||
|
||
[[trigger.test-trigger]] | ||
component = "empty" | ||
|
||
[component.empty] | ||
source = "does-not-exist.wasm" | ||
}; | ||
let mut locked_app = build_locked_app(&manifest).await.unwrap(); | ||
let mut locked_app = locked_app(); | ||
locked_app = retain_components(locked_app, &["empty"], &[&does_nothing_validator]).unwrap(); | ||
let components = locked_app | ||
.components | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Intentional that tests are being removed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was moved into the
spin-factor-test
crate to avoid the cyclic dependency issue the compiler frowns at.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
factors-test
seems like a rather strange place for it - I thought retaining components was a pure LockedApp operation whereasfactors-test
seems to be more about instantiation and runtime configuration. What was the cyclic dependency that you ran into?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test inside the module
test_retain_components_filtering_for_only_component_works
callsbuild_locked_app
fromspin-factor-test
, which returnsLockedApp
.LockedApp
previously lived insidespin-locked_app
; hence, bothspin-app
andspin-factor-test
depend onspin-locked-app
, no conflict.With the merge,
LockedApp
lives insidespin-app
, that meansspin-factor-test
, which hostsbuild_locked_app
, would have to depend onspin-app
to importLockedApp
(which the function returns alongside other functions), whilespin-app
depends onspin-factor-test
forbuild_locked_app
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like the tail wagging the dog.
build_locked_app
is a tiny ancillary function that basically wrapsspin_loader::from_file
. I don't think we should be locating tests based on where we happen to have existing helpers - I'd strongly prefer to keep the test with the thing it's testing and figure out how to support it in that location.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see you've moved the
retain_components
test tospin_loader
, which still seems like you're letting the ancillarybuild_locked_app
control where you put the test. The test should go alongsideretain_components
and then you should figure out what supporting infrastructure it needs in that location. If that involves reworking or jettisoningbuild_locked_app
then so be it -build_locked_app
is not the important thing in the test and should not be driving the design.Looking at existing code, I'm a bit puzzled at why the compiler is getting mad now anyway. The
app
crate already has a dev-dependency onfactors-test
, andfactors-test
already has a dependency onapp
. Is it becausefactors-test
really only depended on theLockedApp
re-export and the compiler realised it wasn't a true circle?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think because of the cyclic issue even though
spin-app
hasspin-factors-test
as adev-dependence
, the compiler keeps having issues knowing whichLockedApp
retain_components
returns.the crate `spin_app` is compiled multiple times, possibly with different configurations
Full error: