fluvio-cluster 0.7.1

Tools for installing and managing Fluvio clusters
Documentation
use tracing::{info, debug, instrument};
use derive_builder::Builder;
use crate::{
    ChartLocation, DEFAULT_CHART_APP_REPO, DEFAULT_CHART_SYS_REPO, DEFAULT_NAMESPACE,
    DEFAULT_CHART_REMOTE,
};
use fluvio_helm::{HelmClient, InstallArg};
use std::path::{Path, PathBuf};
use crate::error::SysInstallError;

const DEFAULT_SYS_NAME: &str = "fluvio-sys";
const DEFAULT_CHART_SYS_NAME: &str = "fluvio/fluvio-sys";
const DEFAULT_CLOUD_NAME: &str = "minikube";

/// Configuration options for installing Fluvio system charts
#[derive(Builder, Debug, Clone)]
#[builder(build_fn(private, name = "build_impl"))]
pub struct SysConfig {
    /// The type of cloud infrastructure the cluster will be running on
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::SysConfigBuilder;
    /// # fn add_cloud(builder: &mut SysConfigBuilder) {
    /// builder.cloud("minikube");
    /// # }
    /// ```
    #[builder(setter(into), default = "DEFAULT_CLOUD_NAME.to_string()")]
    pub cloud: String,
    /// The namespace in which to install the system chart
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::SysConfigBuilder;
    /// # fn add_namespace(builder: &mut SysConfigBuilder) {
    /// builder.namespace("fluvio");
    /// # }
    /// ```
    #[builder(setter(into), default = "DEFAULT_NAMESPACE.to_string()")]
    pub namespace: String,
    /// The location at which to find the system chart to install
    #[builder(default = "ChartLocation::Remote(DEFAULT_CHART_REMOTE.to_string())")]
    pub chart_location: ChartLocation,
    /// The version of the system chart to install (REQUIRED).
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::SysConfigBuilder;
    /// # fn example(builder: &mut SysConfigBuilder) {
    /// builder.chart_version("0.6.1");
    /// # }
    /// ```
    #[builder(setter(into))]
    pub chart_version: String,
}

impl SysConfig {
    /// Creates a default [`SysConfigBuilder`].
    ///
    /// The required argument `chart_version` must be provdied when
    /// constructing the builder.
    ///
    /// # Example
    ///
    /// ```
    /// use fluvio_cluster::SysConfig;
    /// let builder = SysConfig::builder("0.7.0-alpha.1");
    /// ```
    pub fn builder<S: Into<String>>(chart_version: S) -> SysConfigBuilder {
        let mut builder = SysConfigBuilder::default();
        builder.chart_version(chart_version);
        builder
    }
}

impl SysConfigBuilder {
    /// Validates all builder options and constructs a `SysConfig`
    pub fn build(&self) -> Result<SysConfig, SysInstallError> {
        let config = self
            .build_impl()
            .map_err(SysInstallError::MissingRequiredConfig)?;
        Ok(config)
    }

    /// The local chart location to install sys charts from
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::SysConfigBuilder;
    /// # fn add_local_chart(builder: &mut SysConfigBuilder) {
    /// builder.local_chart("./helm/fluvio-sys");
    /// # }
    /// ```
    pub fn local_chart<P: Into<PathBuf>>(&mut self, path: P) -> &mut Self {
        self.chart_location = Some(ChartLocation::Local(path.into()));
        self
    }

    /// The remote chart location to install sys charts from
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::SysConfigBuilder;
    /// # fn add_remote_chart(builder: &mut SysConfigBuilder) {
    /// builder.remote_chart("https://charts.fluvio.io");
    /// # }
    /// ```
    pub fn remote_chart<S: Into<String>>(&mut self, location: S) -> &mut Self {
        self.chart_location = Some(ChartLocation::Remote(location.into()));
        self
    }

    /// A builder helper for conditionally setting options
    ///
    /// This is useful for maintaining a fluid call chain even when
    /// we only want to set certain options conditionally and the
    /// conditions are more complicated than a simple boolean.
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::{SysConfig, SysInstallError};
    /// enum NamespaceCandidate {
    ///     UserGiven(String),
    ///     System,
    ///     Default,
    /// }
    /// fn make_config(ns: NamespaceCandidate) -> Result<SysConfig, SysInstallError> {
    ///     let config = SysConfig::builder("0.7.0-alpha.1")
    ///         .with(|builder| match &ns {
    ///             NamespaceCandidate::UserGiven(user) => builder.namespace(user),
    ///             NamespaceCandidate::System => builder.namespace("system"),
    ///             NamespaceCandidate::Default => builder,
    ///         })
    ///         .build()?;
    ///     Ok(config)
    /// }
    /// ```
    pub fn with<F>(&mut self, f: F) -> &mut Self
    where
        F: Fn(&mut Self) -> &mut Self,
    {
        f(self)
    }

    /// A builder helper for conditionally setting options
    ///
    /// This is useful for maintaining a builder call chain even when you
    /// only want to apply some options conditionally based on a boolean value.
    ///
    /// # Example
    ///
    /// ```
    /// # use fluvio_cluster::{SysInstallError, SysConfig};
    /// # fn example() -> Result<(), SysInstallError> {
    /// let custom_namespace = false;
    /// let config = SysConfig::builder("0.7.0-alpha.1")
    ///     // Custom namespace is not applied
    ///     .with_if(custom_namespace, |builder| builder.namespace("my-namespace"))
    ///     .build()?;
    /// # Ok(())
    /// # }
    /// ```
    pub fn with_if<F>(&mut self, cond: bool, f: F) -> &mut Self
    where
        F: Fn(&mut Self) -> &mut Self,
    {
        if cond {
            f(self)
        } else {
            self
        }
    }
}

/// Installs or upgrades the Fluvio system charts
///
/// # Example
///
/// ```
/// # use fluvio_cluster::{SysInstallError, SysConfig, SysInstaller};
/// # fn example() -> Result<(), SysInstallError> {
/// let config = SysConfig::builder("0.7.0-alpha.1")
///     .namespace("fluvio")
///     .chart_version("0.7.0-alpha.1")
///     .build()?;
/// let installer = SysInstaller::from_config(config)?;
/// installer.install()?;
/// # Ok(())
/// # }
/// ```
#[derive(Debug)]
pub struct SysInstaller {
    config: SysConfig,
    helm_client: HelmClient,
}

impl SysInstaller {
    /// Create a new `SysInstaller` using the given config
    pub fn from_config(config: SysConfig) -> Result<Self, SysInstallError> {
        let helm_client = HelmClient::new()?;
        Ok(Self {
            config,
            helm_client,
        })
    }

    /// Install the Fluvio System chart on the configured cluster
    pub fn install(&self) -> Result<(), SysInstallError> {
        self.process(false)
    }

    /// Upgrade the Fluvio System chart on the configured cluster
    pub fn upgrade(&self) -> Result<(), SysInstallError> {
        self.process(true)
    }

    /// Tells whether a system chart with the configured details is already installed
    pub fn is_installed(&self) -> Result<bool, SysInstallError> {
        let sys_charts = self
            .helm_client
            .get_installed_chart_by_name(DEFAULT_CHART_SYS_REPO, None)?;
        Ok(!sys_charts.is_empty())
    }

    #[instrument(skip(self))]
    fn process(&self, upgrade: bool) -> Result<(), SysInstallError> {
        let settings = vec![("cloud".to_owned(), self.config.cloud.to_owned())];
        match &self.config.chart_location {
            ChartLocation::Remote(chart_location) => {
                self.process_remote_chart(chart_location, upgrade, settings)?;
            }
            ChartLocation::Local(chart_home) => {
                self.process_local_chart(chart_home, upgrade, settings)?;
            }
        }

        info!("Fluvio sys chart has been installed");
        Ok(())
    }

    #[instrument(skip(self, upgrade))]
    fn process_remote_chart(
        &self,
        chart: &str,
        upgrade: bool,
        settings: Vec<(String, String)>,
    ) -> Result<(), SysInstallError> {
        debug!(?chart, "Using remote helm chart:");
        self.helm_client.repo_add(DEFAULT_CHART_APP_REPO, chart)?;
        self.helm_client.repo_update()?;
        let args = InstallArg::new(DEFAULT_CHART_SYS_REPO, DEFAULT_CHART_SYS_NAME)
            .namespace(&self.config.namespace)
            .version(&self.config.chart_version)
            .opts(settings)
            .develop();
        if upgrade {
            self.helm_client.upgrade(&args)?;
        } else {
            self.helm_client.install(&args)?;
        }
        Ok(())
    }

    #[instrument(skip(self, upgrade))]
    fn process_local_chart(
        &self,
        chart_home: &Path,
        upgrade: bool,
        settings: Vec<(String, String)>,
    ) -> Result<(), SysInstallError> {
        let chart_location = chart_home.join(DEFAULT_SYS_NAME);
        let chart_string = chart_location.to_string_lossy();
        debug!(chart_location = %chart_location.display(), "Using local helm chart:");

        let args = InstallArg::new(DEFAULT_CHART_SYS_REPO, chart_string)
            .namespace(&self.config.namespace)
            .version(&self.config.chart_version)
            .develop()
            .opts(settings);
        if upgrade {
            self.helm_client.upgrade(&args)?;
        } else {
            self.helm_client.install(&args)?;
        }
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_build_config() {
        let config: SysConfig = SysConfig::builder("0.7.0-alpha.1")
            .build()
            .expect("should build config with required options");
        assert_eq!(config.chart_version, "0.7.0-alpha.1");
    }
}