diff --git a/crates/fluvio-cli/src/error.rs b/crates/fluvio-cli/src/error.rs index 428311c627..a7b8d592bb 100644 --- a/crates/fluvio-cli/src/error.rs +++ b/crates/fluvio-cli/src/error.rs @@ -47,6 +47,8 @@ pub enum CliError { #[error("Invalid argument: {0}")] InvalidArg(String), + #[error("Unknown error: {0}")] + Other(String), #[error("{0}")] CollectedError(String), #[error("Unexpected Infallible error")] diff --git a/crates/fluvio-cli/src/install/mod.rs b/crates/fluvio-cli/src/install/mod.rs new file mode 100644 index 0000000000..25a06164e5 --- /dev/null +++ b/crates/fluvio-cli/src/install/mod.rs @@ -0,0 +1 @@ +pub mod plugins; diff --git a/crates/fluvio-cli/src/install/plugins.rs b/crates/fluvio-cli/src/install/plugins.rs new file mode 100644 index 0000000000..353cc915da --- /dev/null +++ b/crates/fluvio-cli/src/install/plugins.rs @@ -0,0 +1,269 @@ +use std::str::FromStr; +use clap::Parser; +use tracing::debug; +use anyhow::Result; +use current_platform::CURRENT_PLATFORM; + +use fluvio_cli_common::error::{HttpError, PackageNotFound}; +use fluvio_cli_common::install::{ + fetch_latest_version, fetch_package_file, fluvio_extensions_dir, install_bin, install_println, + fluvio_bin_dir, +}; + +use fluvio_index::{PackageId, HttpAgent, MaybeVersion}; +use fluvio_channel::{LATEST_CHANNEL_NAME, FLUVIO_RELEASE_CHANNEL}; +use fluvio_hub_util as hubutil; +use hubutil::{HubAccess, HUB_API_BPKG_AUTH, INFINYON_HUB_REMOTE, FLUVIO_HUB_PROFILE_ENV}; +use hubutil::http::{self, StatusCode}; + +use crate::error::CliError; + +#[derive(Parser, Debug)] +pub struct InstallOpt { + /// The ID of a package to install, e.g. "fluvio/fluvio-cloud". + package: Option>, + /// Used for testing. Specifies alternate package location, e.g. "test/" + #[arg(hide = true, long)] + prefix: Option, + /// Install the latest prerelease rather than the latest release + /// + /// If the package ID contains a version (e.g. `fluvio/fluvio:0.6.0`), this is ignored + #[arg(long)] + pub develop: bool, + + /// When this flag is provided, use the hub. Dev-only + #[arg(long, hide_short_help = true)] + pub hub: bool, + + /// Use local hub defaults. + /// Implied if INFINYON_HUB_REMOTE or FLUVIO_CLOUD_PROFILE env vars are set + /// - Dev-only + #[arg(long, hide_short_help = true)] + pub use_hub_defaults: bool, + + /// When this flag is provided, use the hub. Dev-only + #[arg(long, hide_short_help = true)] + pub channel: Option, + + /// override default target arch determination + #[arg(long, hide_short_help = true)] + pub target: Option, +} + +impl InstallOpt { + pub async fn process(self) -> Result<()> { + println!("warning: `fluvio install` is deprecated, use `fvm install` instead."); + println!("Refer to https://www.fluvio.io/docs/get-started/linux/#install-fluvio-cli"); + + if self.hub { + debug!("Using the hub to install"); + + let mut homedir_path = fluvio_bin_dir()?; + let package_name; + //let binpath = "sample-bin-script"; + let bin_install_path = if let Some(ref p) = self.package { + let package = p; + package_name = package.name().to_string(); + homedir_path.push(package_name.clone()); + homedir_path.to_str().ok_or(crate::CliError::Other( + "Unable to render path to fluvio bin dir".to_string(), + ))? + } else { + return Err(crate::CliError::Other("No package name provided".to_string()).into()); + }; + debug!(?bin_install_path, "Install path"); + + let access_remote = if std::env::var(INFINYON_HUB_REMOTE).is_ok() + || std::env::var(FLUVIO_HUB_PROFILE_ENV).is_ok() + || self.use_hub_defaults + { + None + } else { + Some("https://hub.infinyon.cloud".to_string()) + }; + + let access = HubAccess::default_load(&access_remote).map_err(|_| { + crate::CliError::Other("Something happened getting hub dev info".to_string()) + })?; + let data = self.get_binary(&package_name, &access).await?; + + debug!(?bin_install_path, "Writing binary to fs"); + install_bin(bin_install_path, data)?; + } else { + let agent = match &self.prefix { + Some(prefix) => HttpAgent::with_prefix(prefix)?, + None => HttpAgent::default(), + }; + + let result = self.install_plugin(&agent).await; + match result { + Ok(_) => (), + Err(err) => match err.downcast_ref::() { + Some(crate::CliError::IndexError(fluvio_index::Error::MissingTarget( + target, + ))) => { + install_println(format!( + "❕ Package '{}' is not available for target {}, skipping", + self.package + .ok_or(crate::CliError::Other( + "Package name not provided".to_string(), + ))? + .name(), + target + )); + install_println("❕ Consider filing an issue to add support for this platform using the link below! 👇"); + install_println(format!( + "❕ https://github.com/infinyon/fluvio/issues/new?title=Support+fluvio-cloud+on+target+{target}" + )); + return Ok(()); + } + _ => return Err(err), + }, + } + } + + Ok(()) + } + + async fn install_plugin(&self, agent: &HttpAgent) -> Result<()> { + let target = if let Some(user_override) = &self.target { + fluvio_index::Target::from_str(&user_override.to_string())? + } else { + // need to analyze to if we can make CURRENT_PLATFORM + // the default instead of PACKAGE_TARGET, keep + // each use the same for now + fluvio_index::package_target()? + }; + + // If a version is given in the package ID, use it. Otherwise, use latest + let id = match self + .package + .clone() + .ok_or(crate::CliError::Other( + "Package name not provided".to_string(), + ))? + .maybe_version() + { + Some(version) => { + install_println(format!( + "⏳ Downloading package with provided version: {}...", + &self.package.clone().ok_or(crate::CliError::Other( + "Package name not provided".to_string(), + ))? + )); + let version = version.clone(); + self.package + .clone() + .ok_or(crate::CliError::Other( + "Package name not provided".to_string(), + ))? + .into_versioned(version) + } + None => { + let id = &self.package.clone().ok_or(crate::CliError::Other( + "Package name not provided".to_string(), + ))?; + install_println(format!("🎣 Fetching latest version for package: {id}...")); + let version = fetch_latest_version(agent, id, &target, self.develop).await?; + let id = id.clone().into_versioned(version.into()); + install_println(format!( + "⏳ Downloading package with latest version: {id}..." + )); + id + } + }; + + // Download the package file from the package registry + let package_result = fetch_package_file(agent, &id, &target).await; + let package_file = match package_result { + Ok(pf) => pf, + Err(err) => match err.downcast_ref::() { + Some(PackageNotFound { + package, + version, + target, + }) => { + install_println(format!( + "❕ Package {package} is not published at {version} for {target}, skipping" + )); + return Ok(()); + } + None => return Err(err), + }, + }; + install_println("🔑 Downloaded and verified package file"); + + // Install the package to the ~/.fluvio/bin/ dir + // If the plugin name doesn't start with `fluvio-`, then install it to the bin dir + let fluvio_dir = if id.name().to_string().starts_with("fluvio-") { + fluvio_extensions_dir()? + } else { + fluvio_bin_dir()? + }; + debug!("{fluvio_dir:#?}"); + + let package_filename = if target.to_string().contains("windows") { + format!("{}.exe", id.name().as_str()) + } else { + id.name().to_string() + }; + let package_path = fluvio_dir.join(package_filename); + install_bin(package_path, package_file)?; + + Ok(()) + } + + fn get_channel(&self) -> String { + if let Some(user_override) = &self.channel { + user_override.to_string() + } else if let Ok(channel_name) = std::env::var(FLUVIO_RELEASE_CHANNEL) { + channel_name + } else { + LATEST_CHANNEL_NAME.to_string() + } + } + + fn get_target(&self) -> String { + if let Some(user_override) = &self.target { + user_override.to_string() + } else { + CURRENT_PLATFORM.to_string() + } + } + + async fn get_binary(&self, bin_name: &str, access: &HubAccess) -> Result> { + let actiontoken = access + .get_bpkg_get_token() + .await + .map_err(|_| HttpError::InvalidInput("authorization error".into()))?; + + let binurl = format!( + "{}/{HUB_API_BPKG_AUTH}/{channel}/{systuple}/{bin_name}", + access.remote, + channel = self.get_channel(), + systuple = self.get_target(), + ); + debug!("Downloading binary from hub: {binurl}"); + let mut resp = http::get(binurl) + .header("Authorization", actiontoken) + .await + .map_err(|_| HttpError::InvalidInput("authorization error".into()))?; + + match resp.status() { + StatusCode::Ok => {} + code => { + let body_err_message = resp + .body_string() + .await + .unwrap_or_else(|_err| "couldn't fetch error message".to_string()); + let msg = format!("Status({code}) {body_err_message}"); + return Err(crate::CliError::HubError(msg).into()); + } + } + let data = resp + .body_bytes() + .await + .map_err(|_| crate::CliError::HubError("Data unpack failure".into()))?; + Ok(data) + } +} diff --git a/crates/fluvio-cli/src/lib.rs b/crates/fluvio-cli/src/lib.rs index 930b7921bd..9ee163c0ca 100644 --- a/crates/fluvio-cli/src/lib.rs +++ b/crates/fluvio-cli/src/lib.rs @@ -3,20 +3,16 @@ //! CLI configurations at the top of the tree mod error; -mod metadata; +pub mod client; +pub mod install; mod profile; -mod render; mod version; - -pub mod client; - +mod metadata; +mod render; pub(crate) mod monitoring; -// Re-exported pub(crate) use error::CliError; - use fluvio_extension_common as common; - pub(crate) const VERSION: &str = include_str!("../../../VERSION"); // list of public export @@ -56,9 +52,10 @@ mod root { #[cfg(feature = "k8s")] use fluvio_cluster::cli::ClusterCmd; use fluvio_cli_common::install::fluvio_extensions_dir; - use fluvio_channel::FLUVIO_RELEASE_CHANNEL; + use fluvio_channel::{FLUVIO_RELEASE_CHANNEL, LATEST_CHANNEL_NAME}; use crate::profile::ProfileOpt; + use crate::install::plugins::InstallOpt; use crate::client::FluvioCmd; use crate::metadata::{MetadataOpt, subcommand_metadata}; use crate::version::VersionOpt; @@ -121,6 +118,16 @@ mod root { #[command(subcommand, name = "cluster")] Cluster(Box), + /// Install Fluvio plugins + /// + /// The Fluvio CLI considers any executable with the prefix `fluvio-` to be a + /// CLI plugin. For example, an executable named `fluvio-foo` in your PATH may + /// be invoked by running `fluvio foo`. + /// + /// This command allows you to install plugins from Fluvio's package registry. + #[command(name = "install", hide = true)] + Install(InstallOpt), + /// Print Fluvio version information #[command(name = "version")] Version(VersionOpt), @@ -164,6 +171,17 @@ mod root { let version = semver::Version::parse(crate::VERSION).unwrap(); cluster.process(out, version, root.target).await?; } + Self::Install(mut install) => { + if let Ok(channel_name) = std::env::var(FLUVIO_RELEASE_CHANNEL) { + println!("Current channel: {}", &channel_name); + + if channel_name == LATEST_CHANNEL_NAME { + install.develop = true; + } + }; + + install.process().await?; + } Self::Version(version) => { version.process(root.target).await?; }