certsd 0.6.12

CertsD - automated, asynchronous LE certificate issuer.
Documentation
use crate::*;
use ron::de::*;

use serde::Deserialize;
use std::path::Path;
use tokio::fs::read_to_string;


#[derive(Debug, Clone, Deserialize, Default)]
pub struct Config {
    pub acme_staging: bool,
    pub notifications: Vec<NotifyWith>,
    pub accounts: Vec<CloudFlareAccount>,
}

#[derive(Debug, Clone, Deserialize, Default)]
pub struct CloudFlareAccount {
    pub cloudflare_api_token: String,
    pub cloudflare_zone_id: String,
    pub domain: String,
    pub contacts: Vec<String>,
}


const CONFIG_PATHS: [&str; 5] = [
    "/etc/certsd/certsd.conf",
    "/Services/Certsd/service.conf",
    "/Projects/certsd/certsd.conf",
    "/Volumes/Projects/certsd/certsd.conf",
    "certsd.conf",
];


impl Config {
    #[instrument]
    pub async fn config_file() -> String {
        CONFIG_PATHS
            .iter()
            .filter(|file| Path::new(file).exists())
            .take(1)
            .cloned()
            .collect()
    }


    #[instrument]
    pub async fn config_dir() -> String {
        let config_file = Self::config_file().await;
        match Path::new(&config_file).parent() {
            Some(path) if path.to_string_lossy().is_empty() => String::from("."),
            Some(path) => path.to_string_lossy().to_string(),
            None => String::from("."),
        }
    }


    #[instrument]
    pub async fn config_data_dir() -> Result<String> {
        let data_dir = format!("{}/certs", Config::config_dir().await);
        tokio::fs::create_dir_all(data_dir.to_owned()).await?;
        Ok(data_dir)
    }


    #[instrument]
    pub async fn load() -> Result<Config, SpannedError> {
        let config_file = Self::config_file().await;
        info!("Loading the configuration from: {config_file}");
        from_str::<Config>(&read_to_string(config_file).await?)
    }


    #[instrument]
    pub async fn from(config_file: &str) -> Result<Config, SpannedError> {
        info!("Loading the configuration from: {config_file}");
        from_str::<Config>(&read_to_string(config_file).await?)
    }


    #[instrument]
    pub async fn domains(&self) -> Vec<String> {
        self.accounts
            .iter()
            .cloned()
            .map(|accts| accts.domain)
            .collect()
    }


    #[instrument]
    pub async fn contacts_of(&self, domain: &str) -> Vec<String> {
        self.accounts
            .iter()
            .find(|&entry| entry.domain == domain)
            .cloned()
            .map(|entry| entry.contacts)
            .unwrap_or_default()
    }


    #[instrument]
    pub async fn api_token_of(&self, domain: &str) -> String {
        self.accounts
            .iter()
            .find(|&entry| entry.domain == domain)
            .cloned()
            .map(|entry| entry.cloudflare_api_token)
            .unwrap_or_default()
    }


    #[instrument]
    pub async fn zone_id_of(&self, domain: &str) -> String {
        self.accounts
            .iter()
            .find(|&entry| entry.domain == domain)
            .cloned()
            .map(|entry| entry.cloudflare_zone_id)
            .unwrap_or_default()
    }


    #[instrument]
    pub async fn notifications(&self) -> Vec<NotifyWith> {
        self.notifications.to_owned()
    }


    #[instrument]
    pub async fn acme_staging(&self) -> bool {
        self.acme_staging
    }
}


#[tokio::test]
async fn test_config_load() -> Result<()> {
    let config = Config::from("certsd.test.conf").await?;
    assert!(config.acme_staging().await);
    assert_eq!(
        config.domains().await,
        vec!["the-domain.com", "the-second-domain.com"]
    );

    let domain = "the-domain.com";
    assert_eq!(
        config.contacts_of(domain).await,
        ["me@example.com", "someone@example.com"]
    );
    let zone_id = config.zone_id_of(domain).await;
    assert_eq!(&zone_id, "the-zone-id");

    let domain = "the-second-domain.com";
    assert_eq!(config.contacts_of(domain).await, ["another.me@example.com"]);
    let zone_id = config.zone_id_of(domain).await;
    assert_eq!(&zone_id, "the-second-zone-id");
    let api_token = config.api_token_of(domain).await;
    assert_eq!(&api_token, "the-second-api-token");

    config.notifications.iter().for_each(|elem| {
        match elem {
            NotifyWith::Slack {
                webhook,
            } => {
                assert_eq!(
                    "https://hooks.slack.com/services/111111111/33333333333/44444444444444444",
                    webhook
                );
            }

            NotifyWith::Telegram {
                chat_id,
                token,
            } => {
                assert_eq!("@Public_Channel", chat_id);
                assert_eq!("1111111111111111111111111111111", token);
            }

            NotifyWith::None => {
                panic!("Shouldn't have None!");
            }
        }
    });

    Ok(())
}