Skip to content

Commit 4e52721

Browse files
Merge pull request #735 from kate-goldenring/spin-plugins
feat: Add support for Spin plugins
2 parents fcf0803 + 9d85795 commit 4e52721

File tree

20 files changed

+1477
-7
lines changed

20 files changed

+1477
-7
lines changed

Cargo.lock

Lines changed: 33 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ spin-engine = { path = "crates/engine" }
3636
spin-http = { path = "crates/http" }
3737
spin-loader = { path = "crates/loader" }
3838
spin-manifest = { path = "crates/manifest" }
39+
spin-plugins = { path = "crates/plugins" }
3940
spin-publish = { path = "crates/publish" }
4041
spin-redis-engine = { path = "crates/redis" }
4142
spin-templates = { path = "crates/templates" }
@@ -77,6 +78,7 @@ members = [
7778
"crates/manifest",
7879
"crates/outbound-http",
7980
"crates/outbound-redis",
81+
"crates/plugins",
8082
"crates/redis",
8183
"crates/templates",
8284
"crates/testing",

crates/plugins/Cargo.toml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "spin-plugins"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
anyhow = "1.0"
8+
bytes = "1.1"
9+
dirs = "4.0"
10+
flate2 = "1.0"
11+
log = { version = "0.4", default-features = false }
12+
reqwest = { version = "0.11", features = ["json"] }
13+
semver = "1.0"
14+
serde = { version = "1.0", features = ["derive"] }
15+
serde_json = "1.0"
16+
sha2 = "0.10.2"
17+
tar = "0.4.38"
18+
tempfile = "3.3.0"
19+
thiserror = "1"
20+
tokio = { version = "1.10", features = [ "fs", "process", "rt", "macros" ] }
21+
url = "2.2.2"

crates/plugins/src/error.rs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
pub type PluginLookupResult<T> = std::result::Result<T, Error>;
2+
3+
/// Error message during plugin lookup or deserializing
4+
#[derive(Debug, thiserror::Error)]
5+
pub enum Error {
6+
#[error("{0}")]
7+
NotFound(NotFoundError),
8+
9+
#[error("{0}")]
10+
ConnectionFailed(ConnectionFailedError),
11+
12+
#[error("{0}")]
13+
InvalidManifest(InvalidManifestError),
14+
15+
#[error("URL parse error {0}")]
16+
UrlParseError(#[from] url::ParseError),
17+
}
18+
19+
/// Contains error details for when a plugin resource cannot be found at expected location
20+
#[derive(Debug)]
21+
pub struct NotFoundError {
22+
name: Option<String>,
23+
addr: String,
24+
err: String,
25+
}
26+
27+
impl NotFoundError {
28+
pub fn new(name: Option<String>, addr: String, err: String) -> Self {
29+
Self { name, addr, err }
30+
}
31+
}
32+
33+
impl std::fmt::Display for NotFoundError {
34+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35+
f.write_str(&format!(
36+
"plugin '{}' not found at expected location {}: {}",
37+
self.name.as_deref().unwrap_or_default(),
38+
self.addr,
39+
self.err
40+
))
41+
}
42+
}
43+
44+
/// Contains error details for when a plugin manifest cannot be properly serialized
45+
#[derive(Debug)]
46+
pub struct InvalidManifestError {
47+
name: Option<String>,
48+
addr: String,
49+
err: String,
50+
}
51+
52+
impl InvalidManifestError {
53+
pub fn new(name: Option<String>, addr: String, err: String) -> Self {
54+
Self { name, addr, err }
55+
}
56+
}
57+
58+
impl std::fmt::Display for InvalidManifestError {
59+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60+
f.write_str(&format!(
61+
"invalid manifest for plugin '{}' at {}: {}",
62+
self.name.clone().unwrap_or_default(),
63+
self.addr,
64+
self.err
65+
))
66+
}
67+
}
68+
69+
/// Contains error details for when there is an error getting a plugin resource from an address.
70+
#[derive(Debug)]
71+
pub struct ConnectionFailedError {
72+
addr: String,
73+
err: String,
74+
}
75+
76+
impl ConnectionFailedError {
77+
pub fn new(addr: String, err: String) -> Self {
78+
Self { addr, err }
79+
}
80+
}
81+
82+
impl std::fmt::Display for ConnectionFailedError {
83+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
84+
f.write_str(&format!(
85+
"failed to connect to endpoint {}: {}",
86+
self.addr, self.err
87+
))
88+
}
89+
}

crates/plugins/src/git.rs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
use anyhow::Result;
2+
use std::path::{Path, PathBuf};
3+
use tokio::process::Command;
4+
use url::Url;
5+
6+
const DEFAULT_BRANCH: &str = "main";
7+
8+
/// Enables cloning and fetching the latest of a git repository to a local
9+
/// directory.
10+
pub struct GitSource {
11+
/// Address to remote git repository.
12+
source_url: Url,
13+
/// Branch to clone/fetch.
14+
branch: String,
15+
/// Destination to clone repository into.
16+
git_root: PathBuf,
17+
}
18+
19+
impl GitSource {
20+
/// Creates a new git source
21+
pub fn new(source_url: &Url, branch: Option<String>, git_root: impl AsRef<Path>) -> GitSource {
22+
Self {
23+
source_url: source_url.clone(),
24+
branch: branch.unwrap_or_else(|| DEFAULT_BRANCH.to_owned()),
25+
git_root: git_root.as_ref().to_owned(),
26+
}
27+
}
28+
29+
/// Clones a contents of a git repository to a local directory
30+
pub async fn clone_repo(&self) -> Result<()> {
31+
let mut git = Command::new("git");
32+
git.args([
33+
"clone",
34+
self.source_url.as_ref(),
35+
"--branch",
36+
&self.branch,
37+
"--single-branch",
38+
])
39+
.arg(&self.git_root);
40+
let clone_result = git.output().await?;
41+
if !clone_result.status.success() {
42+
anyhow::bail!(
43+
"Error cloning Git repo {}: {}",
44+
self.source_url,
45+
String::from_utf8_lossy(&clone_result.stderr)
46+
)
47+
}
48+
Ok(())
49+
}
50+
51+
/// Fetches the latest changes from the source repository
52+
pub async fn pull(&self) -> Result<()> {
53+
let mut git = Command::new("git");
54+
git.arg("-C").arg(&self.git_root).arg("pull");
55+
let pull_result = git.output().await?;
56+
if !pull_result.status.success() {
57+
anyhow::bail!(
58+
"Error updating Git repo at {}: {}",
59+
self.git_root.display(),
60+
String::from_utf8_lossy(&pull_result.stderr)
61+
)
62+
}
63+
Ok(())
64+
}
65+
}

crates/plugins/src/lib.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
pub mod error;
2+
mod git;
3+
pub mod lookup;
4+
pub mod manager;
5+
pub mod manifest;
6+
mod prompt;
7+
mod store;
8+
pub use prompt::prompt_confirm_install;
9+
pub use store::PluginStore;
10+
11+
/// List of Spin internal subcommands
12+
pub(crate) const SPIN_INTERNAL_COMMANDS: [&str; 9] = [
13+
"templates",
14+
"up",
15+
"new",
16+
"bindle",
17+
"deploy",
18+
"build",
19+
"plugin",
20+
"trigger",
21+
"external",
22+
];

crates/plugins/src/lookup.rs

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
use crate::{error::*, git::GitSource, manifest::PluginManifest, store::manifest_file_name};
2+
use semver::Version;
3+
use std::{
4+
fs::File,
5+
path::{Path, PathBuf},
6+
};
7+
use url::Url;
8+
9+
// Name of directory that contains the cloned centralized Spin plugins
10+
// repository
11+
const PLUGINS_REPO_LOCAL_DIRECTORY: &str = ".spin-plugins";
12+
13+
// Name of directory containing the installed manifests
14+
const PLUGINS_REPO_MANIFESTS_DIRECTORY: &str = "manifests";
15+
16+
const SPIN_PLUGINS_REPO: &str = "https://github.com/fermyon/spin-plugins/";
17+
18+
/// Looks up plugin manifests in centralized spin plugin repository.
19+
pub struct PluginLookup {
20+
pub name: String,
21+
pub version: Option<Version>,
22+
}
23+
24+
impl PluginLookup {
25+
pub fn new(name: &str, version: Option<Version>) -> Self {
26+
Self {
27+
name: name.to_lowercase(),
28+
version,
29+
}
30+
}
31+
32+
pub async fn get_manifest_from_repository(
33+
&self,
34+
plugins_dir: &Path,
35+
) -> PluginLookupResult<PluginManifest> {
36+
let url = plugins_repo_url()?;
37+
log::info!("Pulling manifest for plugin {} from {url}", self.name);
38+
fetch_plugins_repo(&url, plugins_dir, false)
39+
.await
40+
.map_err(|e| {
41+
Error::ConnectionFailed(ConnectionFailedError::new(url.to_string(), e.to_string()))
42+
})?;
43+
let expected_path = spin_plugins_repo_manifest_path(&self.name, &self.version, plugins_dir);
44+
let file = File::open(&expected_path).map_err(|e| {
45+
Error::NotFound(NotFoundError::new(
46+
Some(self.name.clone()),
47+
expected_path.display().to_string(),
48+
e.to_string(),
49+
))
50+
})?;
51+
let manifest: PluginManifest = serde_json::from_reader(file).map_err(|e| {
52+
Error::InvalidManifest(InvalidManifestError::new(
53+
Some(self.name.clone()),
54+
expected_path.display().to_string(),
55+
e.to_string(),
56+
))
57+
})?;
58+
Ok(manifest)
59+
}
60+
}
61+
62+
pub fn plugins_repo_url() -> Result<Url, url::ParseError> {
63+
Url::parse(SPIN_PLUGINS_REPO)
64+
}
65+
66+
pub async fn fetch_plugins_repo(
67+
repo_url: &Url,
68+
plugins_dir: &Path,
69+
update: bool,
70+
) -> anyhow::Result<()> {
71+
let git_root = plugin_manifests_repo_path(plugins_dir);
72+
let git_source = GitSource::new(repo_url, None, &git_root);
73+
if git_root.join(".git").exists() {
74+
if update {
75+
git_source.pull().await?;
76+
}
77+
} else {
78+
git_source.clone_repo().await?;
79+
}
80+
Ok(())
81+
}
82+
83+
fn plugin_manifests_repo_path(plugins_dir: &Path) -> PathBuf {
84+
plugins_dir.join(PLUGINS_REPO_LOCAL_DIRECTORY)
85+
}
86+
87+
// Given a name and option version, outputs expected file name for the plugin.
88+
fn manifest_file_name_version(plugin_name: &str, version: &Option<semver::Version>) -> String {
89+
match version {
90+
Some(v) => format!("{}@{}.json", plugin_name, v),
91+
None => manifest_file_name(plugin_name),
92+
}
93+
}
94+
95+
/// Get expected path to the manifest of a plugin with a given name
96+
/// and version within the spin-plugins repository
97+
fn spin_plugins_repo_manifest_path(
98+
plugin_name: &str,
99+
plugin_version: &Option<Version>,
100+
plugins_dir: &Path,
101+
) -> PathBuf {
102+
plugins_dir
103+
.join(PLUGINS_REPO_LOCAL_DIRECTORY)
104+
.join(PLUGINS_REPO_MANIFESTS_DIRECTORY)
105+
.join(plugin_name)
106+
.join(manifest_file_name_version(plugin_name, plugin_version))
107+
}

0 commit comments

Comments
 (0)