depl 2.4.3

Toolkit for a bunch of local and remote CI/CD actions
Documentation
//! Configurations module.
//!
//! Defines global and project configurations.

use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::path::PathBuf;

use crate::actions::staged::{Stage, StagedAction};
use crate::actions::{Action, DefinedAction};
use crate::bset;
use crate::entities::ansible_opts::AnsibleOpts;
use crate::entities::containered_opts::ContaineredOpts;
use crate::entities::custom_command::CustomCommand;
use crate::entities::github_cicd_opts::GitHubOpts;
use crate::entities::gitlab_cicd_opts::GitLabOpts;
use crate::entities::info::{Info, ShortName};
use crate::entities::remote_host::RemoteHost;
use crate::entities::requirements::Requirement;
use crate::pipelines::DescribedPipeline;

const CURRENT_GLOBAL_CONF_VERSION: u8 = 5;
/// Returns current global configuration version.
pub fn get_default_global_conf_version() -> u8 {
  CURRENT_GLOBAL_CONF_VERSION
}

/// Global Deployer's configuration.
#[derive(Deserialize, Serialize, Clone)]
pub struct DeployerGlobalConfig {
  /// Project list.
  #[serde(default, skip_serializing_if = "Vec::is_empty")]
  pub projects: Vec<String>,

  /// Available Actions Registry.
  #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
  pub actions_registry: BTreeSet<DefinedAction>,

  /// Available Pipelines Registry.
  #[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
  pub pipelines_registry: BTreeSet<DescribedPipeline>,

  /// Available remote hosts Registry.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub remote_hosts: BTreeMap<ShortName, RemoteHost>,

  /// Containered options registry.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub containered_registry: BTreeMap<Info, ContaineredOpts>,

  /// Ansible options registry.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub ansible_registry: BTreeMap<Info, AnsibleOpts>,

  /// GitHub Actions options registry.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub github_registry: BTreeMap<Info, GitHubOpts>,

  /// GitLab CI options registry.
  #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
  pub gitlab_registry: BTreeMap<Info, GitLabOpts>,

  /// Global configuration version
  #[serde(default = "get_default_global_conf_version")]
  pub version: u8,
}

#[derive(Deserialize, Serialize)]
struct Registries {
  actions: BTreeSet<DefinedAction>,
  pipelines: BTreeSet<DescribedPipeline>,
}

impl DeployerGlobalConfig {
  /// Appends global configuration by Actions that can't be added by TUI.
  pub fn make_sure_contain_defaults(actions_registry: &mut BTreeSet<DefinedAction>) {
    let info = Info::new("interrupt", "0.1.0").unwrap();
    actions_registry.insert(DefinedAction {
      info,
      tags: Default::default(),
      action: Action::Interrupt,
      requirements: Default::default(),
      exec_in_project_dir: None,
      skip_sync: None,
    });
  }

  /// Exports action & pipeline registries to a file.
  pub fn export_registries(&self, path: &str) -> anyhow::Result<()> {
    let registries = Registries {
      actions: self.actions_registry.clone(),
      pipelines: self.pipelines_registry.clone(),
    };
    let content = serde_pretty_yaml::to_string_pretty(&registries)?;
    std::fs::write(path, content)?;
    Ok(())
  }

  /// Imports action & pipeline registries from a file and merges with current registries.
  pub fn import_registries(&mut self, path: &str) -> anyhow::Result<()> {
    let content = std::fs::read_to_string(path)?;
    let registries = serde_pretty_yaml::from_str::<Registries>(&content)?;
    self.actions_registry = self.actions_registry.union(&registries.actions).cloned().collect();
    self.pipelines_registry = self.pipelines_registry.union(&registries.pipelines).cloned().collect();
    Ok(())
  }
}

impl Default for DeployerGlobalConfig {
  fn default() -> Self {
    let mut actions_registry = bset!();
    let pipelines_registry = bset!();
    DeployerGlobalConfig::make_sure_contain_defaults(&mut actions_registry);

    let info = Info::new("cargo-release", "0.1.0").unwrap();
    actions_registry.insert(DefinedAction {
      info,
      tags: vec!["rust".into(), "cargo".into()],
      action: Action::Staged(StagedAction {
        stage: Some(Stage::Build),
        command: CustomCommand {
          cmd: "cargo build --release".into(),
          placeholders: Default::default(),
          env: Default::default(),
          ignore_fails: None,
          show_success_output: None,
          show_cmd: None,
          only_when_fresh: None,
          remote_exec: Default::default(),
          daemon: None,
          daemon_wait_seconds: None,
        },
      }),
      requirements: vec![Requirement::ExistsAny {
        paths: [PathBuf::from("/bin/cargo"), PathBuf::from("~/.cargo/bin/cargo")]
          .into_iter()
          .collect::<BTreeSet<_>>(),
        desc: "Install `cargo` via your package manager or with this command: `curl https://sh.rustup.rs -sSf | sh`."
          .to_string(),
      }],
      exec_in_project_dir: None,
      skip_sync: None,
    });

    Self {
      actions_registry,
      pipelines_registry,
      projects: Default::default(),
      remote_hosts: Default::default(),
      containered_registry: Default::default(),
      ansible_registry: Default::default(),
      github_registry: Default::default(),
      gitlab_registry: Default::default(),
      version: CURRENT_GLOBAL_CONF_VERSION,
    }
  }
}

/// Extracts version of configuration file.
pub fn extract_version(value: impl AsRef<str>) -> anyhow::Result<u8> {
  let value = serde_pretty_yaml::from_str::<serde_pretty_yaml::Value>(value.as_ref())?;
  if value.is_mapping()
    && let Some(version) = value.get("version")
    && let Some(version) = version.as_u64()
  {
    return Ok(version as u8);
  }
  anyhow::bail!("Can't detect config version! Config file is invalid!")
}