Skip to content
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 ability for plugins to be fetched from authenticated URLs #2896

Merged
merged 1 commit into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions crates/plugins/src/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use crate::{

use anyhow::{anyhow, bail, Context, Result};
use path_absolutize::Absolutize;
use reqwest::{header::HeaderMap, Client};
use serde::Serialize;
use spin_common::sha256;
use std::{
Expand Down Expand Up @@ -87,6 +88,7 @@ impl PluginManager {
plugin_manifest: &PluginManifest,
plugin_package: &PluginPackage,
source: &ManifestLocation,
auth_header_value: &Option<String>,
) -> Result<String> {
let target = plugin_package.url.to_owned();
let target_url = Url::parse(&target)?;
Expand All @@ -105,7 +107,15 @@ impl PluginManager {
);
}
}
_ => download_plugin(&plugin_manifest.name(), &temp_dir, &target).await?,
_ => {
download_plugin(
&plugin_manifest.name(),
&temp_dir,
&target,
auth_header_value,
)
.await?
}
};
verify_checksum(&plugin_tarball_path, &plugin_package.sha256)?;

Expand Down Expand Up @@ -185,11 +195,16 @@ impl PluginManager {
manifest_location: &ManifestLocation,
skip_compatibility_check: bool,
spin_version: &str,
auth_header_value: &Option<String>,
) -> PluginLookupResult<PluginManifest> {
let plugin_manifest = match manifest_location {
ManifestLocation::Remote(url) => {
tracing::info!("Pulling manifest for plugin from {url}");
reqwest::get(url.as_ref())
let client = Client::new();
client
.get(url.as_ref())
.headers(request_headers(auth_header_value)?)
.send()
.await
.map_err(|e| {
Error::ConnectionFailed(ConnectionFailedError::new(
Expand Down Expand Up @@ -334,9 +349,19 @@ pub fn get_package(plugin_manifest: &PluginManifest) -> Result<&PluginPackage> {
})
}

async fn download_plugin(name: &str, temp_dir: &TempDir, target_url: &str) -> Result<PathBuf> {
async fn download_plugin(
name: &str,
temp_dir: &TempDir,
target_url: &str,
auth_header_value: &Option<String>,
) -> Result<PathBuf> {
tracing::trace!("Trying to get tar file for plugin '{name}' from {target_url}");
let plugin_bin = reqwest::get(target_url).await?;
let client = Client::new();
let plugin_bin = client
.get(target_url)
.headers(request_headers(auth_header_value)?)
.send()
.await?;
if !plugin_bin.status().is_success() {
match plugin_bin.status() {
reqwest::StatusCode::NOT_FOUND => bail!("The download URL specified in the plugin manifest was not found ({target_url} returned HTTP error 404). Please contact the plugin author."),
Expand Down Expand Up @@ -364,6 +389,17 @@ fn verify_checksum(plugin_file: &Path, expected_sha256: &str) -> Result<()> {
}
}

/// Get the request headers for a call to the plugin API
///
/// If set, this will include the user provided authorization header.
fn request_headers(auth_header_value: &Option<String>) -> Result<HeaderMap> {
let mut headers = HeaderMap::new();
if let Some(auth_value) = auth_header_value {
headers.insert(reqwest::header::AUTHORIZATION, auth_value.parse()?);
}
Ok(headers)
}

#[cfg(test)]
mod tests {
use super::*;
Expand All @@ -385,6 +421,7 @@ mod tests {
&ManifestLocation::Local(PathBuf::from(
"../tests/nonexistent-url/nonexistent-url.json",
)),
&None,
)
.await;

Expand Down
1 change: 1 addition & 0 deletions src/commands/external.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ fn installer_for(plugin_name: &str) -> Install {
remote_manifest_src: None,
override_compatibility_check: false,
version: None,
auth_header_value: None,
}
}

Expand Down
40 changes: 37 additions & 3 deletions src/commands/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,11 @@ pub struct Install {
#[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)]
pub override_compatibility_check: bool,

/// Provide the value for the authorization header to be able to install a plugin from a private repository.
/// (e.g) --auth-header-value "Bearer <token>"
#[clap(long = "auth-header-value", requires = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT)]
karthik2804 marked this conversation as resolved.
Show resolved Hide resolved
pub auth_header_value: Option<String>,

/// Specific version of a plugin to be install from the centralized plugins
/// repository.
#[clap(
Expand Down Expand Up @@ -126,6 +131,7 @@ impl Install {
&manifest_location,
self.override_compatibility_check,
SPIN_VERSION,
&self.auth_header_value,
)
.await?;
try_install(
Expand All @@ -135,6 +141,7 @@ impl Install {
self.override_compatibility_check,
downgrade,
&manifest_location,
&self.auth_header_value,
)
.await?;
Ok(())
Expand Down Expand Up @@ -207,6 +214,11 @@ pub struct Upgrade {
#[clap(short = 'y', long = "yes", takes_value = false)]
pub yes_to_all: bool,

/// Provide the value for the authorization header to be able to install a plugin from a private repository.
/// (e.g) --auth-header-value "Bearer <token>"
#[clap(long = "auth-header-value", requires = PLUGIN_REMOTE_PLUGIN_MANIFEST_OPT)]
pub auth_header_value: Option<String>,

/// Overrides a failed compatibility check of the plugin with the current version of Spin.
#[clap(long = PLUGIN_OVERRIDE_COMPATIBILITY_CHECK_FLAG, takes_value = false)]
pub override_compatibility_check: bool,
Expand Down Expand Up @@ -288,7 +300,12 @@ impl Upgrade {

// Attempt to get the manifest to check eligibility to upgrade
if let Ok(manifest) = manager
.get_manifest(&manifest_location, false, SPIN_VERSION)
.get_manifest(
&manifest_location,
false,
SPIN_VERSION,
&self.auth_header_value,
)
.await
{
// Check if upgraded candidates have a newer version and if are compatible
Expand Down Expand Up @@ -341,7 +358,16 @@ impl Upgrade {
None,
));

try_install(&manifest, &manager, true, false, false, &manifest_location).await?;
try_install(
&manifest,
&manager,
true,
false,
false,
&manifest_location,
&self.auth_header_value,
)
.await?;
}

Ok(())
Expand All @@ -365,6 +391,7 @@ impl Upgrade {
&manifest_location,
self.override_compatibility_check,
SPIN_VERSION,
&self.auth_header_value,
)
.await
{
Expand All @@ -382,6 +409,7 @@ impl Upgrade {
self.override_compatibility_check,
self.downgrade,
&manifest_location,
&self.auth_header_value,
)
.await?;
}
Expand All @@ -405,6 +433,7 @@ impl Upgrade {
&manifest_location,
self.override_compatibility_check,
SPIN_VERSION,
&self.auth_header_value,
)
.await?;
try_install(
Expand All @@ -414,6 +443,7 @@ impl Upgrade {
self.override_compatibility_check,
self.downgrade,
&manifest_location,
&self.auth_header_value,
)
.await?;
Ok(())
Expand All @@ -434,6 +464,7 @@ impl Show {
&ManifestLocation::PluginsRepository(PluginLookup::new(&self.name, None)),
false,
SPIN_VERSION,
&None,
)
.await?;

Expand Down Expand Up @@ -789,6 +820,7 @@ async fn try_install(
override_compatibility_check: bool,
downgrade: bool,
source: &ManifestLocation,
auth_header_value: &Option<String>,
) -> Result<bool> {
let install_action = manager.check_manifest(
manifest,
Expand All @@ -804,7 +836,9 @@ async fn try_install(

let package = manager::get_package(manifest)?;
if continue_to_install(manifest, package, yes_to_all)? {
let installed = manager.install(manifest, package, source).await?;
let installed = manager
.install(manifest, package, source, auth_header_value)
.await?;
println!("Plugin '{installed}' was installed successfully!");

if let Some(description) = manifest.description() {
Expand Down
Loading