depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Variables module.
//!
//! Variables allows you to modify your shell commands with no need to
//! edit project configuration file.

use anyhow::anyhow;
use env_file_reader::read_file;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::PathBuf;
use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
use vaultrs::kv2;

use crate::entities::custom_command::CustomCommand;
use crate::entities::environment::RunEnvironment;

/// Vault address environment variable's name.
pub const VAULT_ADDR_ENV: &str = "DEPLOYER_VAULT_ADDR";
/// Vault token environment variable's name.
pub const VAULT_ADDR_TOKEN: &str = "DEPLOYER_VAULT_TOKEN";

/// Variable.
///
/// Contains some metadata about variable value.
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone)]
pub struct Variable {
  /// Is the variable a secret.
  #[serde(default, skip_serializing_if = "crate::utils::is_false")]
  pub is_secret: bool,
  /// Variable value (will replace one of shell command's placeholders).
  pub value: VarValue,
}

/// Variable value type.
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone)]
#[serde(rename_all = "snake_case", tag = "type")]
#[allow(missing_docs)]
pub enum VarValue {
  /// Plain (stored inside this struct).
  Plain { value: String },
  /// Environment file's variable (specified by env file path and var's name).
  FromEnvFile(FromEnvFile),
  /// Environment variable (specified by env var's name).
  FromEnvVar { var_name: String },
  /// Variable returned by some custom command.
  FromCmd { cmd: String },
  /// Vault KV2 secret (specified by `mount_path` and `secret_path`).
  FromHcVaultKv2(Kv2Paths),
}

impl Variable {
  /// Creates new plain variable from given title and value.
  ///
  /// Note that plain variable can't be a secret in any safe way. It will be
  /// stored inside your project configuration.
  pub fn new_plain(value: &str) -> anyhow::Result<Self> {
    Ok(Self {
      is_secret: false,
      value: VarValue::Plain {
        value: value.to_string(),
      },
    })
  }

  /// Gets the variable's value.
  pub async fn get_value(&self, env: &RunEnvironment<'_>) -> anyhow::Result<String> {
    match &self.value {
      VarValue::Plain { value } => Ok(value.to_owned()),
      VarValue::FromEnvFile(info) => {
        let env_variables = read_file(&info.env_file_path)?;
        let val = env_variables
          .get(&info.key)
          .ok_or(anyhow!("There is no such key in your ENV file."))?;
        Ok(val.to_owned())
      }
      VarValue::FromEnvVar { var_name } => Ok(std::env::var(var_name)?),
      VarValue::FromCmd { cmd } => CustomCommand::run_simple(env, cmd)
        .await
        .map(|res| res.join("\n").trim().to_string()),
      VarValue::FromHcVaultKv2(info) => {
        let vault_addr = std::env::var(VAULT_ADDR_ENV)?;
        let vault_token = std::env::var(VAULT_ADDR_TOKEN)?;

        let client = VaultClient::new(
          VaultClientSettingsBuilder::default()
            .address(vault_addr)
            .token(vault_token)
            .build()?,
        )?;

        kv2::read(&client, &info.mount_path, &info.secret_path)
          .await
          .map_err(|e| anyhow!(e.to_string()))
      }
    }
  }
}

/// Environment file's variable metadata.
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone)]
pub struct FromEnvFile {
  /// Path to the environment variable's file.
  pub env_file_path: PathBuf,
  /// Name of the variable by which it's stored inside given file.
  pub key: String,
}

/// Vault KV2 secret's metadata.
///
/// See [KV2 docs](https://developer.hashicorp.com/vault/api-docs/secret/kv/kv-v2).
#[derive(Deserialize, Serialize, PartialEq, Eq, Hash, Clone)]
pub struct Kv2Paths {
  /// The path to the KV mount containing the secret to read.
  pub mount_path: String,
  /// Specifies the path of the secret to read.
  pub secret_path: String,
}

/// Some additional methods.
#[allow(missing_docs)]
pub trait VarTraits {
  fn is_secret(&self, title: &str) -> bool;
  fn titles(&self) -> Vec<String>;
  fn find(&self, title: &str) -> Option<Variable>;
}

impl VarTraits for BTreeMap<String, Variable> {
  /// Is the list containing a secret?
  fn is_secret(&self, title: &str) -> bool {
    self
      .iter()
      .find(|(t, _)| t.as_str().eq(title))
      .is_some_and(|(_, v)| v.is_secret)
  }

  /// Gets variable titles.
  fn titles(&self) -> Vec<String> {
    self.keys().map(|t| t.as_str().to_string()).collect::<Vec<_>>()
  }

  /// Searches the variable by given title.
  fn find(&self, title: &str) -> Option<Variable> {
    self.iter().find(|(t, _)| t.as_str().eq(title)).map(|(_, v)| v.clone())
  }
}