-
Notifications
You must be signed in to change notification settings - Fork 257
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
Add experimental spin up --component
flag to run a subset of app components
#2826
Add experimental spin up --component
flag to run a subset of app components
#2826
Conversation
c8dc850
to
d8834c8
Compare
d8834c8
to
67af2d3
Compare
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't id()
return &str
making the to_string()
unnecessary?
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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 |
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.
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
@@ -113,6 +114,11 @@ pub struct UpCommand { | |||
#[clap(long, takes_value = false, env = ALWAYS_BUILD_ENV)] | |||
pub build: bool, | |||
|
|||
/// 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is hide=true
here because this is experimental?
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yea not sure what the precedent is but I think a comment would be helpful.
+1 to @itowlson's comment about the |
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.
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 |
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 mostly have nits (feel free to ignore them if you like), but I am unsure about whether the service chaining check is correct.
@@ -1,8 +1,6 @@ | |||
mod config; | |||
pub mod runtime_config; | |||
|
|||
use std::{collections::HashMap, sync::Arc}; |
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.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry - this is all about formatting of the use
statements. Typically use
statements are grouped into three groups:
std
lib 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this mean we might allow some components that we shouldn't?
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call out. I elaborated in the comment to make the best effort clearer.
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 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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
App
struct to pull out information mapping components to their triggers.