diff --git a/crates/plugins/src/badger/mod.rs b/crates/plugins/src/badger/mod.rs index 8ceaf1ae67..f4cbf9a852 100644 --- a/crates/plugins/src/badger/mod.rs +++ b/crates/plugins/src/badger/mod.rs @@ -168,18 +168,15 @@ impl BadgerEvaluator { } async fn available_upgrades(&self) -> anyhow::Result { - let store = self.plugin_manager.store(); + let catalogue = self.plugin_manager.catalogue(); let latest_version = { - let latest_lookup = crate::lookup::PluginLookup::new(&self.plugin_name, None); - let latest_manifest = latest_lookup - .resolve_manifest_exact(store.get_plugins_directory()) - .await - .ok(); + let latest_lookup = crate::lookup::PluginRef::new(&self.plugin_name, None); + let latest_manifest = latest_lookup.resolve_manifest_exact(&catalogue).await.ok(); latest_manifest.and_then(|m| semver::Version::parse(m.version()).ok()) }; - let manifests = store.catalogue_manifests()?; + let manifests = catalogue.manifests()?; let relevant_manifests = manifests .into_iter() .filter(|m| m.name() == self.plugin_name); diff --git a/crates/plugins/src/catalogue.rs b/crates/plugins/src/catalogue.rs new file mode 100644 index 0000000000..897c1d5ee7 --- /dev/null +++ b/crates/plugins/src/catalogue.rs @@ -0,0 +1,108 @@ +use crate::{git::GitSource, manifest::PluginManifest}; +use anyhow::Context; +use semver::Version; +use std::path::{Path, PathBuf}; +use url::Url; + +const SPIN_PLUGINS_REPO: &str = "https://github.com/spinframework/spin-plugins/"; + +pub(crate) fn plugins_repo_url() -> Result { + Url::parse(SPIN_PLUGINS_REPO) +} + +/// The local clone of the spin-plugins repo. +pub struct Catalogue { + git_root: PathBuf, + manifests_root: PathBuf, +} + +// Name of directory containing the installed manifests +const LOCAL_CATALOGUE_MANIFESTS_DIRECTORY: &str = "manifests"; + +impl Catalogue { + pub fn new(git_root: PathBuf) -> Self { + let manifests_root = git_root.join(LOCAL_CATALOGUE_MANIFESTS_DIRECTORY); + Self { + git_root, + manifests_root, + } + } + + pub fn manifests(&self) -> anyhow::Result> { + // Structure: + // CATALOGUE_DIR (spin/plugins/.spin-plugins/manifests) + // |- foo + // | |- foo@0.1.2.json + // | |- foo@1.2.3.json + // | |- foo.json + // |- bar + // |- bar.json + let catalogue_manifests_dir = &self.manifests_root; + + // Catalogue directory doesn't exist so likely nothing has been installed. + if !catalogue_manifests_dir.exists() { + return Ok(Vec::new()); + } + + let plugin_dirs = catalogue_manifests_dir + .read_dir() + .with_context(|| format!("reading manifest catalogue at {catalogue_manifests_dir:?}"))? + .filter_map(|d| d.ok()) + .map(|d| d.path()) + .filter(|p| p.is_dir()); + let manifest_paths = plugin_dirs.flat_map(|path| crate::util::json_files_in(&path)); + let manifests: Vec<_> = manifest_paths + .filter_map(|path| crate::util::try_read_manifest_from(&path)) + .collect(); + Ok(manifests) + } + + /// Get expected path to the manifest of a plugin with a given name + /// and version within the spin-plugins repository + pub(crate) fn manifest_path( + &self, + plugin_name: &str, + plugin_version: &Option, + ) -> PathBuf { + self.manifests_root + .join(plugin_name) + .join(crate::util::manifest_file_name_version( + plugin_name, + plugin_version, + )) + } + + /// Clones or pulls the spin-plugins repo as required. THIS IS NOT SYNCHRONISED + /// and should be used only if you know nothing else is updating the working + /// copy at the same time: generally, prefer `PluginManager::update()` which + /// checks for contention. + pub(crate) async fn fetch_from_remote(&self, repo_url: &Url) -> anyhow::Result<()> { + let git_root = &self.git_root; + let git_source = GitSource::new(repo_url, None, git_root); + if accept_as_repo(git_root) { + git_source.pull().await?; + } else { + git_source.clone_repo().await?; + } + Ok(()) + } + + pub(crate) async fn ensure_inited(&self, repo_url: &Url) -> anyhow::Result<()> { + let git_root = &self.git_root; + let git_source = GitSource::new(repo_url, None, git_root); + if !accept_as_repo(git_root) { + git_source.clone_repo().await?; + } + Ok(()) + } +} + +#[cfg(not(test))] +fn accept_as_repo(git_root: &Path) -> bool { + git_root.join(".git").exists() +} + +#[cfg(test)] +fn accept_as_repo(git_root: &Path) -> bool { + git_root.join(".git").exists() || git_root.join("_spin_test_dot_git").exists() +} diff --git a/crates/plugins/src/lib.rs b/crates/plugins/src/lib.rs index 0759eaf9fc..6974458933 100644 --- a/crates/plugins/src/lib.rs +++ b/crates/plugins/src/lib.rs @@ -1,11 +1,16 @@ pub mod badger; +mod catalogue; pub mod error; mod git; -pub mod lookup; +mod lookup; pub mod manager; pub mod manifest; mod store; -pub use store::PluginStore; +mod util; + +pub use catalogue::Catalogue; +pub use lookup::PluginRef; +pub use manager::PluginManager; /// List of Spin internal subcommands pub(crate) const SPIN_INTERNAL_COMMANDS: &[&str] = &[ diff --git a/crates/plugins/src/lookup.rs b/crates/plugins/src/lookup.rs index e224da5d3d..b0d402864e 100644 --- a/crates/plugins/src/lookup.rs +++ b/crates/plugins/src/lookup.rs @@ -1,27 +1,14 @@ -use crate::{error::*, git::GitSource, manifest::PluginManifest, store::manifest_file_name}; +use crate::{Catalogue, catalogue::plugins_repo_url, error::*, manifest::PluginManifest}; use semver::Version; -use std::{ - fs::File, - path::{Path, PathBuf}, -}; -use url::Url; - -// Name of directory that contains the cloned centralized Spin plugins -// repository -const PLUGINS_REPO_LOCAL_DIRECTORY: &str = ".spin-plugins"; - -// Name of directory containing the installed manifests -pub(crate) const PLUGINS_REPO_MANIFESTS_DIRECTORY: &str = "manifests"; - -pub(crate) const SPIN_PLUGINS_REPO: &str = "https://github.com/spinframework/spin-plugins/"; +use std::fs::File; /// Looks up plugin manifests in centralized spin plugin repository. -pub struct PluginLookup { +pub struct PluginRef { pub name: String, pub version: Option, } -impl PluginLookup { +impl PluginRef { pub fn new(name: &str, version: Option) -> Self { Self { name: name.to_lowercase(), @@ -29,13 +16,17 @@ impl PluginLookup { } } - pub async fn resolve_manifest( + /// This looks up this reference in the current snapshot, but if the reference + /// is missing or incompatible with the given version of Spin and the current OS + /// and processor environment, then it tries to find a fallback version + /// in the snapshot that *will* work. This is the "eager to please" resolver. + pub(crate) async fn resolve_manifest( &self, - plugins_dir: &Path, + catalogue: &Catalogue, skip_compatibility_check: bool, spin_version: &str, ) -> PluginLookupResult { - let exact = self.resolve_manifest_exact(plugins_dir).await?; + let exact = self.resolve_manifest_exact(catalogue).await?; if skip_compatibility_check || self.version.is_some() || exact.is_compatible_spin_version(spin_version) @@ -43,10 +34,8 @@ impl PluginLookup { return Ok(exact); } - let store = crate::store::PluginStore::new(plugins_dir.to_owned()); - // TODO: This is very similar to some logic in the badger module - look for consolidation opportunities. - let manifests = store.catalogue_manifests()?; + let manifests = catalogue.manifests()?; let relevant_manifests = manifests.into_iter().filter(|m| m.name() == self.name); let compatible_manifests = relevant_manifests .filter(|m| m.has_compatible_package() && m.is_compatible_spin_version(spin_version)); @@ -56,29 +45,30 @@ impl PluginLookup { Ok(highest_compatible_manifest.unwrap_or(exact)) } - pub async fn resolve_manifest_exact( + /// This looks up this **exact** reference in the current snapshot. The snapshot + /// will not be refreshed, but it may be initialised if it does not yet exist. + /// Compatibility is not considered; no alternative versions are considered. + pub(crate) async fn resolve_manifest_exact( &self, - plugins_dir: &Path, + catalogue: &Catalogue, ) -> PluginLookupResult { let url = plugins_repo_url()?; tracing::info!("Pulling manifest for plugin {} from {url}", self.name); - fetch_plugins_repo(&url, plugins_dir, false) - .await - .map_err(|e| { - Error::ConnectionFailed(ConnectionFailedError::new(url.to_string(), e.to_string())) - })?; + catalogue.ensure_inited(&url).await.map_err(|e| { + Error::ConnectionFailed(ConnectionFailedError::new(url.to_string(), e.to_string())) + })?; - self.resolve_manifest_exact_from_good_repo(plugins_dir) + self.resolve_manifest_exact_from_good_repo(catalogue) } // This is split from resolve_manifest_exact because it may recurse (once) and that makes // Rust async sad. So we move the potential recursion to a sync helper. #[allow(clippy::let_and_return)] - pub fn resolve_manifest_exact_from_good_repo( + fn resolve_manifest_exact_from_good_repo( &self, - plugins_dir: &Path, + catalogue: &Catalogue, ) -> PluginLookupResult { - let expected_path = spin_plugins_repo_manifest_path(&self.name, &self.version, plugins_dir); + let expected_path = catalogue.manifest_path(&self.name, &self.version); let not_found = |e: std::io::Error| { Err(Error::NotFound(NotFoundError::new( @@ -100,7 +90,7 @@ impl PluginLookup { // If a user has asked for a version by number, and the path doesn't exist, // it _might_ be because it's the latest version. This checks for that case. let latest = Self::new(&self.name, None); - match latest.resolve_manifest_exact_from_good_repo(plugins_dir) { + match latest.resolve_manifest_exact_from_good_repo(catalogue) { Ok(manifest) if manifest.try_version().ok() == self.version => Ok(manifest), _ => not_found(e), } @@ -112,74 +102,16 @@ impl PluginLookup { } } -pub fn plugins_repo_url() -> Result { - Url::parse(SPIN_PLUGINS_REPO) -} - -#[cfg(not(test))] -fn accept_as_repo(git_root: &Path) -> bool { - git_root.join(".git").exists() -} - -#[cfg(test)] -fn accept_as_repo(git_root: &Path) -> bool { - git_root.join(".git").exists() || git_root.join("_spin_test_dot_git").exists() -} - -pub async fn fetch_plugins_repo( - repo_url: &Url, - plugins_dir: &Path, - update: bool, -) -> anyhow::Result<()> { - let git_root = plugin_manifests_repo_path(plugins_dir); - let git_source = GitSource::new(repo_url, None, &git_root); - if accept_as_repo(&git_root) { - if update { - git_source.pull().await?; - } - } else { - git_source.clone_repo().await?; - } - Ok(()) -} - -fn plugin_manifests_repo_path(plugins_dir: &Path) -> PathBuf { - plugins_dir.join(PLUGINS_REPO_LOCAL_DIRECTORY) -} - -// Given a name and option version, outputs expected file name for the plugin. -fn manifest_file_name_version(plugin_name: &str, version: &Option) -> String { - match version { - Some(v) => format!("{plugin_name}@{v}.json"), - None => manifest_file_name(plugin_name), - } -} - -/// Get expected path to the manifest of a plugin with a given name -/// and version within the spin-plugins repository -fn spin_plugins_repo_manifest_path( - plugin_name: &str, - plugin_version: &Option, - plugins_dir: &Path, -) -> PathBuf { - spin_plugins_repo_manifest_dir(plugins_dir) - .join(plugin_name) - .join(manifest_file_name_version(plugin_name, plugin_version)) -} - -pub fn spin_plugins_repo_manifest_dir(plugins_dir: &Path) -> PathBuf { - plugins_dir - .join(PLUGINS_REPO_LOCAL_DIRECTORY) - .join(PLUGINS_REPO_MANIFESTS_DIRECTORY) -} - fn null_version() -> semver::Version { semver::Version::new(0, 0, 0) } #[cfg(test)] mod tests { + use std::path::PathBuf; + use super::*; + use crate::store::PluginStore; const TEST_NAME: &str = "some-spin-ver-some-not"; const TESTS_STORE_DIR: &str = "tests"; @@ -188,11 +120,15 @@ mod tests { PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(TESTS_STORE_DIR) } + fn tests_store() -> Catalogue { + PluginStore::new(tests_store_dir()).catalogue() + } + #[tokio::test] async fn if_no_version_given_and_latest_is_compatible_then_latest() -> PluginLookupResult<()> { - let lookup = PluginLookup::new(TEST_NAME, None); + let lookup = PluginRef::new(TEST_NAME, None); let resolved = lookup - .resolve_manifest(&tests_store_dir(), false, "99.0.0") + .resolve_manifest(&tests_store(), false, "99.0.0") .await?; assert_eq!("99.0.1", resolved.version); Ok(()) @@ -204,9 +140,9 @@ mod tests { // NOTE: The setup assumes you are NOT running Windows on aarch64, so as to check 98.1.0 is not // offered. If that assumption fails then this test will fail with actual version being 98.1.0. // (We use this combination because the OS and architecture enums don't allow for fake operating systems!) - let lookup = PluginLookup::new(TEST_NAME, None); + let lookup = PluginRef::new(TEST_NAME, None); let resolved = lookup - .resolve_manifest(&tests_store_dir(), false, "98.0.0") + .resolve_manifest(&tests_store(), false, "98.0.0") .await?; assert_eq!("98.0.0", resolved.version); Ok(()) @@ -214,9 +150,9 @@ mod tests { #[tokio::test] async fn if_version_given_it_gets_used_regardless() -> PluginLookupResult<()> { - let lookup = PluginLookup::new(TEST_NAME, Some(semver::Version::parse("99.0.0").unwrap())); + let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("99.0.0").unwrap())); let resolved = lookup - .resolve_manifest(&tests_store_dir(), false, "98.0.0") + .resolve_manifest(&tests_store(), false, "98.0.0") .await?; assert_eq!("99.0.0", resolved.version); Ok(()) @@ -224,9 +160,9 @@ mod tests { #[tokio::test] async fn if_latest_version_given_it_gets_used_regardless() -> PluginLookupResult<()> { - let lookup = PluginLookup::new(TEST_NAME, Some(semver::Version::parse("99.0.1").unwrap())); + let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("99.0.1").unwrap())); let resolved = lookup - .resolve_manifest(&tests_store_dir(), false, "98.0.0") + .resolve_manifest(&tests_store(), false, "98.0.0") .await?; assert_eq!("99.0.1", resolved.version); Ok(()) @@ -234,9 +170,9 @@ mod tests { #[tokio::test] async fn if_no_version_given_but_skip_compat_then_highest() -> PluginLookupResult<()> { - let lookup = PluginLookup::new(TEST_NAME, None); + let lookup = PluginRef::new(TEST_NAME, None); let resolved = lookup - .resolve_manifest(&tests_store_dir(), true, "98.0.0") + .resolve_manifest(&tests_store(), true, "98.0.0") .await?; assert_eq!("99.0.1", resolved.version); Ok(()) @@ -244,9 +180,9 @@ mod tests { #[tokio::test] async fn if_non_existent_version_given_then_error() -> PluginLookupResult<()> { - let lookup = PluginLookup::new(TEST_NAME, Some(semver::Version::parse("177.7.7").unwrap())); + let lookup = PluginRef::new(TEST_NAME, Some(semver::Version::parse("177.7.7").unwrap())); lookup - .resolve_manifest(&tests_store_dir(), true, "99.0.0") + .resolve_manifest(&tests_store(), true, "99.0.0") .await .expect_err("Should have errored because plugin v177.7.7 does not exist"); Ok(()) diff --git a/crates/plugins/src/manager.rs b/crates/plugins/src/manager.rs index 6f394efd54..e1f655db4a 100644 --- a/crates/plugins/src/manager.rs +++ b/crates/plugins/src/manager.rs @@ -1,7 +1,7 @@ use crate::{ SPIN_INTERNAL_COMMANDS, error::*, - lookup::PluginLookup, + lookup::PluginRef, manifest::{PluginManifest, PluginPackage, warn_unsupported_version}, store::PluginStore, }; @@ -30,7 +30,7 @@ pub enum ManifestLocation { /// Plugin manifest should be pulled from a specific address. Remote(Url), /// Plugin manifest lives in the centralized plugins repository - PluginsRepository(PluginLookup), + PluginsRepository(PluginRef), } impl ManifestLocation { @@ -61,7 +61,11 @@ pub(crate) enum RawInstallRecord { Local { file: PathBuf }, } -/// Provides accesses to functionality to inspect and manage the installation of plugins. +/// The entry point for plugin functionality. Use this to list, install, and remove +/// plugins, and to locate plugin binaries for execution. +/// +/// PluginManager also provides access to the catalogue of available manifests via +/// the `catalogue()` function. It also provides for synchronised catalogue updates. pub struct PluginManager { store: PluginStore, } @@ -73,11 +77,6 @@ impl PluginManager { Ok(Self { store }) } - /// Returns the underlying store object - pub fn store(&self) -> &PluginStore { - &self.store - } - /// Installs the Spin plugin with the given manifest If installing a plugin from the centralized /// Spin plugins repository, it fetches the latest contents of the repository and searches for /// the appropriately named and versioned plugin manifest. Parses the plugin manifest to get the @@ -135,7 +134,7 @@ impl PluginManager { /// directory. /// Returns true if plugin was successfully uninstalled and false if plugin did not exist. pub fn uninstall(&self, plugin_name: &str) -> Result { - let plugin_store = self.store(); + let plugin_store = &self.store; let manifest_file = plugin_store.installed_manifest_path(plugin_name); let exists = manifest_file.exists(); if exists { @@ -168,7 +167,7 @@ impl PluginManager { } // Disallow reinstalling identical plugins and downgrading unless permitted. - if let Ok(installed) = self.store.read_plugin_manifest(&plugin_manifest.name()) { + if let Ok(installed) = self.get_installed_manifest(&plugin_manifest.name()) { if &installed == plugin_manifest { return Ok(InstallAction::NoAction { name: plugin_manifest.name(), @@ -251,30 +250,146 @@ impl PluginManager { } ManifestLocation::PluginsRepository(lookup) => { lookup - .resolve_manifest( - self.store().get_plugins_directory(), - skip_compatibility_check, - spin_version, - ) + .resolve_manifest(&self.catalogue(), skip_compatibility_check, spin_version) .await? } }; Ok(plugin_manifest) } - pub async fn update_lock(&self) -> PluginManagerUpdateLock { + /// Returns the PluginManifest for an installed plugin with a given name. + /// Looks up and parses the JSON plugin manifest file into object form. + pub fn get_installed_manifest(&self, plugin_name: &str) -> PluginLookupResult { + let manifest_path = self.store.installed_manifest_path(plugin_name); + tracing::info!("Reading plugin manifest from {}", manifest_path.display()); + let manifest_file = File::open(manifest_path.clone()).map_err(|e| { + Error::NotFound(NotFoundError::new( + Some(plugin_name.to_string()), + manifest_path.display().to_string(), + e.to_string(), + )) + })?; + let manifest = serde_json::from_reader(manifest_file).map_err(|e| { + Error::InvalidManifest(InvalidManifestError::new( + Some(plugin_name.to_string()), + manifest_path.display().to_string(), + e.to_string(), + )) + })?; + Ok(manifest) + } + + pub fn is_empty(&self) -> bool { + let manifests_dir = self.store.installed_manifests_directory(); + if !manifests_dir.exists() { + return true; + } + let Ok(mut rd) = manifests_dir.read_dir() else { + return true; + }; + rd.next().is_none() + } + + pub fn installed_plugins(&self) -> anyhow::Result> { + let manifests_dir = self.store.installed_manifests_directory(); + let manifest_paths = crate::util::json_files_in(&manifests_dir); + let manifests = manifest_paths + .iter() + .filter_map(|path| crate::util::try_read_manifest_from(path)) + .collect(); + Ok(manifests) + } + + pub async fn installed_plugins_latest_versions( + &self, + skip_compatibility_check: bool, + spin_version: &str, + auth_header_value: &Option, + ) -> anyhow::Result> { + let mut plugins = vec![]; + + let manifests_dir = self.store.installed_manifests_directory(); + + for plugin in std::fs::read_dir(manifests_dir)? { + let path = plugin?.path(); + let name = path + .file_stem() + .ok_or_else(|| anyhow!("No stem for path {}", path.display()))? + .to_str() + .ok_or_else(|| anyhow!("Cannot convert path {} stem to str", path.display()))? + .to_string(); + let manifest_location = + ManifestLocation::PluginsRepository(PluginRef::new(&name, None)); + let manifest = match self + .get_manifest( + &manifest_location, + skip_compatibility_check, + spin_version, + auth_header_value, + ) + .await + { + Err(Error::NotFound(e)) => { + tracing::info!("Could not upgrade plugin '{name}': {e:?}"); + continue; + } + Err(e) => return Err(e.into()), + Ok(m) => m, + }; + + plugins.push((manifest, manifest_location)); + } + + Ok(plugins) + } + + pub fn is_installed(&self, plugin_name: &str) -> bool { + self.installed_plugins() + .unwrap_or_default() + .iter() + .any(|m| m.name() == plugin_name) + } + + pub fn is_installed_exact(&self, manifest: &PluginManifest) -> bool { + match self.get_installed_manifest(&manifest.name()) { + Ok(m) => m.eq(manifest), + Err(_) => false, + } + } + + pub async fn update(&self) -> Result<()> { + let mut locker = self.update_lock().await; + let guard = locker.lock_updates(); + if guard.denied() { + anyhow::bail!("Another plugin update operation is already in progress"); + } + + let url = crate::catalogue::plugins_repo_url()?; + self.catalogue().fetch_from_remote(&url).await?; + Ok(()) + } + + async fn update_lock(&self) -> PluginManagerUpdateLock { let lock = self.update_lock_impl().await; PluginManagerUpdateLock::from(lock) } async fn update_lock_impl(&self) -> anyhow::Result> { - let plugins_dir = self.store().get_plugins_directory(); + let plugins_dir = self.store.get_plugins_directory(); tokio::fs::create_dir_all(plugins_dir).await?; let file = tokio::fs::File::create(plugins_dir.join(".updatelock")).await?; let locker = fd_lock::RwLock::new(file); Ok(locker) } + pub fn catalogue(&self) -> crate::Catalogue { + self.store.catalogue() + } + + pub fn installed_binary_path(&self, plugin_name: &str) -> PathBuf { + self.store.installed_binary_path(plugin_name) + } + fn write_install_record(&self, plugin_name: &str, source: &ManifestLocation) { let install_record_path = self.store.installation_record_file(plugin_name); @@ -340,18 +455,6 @@ pub enum InstallAction { NoAction { name: String, version: String }, } -/// Gets the appropriate package for the running OS and Arch if exists -pub fn get_package(plugin_manifest: &PluginManifest) -> Result<&PluginPackage> { - use std::env::consts::{ARCH, OS}; - plugin_manifest - .packages - .iter() - .find(|p| p.os.rust_name() == OS && p.arch.rust_name() == ARCH) - .ok_or_else(|| { - anyhow!("This plugin does not support this OS ({OS}) or architecture ({ARCH}).") - }) -} - async fn download_plugin( name: &str, temp_dir: &TempDir, diff --git a/crates/plugins/src/manifest.rs b/crates/plugins/src/manifest.rs index dc93207e08..76c04f8dd1 100644 --- a/crates/plugins/src/manifest.rs +++ b/crates/plugins/src/manifest.rs @@ -5,8 +5,6 @@ use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use url::Url; -use crate::PluginStore; - /// Expected schema of a plugin manifest. Should match the latest Spin plugin /// manifest JSON schema: /// @@ -57,15 +55,10 @@ impl PluginManifest { pub fn has_compatible_package(&self) -> bool { self.packages.iter().any(|p| p.matches_current_os_arch()) } + pub fn is_compatible_spin_version(&self, spin_version: &str) -> bool { is_version_compatible_enough(&self.spin_compatibility, spin_version).unwrap_or(false) } - pub fn is_installed_in(&self, store: &PluginStore) -> bool { - match store.read_plugin_manifest(&self.name) { - Ok(m) => m.eq(self), - Err(_) => false, - } - } pub fn try_version(&self) -> Result { semver::Version::parse(&self.version) @@ -80,6 +73,17 @@ impl PluginManifest { } None } + + /// Gets the appropriate package for the running OS and Arch if exists + pub fn get_package(&self) -> Result<&PluginPackage> { + use std::env::consts::{ARCH, OS}; + self.packages + .iter() + .find(|p| p.os.rust_name() == OS && p.arch.rust_name() == ARCH) + .ok_or_else(|| { + anyhow!("This plugin does not support this OS ({OS}) or architecture ({ARCH}).") + }) + } } /// Describes compatibility and location of a plugin source. diff --git a/crates/plugins/src/store.rs b/crates/plugins/src/store.rs index 0e84aeb2dd..186f80a79f 100644 --- a/crates/plugins/src/store.rs +++ b/crates/plugins/src/store.rs @@ -1,19 +1,22 @@ -use anyhow::{Context, Result}; +use anyhow::Result; use flate2::read::GzDecoder; use spin_common::data_dir::data_dir; use std::{ - ffi::OsStr, fs::{self, File}, path::{Path, PathBuf}, }; use tar::Archive; -use crate::{error::*, manifest::PluginManifest}; +use crate::{Catalogue, manifest::PluginManifest}; /// Directory where the manifests of installed plugins are stored. pub const PLUGIN_MANIFESTS_DIRECTORY_NAME: &str = "manifests"; const INSTALLATION_RECORD_FILE_NAME: &str = ".install.json"; +// Name of directory that contains the cloned centralized Spin plugins +// repository +const LOCAL_SNAPSHOT_PATH_IN_STORE: &str = ".spin-plugins"; + /// Houses utilities for getting the path to Spin plugin directories. pub struct PluginStore { root: PathBuf, @@ -38,6 +41,14 @@ impl PluginStore { self.root.join(plugin_name) } + pub fn installed_binary_path(&self, plugin_name: &str) -> PathBuf { + let mut binary = self.root.join(plugin_name).join(plugin_name); + if cfg!(target_os = "windows") { + binary.set_extension("exe"); + } + binary + } + /// Get the path to the manifests directory which contains the plugin manifests /// of all installed Spin plugins. pub fn installed_manifests_directory(&self) -> PathBuf { @@ -46,15 +57,7 @@ impl PluginStore { pub fn installed_manifest_path(&self, plugin_name: &str) -> PathBuf { self.installed_manifests_directory() - .join(manifest_file_name(plugin_name)) - } - - pub fn installed_binary_path(&self, plugin_name: &str) -> PathBuf { - let mut binary = self.root.join(plugin_name).join(plugin_name); - if cfg!(target_os = "windows") { - binary.set_extension("exe"); - } - binary + .join(crate::util::manifest_file_name(plugin_name)) } pub fn installation_record_file(&self, plugin_name: &str) -> PathBuf { @@ -63,84 +66,9 @@ impl PluginStore { .join(INSTALLATION_RECORD_FILE_NAME) } - pub fn installed_manifests(&self) -> Result> { - let manifests_dir = self.installed_manifests_directory(); - let manifest_paths = Self::json_files_in(&manifests_dir); - let manifests = manifest_paths - .iter() - .filter_map(|path| Self::try_read_manifest_from(path)) - .collect(); - Ok(manifests) - } - - // TODO: report errors on individuals - pub fn catalogue_manifests(&self) -> Result> { - // Structure: - // CATALOGUE_DIR (spin/plugins/.spin-plugins/manifests) - // |- foo - // | |- foo@0.1.2.json - // | |- foo@1.2.3.json - // | |- foo.json - // |- bar - // |- bar.json - let catalogue_dir = - crate::lookup::spin_plugins_repo_manifest_dir(self.get_plugins_directory()); - - // Catalogue directory doesn't exist so likely nothing has been installed. - if !catalogue_dir.exists() { - return Ok(Vec::new()); - } - - let plugin_dirs = catalogue_dir - .read_dir() - .context("reading manifest catalogue at {catalogue_dir:?}")? - .filter_map(|d| d.ok()) - .map(|d| d.path()) - .filter(|p| p.is_dir()); - let manifest_paths = plugin_dirs.flat_map(|path| Self::json_files_in(&path)); - let manifests: Vec<_> = manifest_paths - .filter_map(|path| Self::try_read_manifest_from(&path)) - .collect(); - Ok(manifests) - } - - fn try_read_manifest_from(manifest_path: &Path) -> Option { - let manifest_file = File::open(manifest_path).ok()?; - serde_json::from_reader(manifest_file).ok() - } - - fn json_files_in(dir: &Path) -> Vec { - let json_ext = Some(OsStr::new("json")); - match dir.read_dir() { - Err(_) => vec![], - Ok(rd) => rd - .filter_map(|de| de.ok()) - .map(|de| de.path()) - .filter(|p| p.is_file() && p.extension() == json_ext) - .collect(), - } - } - - /// Returns the PluginManifest for an installed plugin with a given name. - /// Looks up and parses the JSON plugin manifest file into object form. - pub fn read_plugin_manifest(&self, plugin_name: &str) -> PluginLookupResult { - let manifest_path = self.installed_manifest_path(plugin_name); - tracing::info!("Reading plugin manifest from {}", manifest_path.display()); - let manifest_file = File::open(manifest_path.clone()).map_err(|e| { - Error::NotFound(NotFoundError::new( - Some(plugin_name.to_string()), - manifest_path.display().to_string(), - e.to_string(), - )) - })?; - let manifest = serde_json::from_reader(manifest_file).map_err(|e| { - Error::InvalidManifest(InvalidManifestError::new( - Some(plugin_name.to_string()), - manifest_path.display().to_string(), - e.to_string(), - )) - })?; - Ok(manifest) + pub(crate) fn catalogue(&self) -> Catalogue { + let git_root = self.root.join(LOCAL_SNAPSHOT_PATH_IN_STORE); + Catalogue::new(git_root) } pub(crate) fn add_manifest(&self, plugin_manifest: &PluginManifest) -> Result<()> { @@ -170,8 +98,3 @@ impl PluginStore { Ok(()) } } - -/// Given a plugin name, returns the expected file name for the installed manifest -pub fn manifest_file_name(plugin_name: &str) -> String { - format!("{plugin_name}.json") -} diff --git a/crates/plugins/src/util.rs b/crates/plugins/src/util.rs new file mode 100644 index 0000000000..2c510624ec --- /dev/null +++ b/crates/plugins/src/util.rs @@ -0,0 +1,36 @@ +use crate::manifest::PluginManifest; +use std::{ + ffi::OsStr, + fs::File, + path::{Path, PathBuf}, +}; + +pub fn try_read_manifest_from(manifest_path: &Path) -> Option { + let manifest_file = File::open(manifest_path).ok()?; + serde_json::from_reader(manifest_file).ok() +} + +pub fn json_files_in(dir: &Path) -> Vec { + let json_ext = Some(OsStr::new("json")); + match dir.read_dir() { + Err(_) => vec![], + Ok(rd) => rd + .filter_map(|de| de.ok()) + .map(|de| de.path()) + .filter(|p| p.is_file() && p.extension() == json_ext) + .collect(), + } +} + +// Given a name and option version, outputs expected file name for the plugin. +pub fn manifest_file_name_version(plugin_name: &str, version: &Option) -> String { + match version { + Some(v) => format!("{plugin_name}@{v}.json"), + None => manifest_file_name(plugin_name), + } +} + +/// Given a plugin name, returns the expected file name for the installed manifest +pub fn manifest_file_name(plugin_name: &str) -> String { + format!("{plugin_name}.json") +} diff --git a/examples/spin-timer/Cargo.lock b/examples/spin-timer/Cargo.lock index 0b8e840070..e4a924ec11 100644 --- a/examples/spin-timer/Cargo.lock +++ b/examples/spin-timer/Cargo.lock @@ -2375,6 +2375,7 @@ dependencies = [ "hyper 1.8.1", "hyper-util", "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -4169,6 +4170,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -5112,6 +5114,7 @@ dependencies = [ "spin-factor-outbound-networking", "spin-factors", "spin-resource-table", + "spin-telemetry", "spin-world", "tokio", "tracing", @@ -5139,7 +5142,7 @@ dependencies = [ "spin-serde", "tracing", "url", - "webpki-roots", + "webpki-root-certs", ] [[package]] @@ -5164,6 +5167,7 @@ dependencies = [ "spin-factors", "spin-locked-app", "spin-resource-table", + "spin-telemetry", "spin-wasi-async", "spin-world", "tokio", @@ -5511,6 +5515,7 @@ dependencies = [ "opentelemetry-appender-tracing", "opentelemetry-otlp", "opentelemetry_sdk", + "reqwest", "terminal", "tracing", "tracing-opentelemetry", @@ -7128,9 +7133,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] diff --git a/src/commands/external.rs b/src/commands/external.rs index da9035dbc7..71aed89dfa 100644 --- a/src/commands/external.rs +++ b/src/commands/external.rs @@ -3,9 +3,9 @@ use crate::commands::plugins::{Install, update}; use crate::opts::PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG; use anyhow::{Result, anyhow}; use spin_common::ui::quoted_path; +use spin_plugins::PluginManager; use spin_plugins::{ - PluginStore, badger::BadgerChecker, error::Error as PluginError, - manifest::warn_unsupported_version, + badger::BadgerChecker, error::Error as PluginError, manifest::warn_unsupported_version, }; use std::io::{IsTerminal, stderr}; use std::{collections::HashMap, env, process}; @@ -54,16 +54,16 @@ pub async fn execute_external_subcommand( cmd: clap::Command, ) -> anyhow::Result<()> { let (plugin_name, args, override_compatibility_check) = parse_subcommand(subcmd)?; - let plugin_store = PluginStore::try_default()?; + let plugin_manager = PluginManager::try_default()?; let plugin_version = ensure_plugin_available( &plugin_name, - &plugin_store, + &plugin_manager, cmd, override_compatibility_check, ) .await?; - let binary = plugin_store.installed_binary_path(&plugin_name); + let binary = plugin_manager.installed_binary_path(&plugin_name); if !binary.exists() { return Err(anyhow!( "plugin executable {} is missing. Try uninstalling and installing the plugin '{}' again.", @@ -111,11 +111,11 @@ fn set_kill_on_ctrl_c(child: &tokio::process::Child) { async fn ensure_plugin_available( plugin_name: &str, - plugin_store: &PluginStore, + plugin_manager: &PluginManager, cmd: clap::Command, override_compatibility_check: bool, ) -> anyhow::Result> { - let plugin_version = match plugin_store.read_plugin_manifest(plugin_name) { + let plugin_version = match plugin_manager.get_installed_manifest(plugin_name) { Ok(manifest) => { if let Err(e) = warn_unsupported_version(&manifest, SPIN_VERSION, override_compatibility_check) @@ -127,7 +127,7 @@ async fn ensure_plugin_available( Some(manifest.version().to_owned()) } Err(PluginError::NotFound(e)) => { - consider_install(plugin_name, plugin_store, cmd, &e).await? + consider_install(plugin_name, plugin_manager, cmd, &e).await? } Err(e) => return Err(e.into()), }; @@ -136,7 +136,7 @@ async fn ensure_plugin_available( async fn consider_install( plugin_name: &str, - plugin_store: &PluginStore, + plugin_manager: &PluginManager, cmd: clap::Command, e: &spin_plugins::error::NotFoundError, ) -> anyhow::Result> { @@ -158,9 +158,9 @@ async fn consider_install( } if stderr().is_terminal() - && let Some(plugin) = match_catalogue_plugin(plugin_store, plugin_name) + && let Some(plugin) = match_catalogue_plugin(plugin_manager, plugin_name) { - let package = spin_plugins::manager::get_package(&plugin)?; + let package = plugin.get_package()?; if offer_install(&plugin, package)? { let plugin_installer = installer_for(plugin_name); plugin_installer.run().await?; @@ -211,12 +211,12 @@ fn installer_for(plugin_name: &str) -> Install { } fn match_catalogue_plugin( - plugin_store: &PluginStore, + plugin_manager: &PluginManager, plugin_name: &str, ) -> Option { use itertools::Itertools; - let Ok(known) = plugin_store.catalogue_manifests() else { + let Ok(known) = plugin_manager.catalogue().manifests() else { return None; }; let candidates = known diff --git a/src/commands/plugins.rs b/src/commands/plugins.rs index 66642100b2..49d28df3cb 100644 --- a/src/commands/plugins.rs +++ b/src/commands/plugins.rs @@ -1,16 +1,15 @@ // Needed for clap derive: https://github.com/clap-rs/clap/issues/4857 #![allow(clippy::almost_swapped)] -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand, ValueEnum}; use semver::Version; use spin_plugins::{ - error::Error, - lookup::{PluginLookup, fetch_plugins_repo, plugins_repo_url}, - manager::{self, InstallAction, ManifestLocation, PluginManager}, + PluginManager, PluginRef, + manager::{InstallAction, ManifestLocation}, manifest::{PluginManifest, PluginPackage}, }; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use url::Url; use crate::build_info::*; @@ -127,7 +126,7 @@ impl Install { (Some(path), None, None) => ManifestLocation::Local(path.to_path_buf()), (None, Some(url), None) => ManifestLocation::Remote(url.clone()), (None, None, Some(name)) => { - ManifestLocation::PluginsRepository(PluginLookup::new(name, self.version.clone())) + ManifestLocation::PluginsRepository(PluginRef::new(name, self.version.clone())) } _ => { return Err(anyhow::anyhow!( @@ -261,31 +260,30 @@ impl Upgrade { /// the catalogue and prompts user to choose which ones to upgrade. pub async fn run(self) -> Result<()> { let manager = PluginManager::try_default()?; - let manifests_dir = manager.store().installed_manifests_directory(); // Check if no plugins are currently installed - if !manifests_dir.exists() { + if manager.is_empty() { println!("No currently installed plugins to upgrade."); return Ok(()); } if self.all { - self.upgrade_all(manifests_dir).await + self.upgrade_all(&manager).await } else if self.name.is_none() && self.local_manifest_src.is_none() && self.remote_manifest_src.is_none() { // Default behavior (multiselect) - self.upgrade_multiselect().await + self.upgrade_multiselect(&manager).await } else { - self.upgrade_one().await + self.upgrade_one(&manager).await } } // Multiselect plugin upgrade experience - async fn upgrade_multiselect(self) -> Result<()> { - let catalogue_plugins = list_catalogue_plugins().await?; - let installed_plugins = list_installed_plugins()?; + async fn upgrade_multiselect(self, manager: &PluginManager) -> Result<()> { + let catalogue_plugins = list_catalogue_plugins(manager).await?; + let installed_plugins = list_installed_plugins(manager)?; let installed_in_catalogue: Vec<_> = installed_plugins .into_iter() @@ -306,10 +304,8 @@ impl Upgrade { // Getting only eligible plugins to upgrade for installed_plugin in installed_in_catalogue { let manager = PluginManager::try_default()?; - let manifest_location = ManifestLocation::PluginsRepository(PluginLookup::new( - &installed_plugin.name, - None, - )); + let manifest_location = + ManifestLocation::PluginsRepository(PluginRef::new(&installed_plugin.name, None)); // Attempt to get the manifest to check eligibility to upgrade if let Ok(manifest) = manager @@ -366,10 +362,8 @@ impl Upgrade { // Upgrade plugins selected for (installed_plugin, manifest) in plugins_selected { let manager = PluginManager::try_default()?; - let manifest_location = ManifestLocation::PluginsRepository(PluginLookup::new( - &installed_plugin.name, - None, - )); + let manifest_location = + ManifestLocation::PluginsRepository(PluginRef::new(&installed_plugin.name, None)); try_install( &manifest, @@ -387,37 +381,18 @@ impl Upgrade { } // Install the latest of all currently installed plugins - async fn upgrade_all(&self, manifests_dir: impl AsRef) -> Result<()> { - let manager = PluginManager::try_default()?; - for plugin in std::fs::read_dir(manifests_dir)? { - let path = plugin?.path(); - let name = path - .file_stem() - .ok_or_else(|| anyhow!("No stem for path {}", path.display()))? - .to_str() - .ok_or_else(|| anyhow!("Cannot convert path {} stem to str", path.display()))? - .to_string(); - let manifest_location = - ManifestLocation::PluginsRepository(PluginLookup::new(&name, None)); - let manifest = match manager - .get_manifest( - &manifest_location, - self.override_compatibility_check, - SPIN_VERSION, - &self.auth_header_value, - ) - .await - { - Err(Error::NotFound(e)) => { - tracing::info!("Could not upgrade plugin '{name}': {e:?}"); - continue; - } - Err(e) => return Err(e.into()), - Ok(m) => m, - }; + async fn upgrade_all(&self, manager: &PluginManager) -> Result<()> { + for (manifest, manifest_location) in manager + .installed_plugins_latest_versions( + self.override_compatibility_check, + SPIN_VERSION, + &self.auth_header_value, + ) + .await? + { try_install( &manifest, - &manager, + manager, self.yes_to_all, self.override_compatibility_check, self.downgrade, @@ -429,12 +404,11 @@ impl Upgrade { Ok(()) } - async fn upgrade_one(self) -> Result<()> { - let manager = PluginManager::try_default()?; + async fn upgrade_one(self, manager: &PluginManager) -> Result<()> { let manifest_location = match (self.local_manifest_src, self.remote_manifest_src) { (Some(path), None) => ManifestLocation::Local(path), (None, Some(url)) => ManifestLocation::Remote(url), - _ => ManifestLocation::PluginsRepository(PluginLookup::new( + _ => ManifestLocation::PluginsRepository(PluginRef::new( self.name .as_ref() .context("plugin name is required for upgrades")?, @@ -451,7 +425,7 @@ impl Upgrade { .await?; try_install( &manifest, - &manager, + manager, self.yes_to_all, self.override_compatibility_check, self.downgrade, @@ -475,7 +449,7 @@ impl Show { let manager = PluginManager::try_default()?; let manifest = manager .get_manifest( - &ManifestLocation::PluginsRepository(PluginLookup::new(&self.name, None)), + &ManifestLocation::PluginsRepository(PluginRef::new(&self.name, None)), false, SPIN_VERSION, &None, @@ -506,10 +480,8 @@ fn is_potential_upgrade(current: &PluginManifest, candidate: &PluginManifest) -> // Make list_installed_plugins and list_catalogue_plugins into 'free' module-level functions // in order to call them in Upgrade::upgrade_multiselect -fn list_installed_plugins() -> Result> { - let manager = PluginManager::try_default()?; - let store = manager.store(); - let manifests = store.installed_manifests()?; +fn list_installed_plugins(manager: &PluginManager) -> Result> { + let manifests = manager.installed_plugins()?; let descriptors = manifests .into_iter() .map(|m| PluginDescriptor { @@ -524,20 +496,19 @@ fn list_installed_plugins() -> Result> { Ok(descriptors) } -async fn list_catalogue_plugins() -> Result> { +async fn list_catalogue_plugins(manager: &PluginManager) -> Result> { if update_silent().await.is_err() { terminal::warn!("Couldn't update plugins registry cache - using most recent"); } - let manager = PluginManager::try_default()?; - let store = manager.store(); - let manifests = store.catalogue_manifests(); + let catalogue = manager.catalogue(); + let manifests = catalogue.manifests(); let descriptors = manifests? .into_iter() .map(|m| PluginDescriptor { name: m.name(), version: m.version().to_owned(), - installed: m.is_installed_in(store), + installed: manager.is_installed_exact(&m), compatibility: PluginCompatibility::for_current(&m), manifest: m, installed_version: None, @@ -546,9 +517,11 @@ async fn list_catalogue_plugins() -> Result> { Ok(descriptors) } -async fn list_catalogue_and_installed_plugins() -> Result> { - let catalogue = list_catalogue_plugins().await?; - let installed = list_installed_plugins()?; +async fn list_catalogue_and_installed_plugins( + manager: &PluginManager, +) -> Result> { + let catalogue = list_catalogue_plugins(manager).await?; + let installed = list_installed_plugins(manager)?; Ok(merge_plugin_lists(catalogue, installed)) } @@ -645,10 +618,12 @@ pub enum ListFormat { impl List { pub async fn run(self) -> Result<()> { + let manager = PluginManager::try_default()?; + let mut plugins = if self.installed { - list_installed_plugins() + list_installed_plugins(&manager) } else { - list_catalogue_and_installed_plugins().await + list_catalogue_and_installed_plugins(&manager).await }?; if self.summary { @@ -845,17 +820,7 @@ pub(crate) async fn update() -> Result<()> { pub(crate) async fn update_silent() -> Result<()> { let manager = PluginManager::try_default()?; - - let mut locker = manager.update_lock().await; - let guard = locker.lock_updates(); - if guard.denied() { - anyhow::bail!("Another plugin update operation is already in progress"); - } - - let plugins_dir = manager.store().get_plugins_directory(); - let url = plugins_repo_url()?; - fetch_plugins_repo(&url, plugins_dir, true).await?; - Ok(()) + manager.update().await } fn continue_to_install( @@ -906,7 +871,7 @@ async fn try_install( return Ok(false); } - let package = manager::get_package(manifest)?; + let package = manifest.get_package()?; if continue_to_install(manifest, package, yes_to_all)? { let installed = manager .install(manifest, package, source, auth_header_value) @@ -939,12 +904,12 @@ mod completions { return vec![]; }; - let Ok(catalogue_plugins) = plugin_manager.store().catalogue_manifests() else { + let Ok(catalogue_plugins) = plugin_manager.catalogue().manifests() else { return vec![]; }; let catalogue_names: HashSet<_> = catalogue_plugins.iter().map(|m| m.name()).collect(); - let Ok(installed_plugins) = plugin_manager.store().installed_manifests() else { + let Ok(installed_plugins) = plugin_manager.installed_plugins() else { return vec![]; }; let installed_names: HashSet<_> = installed_plugins.iter().map(|m| m.name()).collect(); @@ -961,7 +926,7 @@ mod completions { return vec![]; }; - let Ok(installed_plugins) = plugin_manager.store().installed_manifests() else { + let Ok(installed_plugins) = plugin_manager.installed_plugins() else { return vec![]; }; @@ -976,12 +941,12 @@ mod completions { return vec![]; }; - let Ok(catalogue_plugins) = plugin_manager.store().catalogue_manifests() else { + let Ok(catalogue_plugins) = plugin_manager.catalogue().manifests() else { return vec![]; }; let catalogue_names: HashSet<_> = catalogue_plugins.iter().map(|m| m.name()).collect(); - let Ok(installed_plugins) = plugin_manager.store().installed_manifests() else { + let Ok(installed_plugins) = plugin_manager.installed_plugins() else { return vec![]; }; let installed_names: HashSet<_> = installed_plugins.iter().map(|m| m.name()).collect(); diff --git a/src/commands/up.rs b/src/commands/up.rs index f7b5e69faf..3826403dfd 100644 --- a/src/commands/up.rs +++ b/src/commands/up.rs @@ -682,24 +682,19 @@ fn parse_env_var(s: &str) -> Result<(String, String)> { fn resolve_trigger_plugin(trigger_type: &str) -> Result { use crate::commands::plugins::PluginCompatibility; - use spin_plugins::manager::PluginManager; + use spin_plugins::PluginManager; let subcommand = format!("trigger-{trigger_type}"); let plugin_manager = PluginManager::try_default() .with_context(|| format!("Failed to access plugins looking for '{subcommand}'"))?; - let plugin_store = plugin_manager.store(); - let is_installed = plugin_store - .installed_manifests() - .unwrap_or_default() - .iter() - .any(|m| m.name() == subcommand); - if is_installed { + if plugin_manager.is_installed(&subcommand) { return Ok(subcommand); } - if let Some(known) = plugin_store - .catalogue_manifests() + if let Some(known) = plugin_manager + .catalogue() + .manifests() .unwrap_or_default() .iter() .find(|m| m.name() == subcommand) diff --git a/src/lib.rs b/src/lib.rs index 55f088baed..cd20130620 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -188,10 +188,10 @@ fn plugin_help_entries() -> Vec { } fn installed_plugin_help_entries() -> Vec { - let Ok(manager) = spin_plugins::manager::PluginManager::try_default() else { + let Ok(manager) = spin_plugins::PluginManager::try_default() else { return vec![]; }; - let Ok(manifests) = manager.store().installed_manifests() else { + let Ok(manifests) = manager.installed_plugins() else { return vec![]; };