-
Notifications
You must be signed in to change notification settings - Fork 259
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
feat: Add support for Spin plugins #735
Conversation
Signed-off-by: Kate Goldenring <[email protected]> Co-authored-by: karthik Ganeshram <[email protected]>
also add Spin compatibility checks, version checks, upgrading via installation. Signed-off-by: karthik Ganeshram <[email protected]> Co-authored-by: Kate Goldenring <[email protected]>
Signed-off-by: Kate Goldenring <[email protected]>
Signed-off-by: karthik Ganeshram <[email protected]>
4a90393
to
7b56ff2
Compare
Signed-off-by: Kate Goldenring <[email protected]> Co-authored-by: Karthik Ganeshram <[email protected]>
7b56ff2
to
0fb6ead
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.
A few thoughts on the command handlers - I've not had chance to look at the actual crate yet but will do so tomorrow. Most of what I have is stylistic issues rather than functionality, and some of it is subjective I know. Anyway, this is fantastic to see - thanks!
src/commands/plugins.rs
Outdated
/// Name of Spin plugin. | ||
#[clap( | ||
name = "PLUGIN_NAME", | ||
conflicts_with = "REMOTE_PLUGIN_MANIFEST", |
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.
Use consts for argument names; that way, if a name changes, it doesn't have to be kept in sync across multiple places.
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 point. Added in 8da5853
src/commands/external.rs
Outdated
let plugin_name = args.first().ok_or_else(|| anyhow!("Expected subcommand"))?; | ||
let plugins_dir = get_spin_plugins_directory()?; | ||
check_plugin_spin_compatibility(plugin_name, env!("VERGEN_BUILD_SEMVER"), &plugins_dir)?; | ||
let path = plugins_dir.join(plugin_name); |
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.
Maybe turn this little stanza into a function to keep the detail of path construction out of the flow?
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.
Moved path construction into PluginStore
and PluginLookup
types for referencing up installed plugins/manifests and looking up plugins from the repository, respectively.
command.envs(&get_env_vars_map(&path)?); | ||
log::info!("Executing command {:?}", command); | ||
// Allow user to interact with stdio/stdout of child process | ||
let status = command.status().await?; |
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.
Will this preserve the exit code? (or do we not care)
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 exit code is logged as info.
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 was thinking if a script wanted to use it. In particular, a script will want to know if the plugin succeeded or failed; at the very least, we should return a nonzero exit code if the plugin does, which I don't think happens at the moment? (do correct me if I'm missing it!)
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.
Do you mean in the case where someone has a script that executes spin <plugin>
and wants to check on its status to verify that this step succeeded before moving on to the next? If so, you are correct. We are currently not doing this but I agree this must be added. Thanks for pointing it out!
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.
Exactly, or e.g. imagine a spin test
plugin being run as part of a CI script.
src/commands/external.rs
Outdated
if cfg!(target_os = "windows") { | ||
binary.set_extension("exe"); | ||
} | ||
let mut command = Command::new(binary); |
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.
Again, I'd be inclined to hive command object construction into a function, though given it needs path
as well as binary
maybe it all gets a bit fiddly...
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.
hid it behind a installed_binary_path
function
src/commands/external.rs
Outdated
( | ||
"SPIN_PLUGIN_PATH".to_string(), | ||
path.to_str() | ||
.ok_or_else(|| anyhow!("Could not convert plugin path 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.
Can you do path.display().to_string()
and avoid the Result
? Or should we use the underlying OS string (as_os_str
) for this and the Spin binary path and pass OsStrings to envs
?
src/commands/plugins.rs
Outdated
name | ||
); | ||
} else { | ||
return Err(e); |
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.
Correct to bail on error rather than upgrading remaining plugins? This could mean one bad plugin blocking others from upgrading.
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.
made it so upgrading continues. Good call out!
src/commands/plugins.rs
Outdated
let name = self | ||
.name | ||
.ok_or_else(|| anyhow!("plugin name is required for upgrades"))?; | ||
// If downgrade is allowed, first uninstall the plugin |
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.
Why is this needed for downgrades if it isn't for upgrades?
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.
Super happy to see this - I have a lot of nits, but they are largely just nits.
The main things that I would like to see are:
- Name more things - create more functions and variables
- Define some sort of Store or Layout type to manage all the paths - there's a lot of duplication and at some point someone is going to join a path to the wrong parent
- Decouple the installer from the specifics of the CLI environment
- Ensure errors give good actionable data e.g. file names (or that users can get this info somehow)
- Ensure that changing your mind (saying no at the confirmation prompt) is a no-op (I think it might not be in a downgrade scenario)
- Distinguish errors (we can't or won't do this) from warnings where the user may want to go ahead anyway even knowing the risks
That said, even these could be iterated on if we want to get this in quickly - the functionality looks great.
use serde::{Deserialize, Serialize}; | ||
|
||
/// Expected schema of a plugin manifest. Should match the latest Spin plugin | ||
/// manifest JSON schema: |
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 for raising this now rather than during the SIP, but all our other manifests are TOML - is there a reason we switched to JSON for this one? (I am okay with an answer of "after giving TOML a try, we prefer JSON after all" if that's the reason!)
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 went with JSON because of the large ecosystem around JSON schema documentation and JSON parsing for schema matching. However, I did not realize that TOML also can be parsed with JSON schema; though, I cannot see examples. Have you seen examples of TOML schemas?
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 "Even Better TOML" VS Code extension does some stuff with TOML schemas. But yeah, it's not as widely used. It's a definite advantage of JSON.
Should we migrate to JSON for our other manifests to take advantage of schema support, or does the plugin manifest have specific considerations (e.g. higher complexity, less human authoring) that make schema support more important (or human readability less important) than in other manifests?
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 like how JSON schemas enable specifying field options (ie OS enum), verify names (regex and enums), can specify manditory and optional fields, and act as documentation on how to construct a manifest. However, the Spin manifest tomls are refreshingly easy to read. I wonder if we can make manifests TOML but when validating them, convert them to JSON and check them against a schema. That could give us the best of both worlds. (Unfortunately the TOML schema parser is unmaintained: https://github.com/json-schema-everywhere/pajv, which is which the initial convert to JSON step may be needed)
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 played around with this in this branch of spin-plugins
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 feels a little messy. In the end, @itowlson, I think your note summarizes it: spin plugin manifests are less frequently authored and can use the benefits of json tooling (schema validation). However, spin app manifests are validated when they are run, so they dont need json schema validation. So i think using json for spin plugin manifests and toml for app manifests is appropriate
crates/plugins/src/install.rs
Outdated
} | ||
} | ||
|
||
/// Installs a Spin plugin. First attempts to retrieve the plugin manifest. |
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 function is 140 lines - can we identify cohesive chunks that could be broken out into functions with intention-revealing names?
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 made it chunky :)
crates/plugins/src/install.rs
Outdated
/// architecture. Verifies the checksum of the source, unpacks and installs | ||
/// it into the plugins directory. | ||
pub async fn install(&self) -> Result<()> { | ||
let plugin_manifest: PluginManifest = match &self.manifest_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.
This match expression gets quite long. Could each arm be a smol function (get_manifest_from_url|file|repo
)? Would it fit better on the ManifestLocation
type? (it looks like one arm uses one local field so not sure, just raising it for consideration)
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 agree that the remote repo pulling could be separated out. this motivated me creating a PluginLookup
type that focuses on the repo fetching and lookups
crates/plugins/src/version_check.rs
Outdated
supported, | ||
e | ||
) | ||
})?; |
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 there a way for the user to go "okay there's a typo in the version spec but I want it anyway"?
9806046
to
8da5853
Compare
Thank you @itowlson for this incredible review! This should be in much better shape now. The following still need to be addressed:
|
@kate-goldenring for me the "override version check" is a way to bridge a mismatch between developers and users. Consider:
The trouble is partly that we're not on useful semver yet, so the change from 0.5 to 0.6 communicates little - they might be backward compatible, or might not. The author has to be conservative and assume minor or patch releases could be breaking, but that means the user gets hosed when a release is only a bugfix or new feature, yet their plugins stop working anyway. Hope that makes sense. I do take the point that the user could hose themselves by running an incompatible plugin, so a dire warning is fine - but blocking it completely feels too strict for a project where users are on the bleeding edge anyway! As always, happy to discuss further. (And apologies for not flagging this at the SIP stage - I realise how frustrating it is to have it relitigated.) |
@itowlson that does make sense. My only concern is what do we do when plugins are executed? We currently are also checking version there in
Currently we throw an error (and should also recommend upgrading). I am not sure how we could pass an |
8da5853
to
cedf319
Compare
Signed-off-by: Kate Goldenring <[email protected]>
cedf319
to
2fc0f23
Compare
Signed-off-by: Kate Goldenring <[email protected]>
36056e7
to
8cd0c2c
Compare
…tdout Signed-off-by: Kate Goldenring <[email protected]>
8cd0c2c
to
629cccc
Compare
We could pass the The positional argument can be either structured as
or
|
@kate-goldenring Another possibility is to prompt if a version mismatch is detected, with options of run, don't run, and "I know this is safe, don't ask again." If the user chooses "don't ask again," it would apply only to the exact combination of plugin and Spin versions. E.g. if the user upgraded to Spin 0.5.1 then we'd prompt for However, we'd still likely need @karthik2804's "skip version check" option as well, so that people could run "incompatible but known safe" plugins in CI. So this is strictly more effort, and we'd need to decide if we felt the added convenience was worth it. We'd also need to pay attention to how any prompt worked in CI environments. You don't want to upgrade your CI environment to 0.5.0 and find all your workflows stall without meaningful error! Maybe this is a can of worms... (Or maybe this wouldn't be an issue because without intervention they'd fail at the plugin install stage.) |
@itowlson prompting seems feasible. A few follow up thoughts/questions:
|
Maybe this is too much of a can of worms. I am struggling rather with how to offer an experience that would be safe in CI but forgiving to interactive users. Perhaps always failing on incompatibility (and allowing Karthik's flag to override) is safer... |
I am conscious the whole override thing is relitigating the SIP so I'm happy to drop it for this PR if you prefer - nothing stops us coming back to it later. |
@bacongobbler what are your thoughts on adding an |
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.
@kate-goldenring @karthik2804 Thanks for this - to me at least this iteration is much easier to follow, and it partitions responsibilities in a way I really liked. There are a few things I think still need a bit of tweaking, but they are local (e.g. more info on errors, renaming here and there) rather than structural. Great stuff!
…nstall functions Signed-off-by: Kate Goldenring <[email protected]>
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 really like how this is looking! And I'm excited to see where this leads us in the future!
@itowlson raises a good point. Because Spin is not yet 1.0, we cannot guarantee compatibility for plugin developers between Spin 0.x and Spin 0.x+1. It is very likely that we'll have plugin developers pin their plugins to a specific release for the time being until we can guarantee backwards compatibility (1.0.0). That will cause this situation to turn up fairly often. Post-1.0, the same situation could occur if Spin 2.0.0 was released and Alice was no longer maintaining the plugin. Bob would likely want to use Alice's plugin against Spin 2.0, but because Alice stepped away from the project, nobody is available to update it. Though in that case, perhaps Bob should consider forking Alice's plugin to ensure compatibility with 2.0.0... However, given that
I'm okay with the feature flag. One point of feedback that you're welcome to disregard: have you thought about putting this behind an interactive prompt window instead? Say for example Bob was installing Alice's plugin with Spin 0.6:
With |
@bacongobbler @itowlson sounds like we've reached consensus on adding an |
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.
Mostly minor style nits / suggestions. Nothing impacting correctness except potentially the to_string_lossy
stuff, though that's pretty unlikely to cause problems.
No blockers from my perspective.
@lann Thank you for the great pointers here -- learned a lot! I think I addressed your comments -- but happy to iterate if you have more feedback. |
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.
Looks good! A few more style suggestions on the changes, but again nothing blocking.
6e13841
to
e5d319e
Compare
Signed-off-by: Kate Goldenring <[email protected]>
e5d319e
to
9d85795
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.
Code LGTM. Unfortunately I didn't participate as much as others in the SIP review so I don't have much to say on functionality.
Adds support for Spin Plugins -- SIP 006.
Spin plugins are additional functionality and subcommands that can be added to Spin via this new
spin plugin
CLI command.A Spin plugin is defined by a JSON plugin manifest. A plugin manifest defines a plugin’s name, version, license, homepage (i.e. GitHub repo), compatible Spin version, and gives a short description of the plugin. It also points to the plugin source for various operating systems and platforms. This manifest is reference for plugin installs and upgrades.
The
spin-plugins
repository defines the plugin manifest JSON schema and contains an index of installable Spin and community maintained plugins.These changes were equally contributed and co-authored by @karthik2804 -- thanks for all fabulous 🍐ing!
Testing locally
Test the flow of installing an
example
plugin that echosThis is an example Spin plugin!
when run. It lives in the centralized Spin plugins repository here.Clone this branch, build spin with
make build
, and install spincargo install --locked --path .
or replacespin
below with the path to the binary./target/release/spin
.Installing a plugin from the
spin-plugins
repoUpgrading a plugin
Installing plugin from remote manifest
Installing plugin from local manifest
See
test_spin_plugin_install_command
inintegration.rs
.