devops-cli 0.2.1

devops cli tools
Documentation
#[cfg(target_os = "macos")]
use osascript::{Error, JavaScript};
use serde::{Deserialize, Serialize};
use thiserror::Error;

pub const TUNNELBLICK_CONFIG: &str = "tunnelblick";

type Result<T> = std::result::Result<T, TunnelblickError>;

#[cfg(target_os = "macos")]
pub fn get_status() -> Result<Vec<Vpn>> {
    let script = JavaScript::new(
        r##"
var tblk = Application('Tunnelblick')
var configs = []

var cfg = tblk.configurations().length
for(let i = 0;i<cfg;i++) {
  let c = tblk.configurations[i];
  configs.push({name: c.name(),  state: c.state()})
}
return configs
    "##,
    );

    Ok(script.execute()?)
}

#[cfg(not(target_os = "macos"))]
pub fn get_status() -> Result<Vec<Vpn>> {
    Err(TunnelblickError::UnsupportedPlatform)
}

#[cfg(target_os = "macos")]
pub fn connect(vpn_name: &str) -> Result<ChangeResult> {
    let result = JavaScript::new(
        r##"var changed = Application('Tunnelblick').connect($params);return {changed: changed};"##,
    )
    .execute_with_params(vpn_name)?;

    Ok(result)
}

#[cfg(not(target_os = "macos"))]
pub fn connect(_vpn_name: &str) -> Result<ChangeResult> {
    Err(TunnelblickError::UnsupportedPlatform)
}

#[cfg(target_os = "macos")]
pub fn disconnect(vpn_name: &str) -> Result<ChangeResult> {
    let result =
        JavaScript::new(r##"var changed = Application('Tunnelblick').disconnect($params);return {changed: changed};"##)
            .execute_with_params(vpn_name)?;

    Ok(result)
}

#[cfg(not(target_os = "macos"))]
pub fn disconnect(_vpn_name: &str) -> Result<ChangeResult> {
    Err(TunnelblickError::UnsupportedPlatform)
}

#[cfg(target_os = "macos")]
pub fn disconnect_all() -> Result<DisconnectResult> {
    let result = JavaScript::new(
        r##"var count = Application("Tunnelblick").disconnectAll();return {count: count};"##,
    )
    .execute()?;

    Ok(result)
}

#[cfg(not(target_os = "macos"))]
pub fn disconnect_all() -> Result<DisconnectResult> {
    Err(TunnelblickError::UnsupportedPlatform)
}

#[derive(Deserialize)]
pub struct ChangeResult {
    pub changed: bool,
}

#[derive(Deserialize)]
pub struct DisconnectResult {
    pub count: i32,
}

#[derive(Deserialize, Serialize, Eq, PartialEq)]
pub struct Vpn {
    pub name: String,
    pub state: State,
}

#[derive(Deserialize, Serialize, Eq, PartialEq, Debug)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum State {
    Connected,
    Auth,
    GetConfig,
    Exiting,
    Disconnecting,
    #[serde(other)]
    Unknown,
}

#[derive(Error, Debug)]
pub enum TunnelblickError {
    #[cfg(target_os = "macos")]
    #[error("Unable to parse response from tunnelblick")]
    ScriptResponseError(#[source] osascript::Error),

    #[cfg(target_os = "macos")]
    #[error("Unable to run osascript to control tunnelblick")]
    ScriptExecutionError(#[source] osascript::Error),

    #[cfg(target_os = "macos")]
    #[error("The script to control tunnelblick is not compatible with your version")]
    ScriptNotCompatible(#[source] osascript::Error),

    #[error("Tunnelblick is only supported on macOS")]
    UnsupportedPlatform,
}

#[cfg(target_os = "macos")]
impl From<osascript::Error> for TunnelblickError {
    fn from(e: Error) -> Self {
        match e {
            Error::Io(_) => TunnelblickError::ScriptExecutionError(e),
            Error::Json(_) => TunnelblickError::ScriptResponseError(e),
            Error::Script(_) => TunnelblickError::ScriptNotCompatible(e),
        }
    }
}

pub async fn wait_for_state<F>(
    wait: std::time::Duration,
    retries: u32,
    f: F,
) -> anyhow::Result<bool>
where
    F: Fn(Vec<Vpn>) -> anyhow::Result<bool>,
{
    for _ in 1..=retries {
        let status = get_status()?;
        match f(status) {
            Ok(false) => tokio::time::sleep(wait).await,
            failure_or_success => return failure_or_success,
        }
    }

    Ok(false)
}