Add experimental spin up --component flag to run a subset of app components#2826
Conversation
c8dc850 to
d8834c8
Compare
d8834c8 to
67af2d3
Compare
itowlson
left a comment
There was a problem hiding this comment.
I think these are mostly nits or "is this the best way" kibbitzing - I do think it's worth adding a test case for templated hosts though. I love that you were able to bring this off so simply!
| // Introspects the LockedApp to find and selectively retain the triggers that correspond to those components | ||
| fn retain_components(locked_app: &mut LockedApp, components: &[String]) -> Result<()> { | ||
| // Create a temporary app to access parsed component and trigger information | ||
| let tmp_app = spin_app::App::new("tmp", locked_app.clone()); |
There was a problem hiding this comment.
I feel like up should not have to create an App just to look at the triggers and components. It seems like this should be possible to get from the LockedApp but maybe not (I know the loose typing can introduce a bit of faff there...). Or maybe App and LockedApp have largely converged at this point - I see the factors work has removed the dependency from spin-app to spin-core which was always my concern in the past - maybe App is just a helpful wrapper around LockedApp now?
There was a problem hiding this comment.
It is really hard to map components to triggers without parsing each trigger type, which the App type does for you. From talking to @lann, this seems like the best approach, but I agree that it feels hacky. I may have missed a recent change in factors that offers a different strategy
src/commands/up.rs
Outdated
| for (c, _) in &component_triggers { | ||
| let allowed_hosts = allowed_hosts(c)?; | ||
| allowed_hosts.iter().try_for_each(|host| { | ||
| let uri = host.parse::<http::Uri>().unwrap(); |
There was a problem hiding this comment.
I think you might have a problem here with templated URIs? I believe they're resolved during trigger startup, unless the factors work has changed that. Might merit a case in the unit tests.
There was a problem hiding this comment.
We can't (currently) resolve templates here, so I guess there are two questions:
-
Will this code panic if it tries to resolve a template? (seems likely to me)
-
What should happen with templates here? I would probably suggest doing nothing. Using templates for service chaining seems like an uncommon scenario and the consequence here is pretty low: a runtime error instead of this nicer startup validation.
There was a problem hiding this comment.
Yes, the current code will panic on an unsubstituted template.
I agree that ignoring the error is likely best. Service chaining via a templated URL is one of those hazy features. It happens to work in the current CLI implementation, and I don't believe we complain if someone does it, but there are no guarantees around it. So it seems reasonable for a failed URL parse to be interpreted as "it's not a service chaining URL" which means it is of no further interest.
If we wanted more belt and braces we could validate during trigger load (after template substitution) that all non-wildcard service chaining URLs pointed to components that exist in the app. Which might not be a bad plan anyway, and can be done outside the scope of this PR.
There was a problem hiding this comment.
Good call out here. I agree that we should ignore URLs that cannot be parsed for now and +1 to validating non-wildcard service chaining URLs are to existing components.
src/commands/up.rs
Outdated
| .triggers() | ||
| .filter_map(|t| match t.component() { | ||
| Ok(comp) => { | ||
| if components.contains(&comp.id().to_string()) { |
There was a problem hiding this comment.
Doesn't id() return &str making the to_string() unnecessary?
There was a problem hiding this comment.
This one really baffled me. I get this error if i don't explicitly pass &String:
expected reference `&std::string::String`
found reference `&str`| no triggers in app | ||
| "#; | ||
|
|
||
| let expected = "Error: No triggers in app\n"; |
There was a problem hiding this comment.
This error might be a bit too terse. I'm not sure what a better wording would be though...
ab2c037 to
67d9b77
Compare
|
@itowlson I added a test to validate templated hosts are ignored/allowed |
itowlson
left a comment
There was a problem hiding this comment.
Thanks for all the changes @kate-goldenring - this reads really clearly to me! I left some minor suggestions but LGTM
|
@itowlson @rylev thoughts on moving |
|
@itowlson what are your thoughts on: spin up --component "foo" --component "bar"vs spin up --components "foo,bar,bap"I think i prefer the latter though it is more prone to parsing errors |
|
@kate-goldenring The comma separated form should be safe to parse, because component IDs can't contain tricksy characters, but even as I write that I hear my ghost gloating "famous last words." I do have reservations about it requiring knowledge (what is the separator), though, and we use the "multiple occurrences" form for most other things. My suggestion would be to go with multiple occurrences for now, and see if it is used by human users (as opposed to deployment scripts) often enough for people to complain about the verbosity - if so we can add support for a CSV form as well - how about that? |
|
Re moving it to |
|
@kate-goldenring I just remembered we do have precedent for this in I did notice that the |
src/commands/up.rs
Outdated
|
|
||
| /// Specific component to run. Can specify multiple. If omitted, all | ||
| /// components are run. | ||
| #[clap(hide = true, long = "component")] |
There was a problem hiding this comment.
Is hide=true here because this is experimental?
There was a problem hiding this comment.
Yeah. I am not sure if there is another way we've marked features as experimental in the past. Should i update the comment to say it is experimental too?
There was a problem hiding this comment.
yea not sure what the precedent is but I think a comment would be helpful.
|
+1 to @itowlson's comment about the |
radu-matei
left a comment
There was a problem hiding this comment.
Tested this with applications with several triggers and components and the behaviour LGTM!
Thanks!
@itowlson thank you for noticing this. I think we should similarly name the flag
In that case, i think we should ideally apply the flag to both commands. I am not sure if that is possible though |
67d9b77 to
ff7feef
Compare
|
@kate-goldenring I am honestly not sure what the right experience is with I guess whatever we choose will be surprising to some people, but they always have the get-out clause of Perhaps for Spin 3 we should enable Sorry for the long and indecisive ramble...! |
|
+1 to what @itowlson said about flags for spin 3. |
ff7feef to
6d29d49
Compare
|
@itowlson I can see that distinction. This is no longer passing |
rylev
left a comment
There was a problem hiding this comment.
I mostly have nits (feel free to ignore them if you like), but I am unsure about whether the service chaining check is correct.
| mod config; | ||
| pub mod runtime_config; | ||
|
|
||
| use std::{collections::HashMap, sync::Arc}; |
There was a problem hiding this comment.
Nit: I don't think it's a hard rule, but we tend to keep std uses separate from external crates (which are both separate from uses local to the crate). It's not blocking, but I would consider reverting this.
There was a problem hiding this comment.
I'm not sure i follow. Are you saying to not use these dependencies, not use them in the root of the crate or change the formatting somehow? Other factors use std libs so i am assuming that is not it: https://github.com/kate-goldenring/spin/blob/6d29d49d5b2d9aad5b13f4c95602a2c4a77b16e8/crates/factor-key-value/src/util.rs#L5-L6
There was a problem hiding this comment.
Sorry - this is all about formatting of the use statements. Typically use statements are grouped into three groups:
stdlib uses (e.g., std::fmt::Display)- external crate uses (e.g.,
tokio::task::spawn) - uses local to the crate (e.g.,
crate::foo)
These three groups are then separated by an empty line.
There are a million exceptions, and it's not really important, so I only bring it up since you moved this line for seemingly no other reason than aesthetics. Feel free to ignore my comment 😄
| .await | ||
| .context("Failed to load application")?; | ||
| if !self.components.is_empty() { | ||
| retain_components(&mut locked_app, &self.components)?; |
There was a problem hiding this comment.
I would add some additional context to this function and some of the subfunctions with .context(). Right now an error from this function might simply read "failed to get allowed hosts" which is highly confusing without the context of the code.
src/commands/up.rs
Outdated
| if let Ok(component) = t.component() { | ||
| if retained_components.contains(&component.id().to_string()) { | ||
| let allowed_hosts = allowed_outbound_hosts(&component).context("failed to get allowed hosts")?; | ||
| allowed_hosts.iter().try_for_each(|host| { |
There was a problem hiding this comment.
Nit: for_each and try_for_each aren't super common in my experience. This code would be a bit more readable IMO if for in were used instead.
src/commands/up.rs
Outdated
| if retained_components.contains(&component.id().to_string()) { | ||
| let allowed_hosts = allowed_outbound_hosts(&component).context("failed to get allowed hosts")?; | ||
| allowed_hosts.iter().try_for_each(|host| { | ||
| // Templated URLs are not resolved at this point, so ignore unresolvable URIs |
There was a problem hiding this comment.
Does this mean we might allow some components that we shouldn't?
There was a problem hiding this comment.
It means that we may spin up something that would later fail to execute. For example, say you have a spin app with 3 components, foo, bar and baz. You run spin up --component-id "foo" --component-id "baz" and foo is configured with allowed_outbound_hosts = [ "https://{{ myvar }}.spin.internal"]. We know we want to retain foo and baz but we also want to check to make sure neither do internal service chaining to bar so that we can catch that error and fail the run. However, we cannot parse that host, so we cannot whether it is service chaining and even if we could we couldn't determine what component this is referencing, so we continue.
No extra components are run but components may be run that cannot service chain. We discuss this a bit in this thread #2826 (comment)
There was a problem hiding this comment.
Ah! Sorry I was reading the logic backwards. Sounds good to me!
One nit that might make this less confusing to readers in the future. We could add a comment outlining that we're doing a best effort lookup of components that are allowed to be accessed through service chaining, and we try to error early if a component tries to chain to another component that is not retained.
There was a problem hiding this comment.
Good call out. I elaborated in the comment to make the best effort clearer.
rylev
left a comment
There was a problem hiding this comment.
I still have some suggestions for improvements, but I don't want to block merging this any more!
src/commands/up.rs
Outdated
| if retained_components.contains(&component.id().to_string()) { | ||
| let allowed_hosts = allowed_outbound_hosts(&component).context("failed to get allowed hosts")?; | ||
| allowed_hosts.iter().try_for_each(|host| { | ||
| // Templated URLs are not resolved at this point, so ignore unresolvable URIs |
There was a problem hiding this comment.
Ah! Sorry I was reading the logic backwards. Sounds good to me!
One nit that might make this less confusing to readers in the future. We could add a comment outlining that we're doing a best effort lookup of components that are allowed to be accessed through service chaining, and we try to error early if a component tries to chain to another component that is not retained.
| let allowed_hosts = allowed_outbound_hosts(&component).context("failed to get allowed hosts")?; | ||
| allowed_hosts.iter().try_for_each(|host| { | ||
| // Templated URLs are not resolved at this point, so ignore unresolvable URIs | ||
| if let Ok(uri) = host.parse::<http::Uri>() { |
There was a problem hiding this comment.
Allowed host configs are very often not valid Uris but are still statically resolvable. For example, *://{foo.spin.internal, bar.spin.internal}. This check wouldn't run because the above cannot be parsed as an http::Uri.
You might want to consider AllowedHostConfig::parse instead. This handles all of the interpolation syntax that allowed host configs are allowed to have. You can then add a new method like AllowedHostConfig::service_chaining_target that returns a Vec<String> with all of the components that that AllowedHostConfig targets.
There was a problem hiding this comment.
I didn't know you could chain to multiple targets in one host *://{foo.spin.internal, bar.spin.internal}. Should we update the docs on this: https://developer.fermyon.com/spin/v2/http-outbound#local-service-chaining?
There was a problem hiding this comment.
It looks like we have two conditions we want to check for:
- List of internal targets:
*://{foo.spin.internal, bar.spin.internal} - Templated targets:
http://{{ myvar }}.spin.internal
The former, we can parse with AllowedHostConfig::parse and get a "host lists are not supported error", but i don't understand why we would allow that syntax rather than requiring multiple entries. The latter, we cannot parse with AllowedHostConfig::parse, so we may want to add support to catch this case with specific error types for each. I am not sure what this adds at this point though? More specific errors we can surface to users?
I think i will merge this as is but I would like to continue to discuss this and potentially follow up on this in another PR
Signed-off-by: Kate Goldenring <kate.goldenring@fermyon.com>
a666b18 to
788dec1
Compare
fixes #2820
Say I have an app with 4 components but only want to run 2, I can do the equivalent of
spin up --component-id "foo" --component-id "bar"Approach: Modifies the locked app to remove undesired components and triggers before the locked app is loaded. Takes somewhat hacky approach of creating a temporary
Appstruct to pull out information mapping components to their triggers.