kcfg 0.1.0

KUBECONFIG manipulation CLI
use crate::common::*;
use crate::{error::KcfgError, KUBECONFIG};
use chrono::*;
use clap::{Clap, ValueHint};
use std::env;
use std::path::Path;

/// Options for the `fork` command
#[derive(Clap, Debug)]
pub struct ForkOption {
    /// Target directory for the forked configuration
    #[clap(short = 'p', long, value_hint = ValueHint::DirPath)]
    pub target_path: Option<String>,
    /// Overwrites any previous forked configuration. No timestamp will be used
    #[clap(short, long)]
    pub overwrite: bool,
    /// The forked file path will not be set as the new KUBECONFIG environment variable
    #[clap(short, long)]
    pub no_export: bool,
}

impl Default for ForkOption {
    fn default() -> Self {
        Self {
            target_path: None,
            overwrite: false,
            no_export: false,
        }
    }
}

/// Forks the current `KUBECONFIG` configuration
///
/// # Arguments
///
/// * `params` - options for the `fork` command
///
/// # Returns
///
/// A `String` containing the path for the forked file returned on success.
///
pub fn fork(params: ForkOption) -> Result<String, KcfgError> {
    // Retrieve KUBECONFIG env var
    let kubeconfig_path = env::var(KUBECONFIG).map_err(|e| KcfgError::EnvVarError {
        source: e,
        var: KUBECONFIG.to_string(),
    })?;
    // Check if KUBECONFIG exists and file
    check_file_path(&kubeconfig_path)?;

    let path = match params.target_path {
        Some(s) => {
            check_directory_path(&s)?;
            let file_name = Path::new(&kubeconfig_path)
                .file_name()
                .ok_or_else(|| KcfgError::FileNameDoesNotExist(kubeconfig_path.clone()))?
                .to_str()
                .ok_or_else(|| KcfgError::PathNotUnicode(kubeconfig_path.clone().into()))?;
            let target = Path::new(&s);
            target.join(file_name)
        }
        None => Path::new(&kubeconfig_path).to_path_buf(),
    };
    let path = path
        .to_str()
        .ok_or_else(|| KcfgError::PathNotUnicode(path.clone().into_os_string()))?;
    let mut forked_file_path = path.to_string();
    if !params.overwrite {
        let time = Utc::now().timestamp_millis();
        let timestamp = time.to_string();
        forked_file_path = format!("{}.{}", forked_file_path, timestamp);
    }
    forked_file_path = format!("{}.fork", forked_file_path);
    #[cfg(not(test))]
    std::fs::copy(kubeconfig_path, &forked_file_path)?;
    if !params.no_export {
        std::env::set_var(KUBECONFIG, &forked_file_path);
    }
    Ok(format!("export {}={}", KUBECONFIG, forked_file_path))
}

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

    #[test]
    fn works_with_no_param() {
        let params = ForkOption {
            target_path: None,
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", "README.md");
        let res = fork(params).unwrap();
        assert_eq!("export KUBECONFIG=README.md.fork".to_string(), res);
        assert_eq!(env::var(KUBECONFIG).unwrap(), "README.md.fork".to_string());
    }

    #[test]
    fn works_with_no_param_2() {
        let params = ForkOption {
            target_path: None,
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", "src/main.rs");
        let res = fork(params).unwrap();
        assert_eq!("export KUBECONFIG=src/main.rs.fork".to_string(), res);
        assert_eq!(
            env::var(KUBECONFIG).unwrap(),
            "src/main.rs.fork".to_string()
        );
    }

    #[test]
    fn works_with_target_path() {
        let params = ForkOption {
            target_path: Some("src/".to_string()),
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", "README.md");
        let res = fork(params).unwrap();
        assert_eq!("export KUBECONFIG=src/README.md.fork".to_string(), res);
        assert_eq!(
            env::var(KUBECONFIG).unwrap(),
            "src/README.md.fork".to_string()
        );
    }

    #[test]
    fn works_with_no_overwrite() {
        let params = ForkOption {
            target_path: Some("src/".to_string()),
            overwrite: false,
            ..Default::default()
        };
        let timestamp = Utc::now().timestamp_millis();
        env::set_var("KUBECONFIG", "README.md");
        let res = fork(params).unwrap();
        assert_eq!(
            format!("export KUBECONFIG=src/README.md.{}.fork", timestamp),
            res
        );
        assert_eq!(
            env::var(KUBECONFIG).unwrap(),
            format!("src/README.md.{}.fork", timestamp)
        );
    }

    #[test]
    fn works_with_no_export() {
        let params = ForkOption {
            target_path: Some("src/".to_string()),
            overwrite: false,
            no_export: true,
        };
        let timestamp = Utc::now().timestamp_millis();
        env::set_var("KUBECONFIG", "README.md");
        let res = fork(params).unwrap();
        assert_eq!(
            format!("export KUBECONFIG=src/README.md.{}.fork", timestamp),
            res
        );
        assert_eq!(env::var(KUBECONFIG).unwrap(), "README.md".to_string());
    }

    #[test]
    fn fail_with_file_target_path() {
        let params = ForkOption {
            target_path: Some("src/main.rs".to_string()),
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", "README.md");
        match fork(params) {
            Ok(_) => panic!("Should return an error"),
            Err(e) => {
                if !matches!(e, KcfgError::WrongDirectory(_)) {
                    panic!("Did not receive the expected error, got `{}`", e)
                }
            }
        }
    }

    #[test]
    fn fails_with_wrong_kubeconfig() {
        let params = ForkOption {
            target_path: None,
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", "src/");

        match fork(params) {
            Ok(_) => panic!("Should return an error"),
            Err(e) => {
                if !matches!(e, KcfgError::WrongFile(_)) {
                    panic!("Did not receive the expected error, got `{}`", e)
                }
            }
        }
    }

    #[test]
    fn fails_with_empty_kubeconfig() {
        let params = ForkOption {
            target_path: None,
            overwrite: true,
            ..Default::default()
        };
        env::set_var("KUBECONFIG", " ");

        match fork(params) {
            Ok(_) => panic!("Should return an error"),
            Err(e) => {
                if !matches!(e, KcfgError::PathDoesNotExist(_)) {
                    panic!("Did not receive the expected error, got `{}`", e)
                }
            }
        }
    }

    #[test]
    fn fails_with_missing_kubeconfig() {
        let params = ForkOption {
            target_path: None,
            overwrite: true,
            ..Default::default()
        };
        env::remove_var("KUBECONFIG");

        match fork(params) {
            Ok(_) => panic!("Should return an error"),
            Err(e) => {
                if !matches!(e, KcfgError::EnvVarError { .. }) {
                    panic!("Did not receive the expected error, got `{}`", e)
                }
            }
        }
    }
}