golem-cli 0.0.23

Command line interface for OSS version of Golem. See also golem-cloud-cli.
Documentation
use crate::context::ContextInfo;
use libtest_mimic::Failed;
use serde::de::DeserializeOwned;
use serde_json::Value;
use std::ffi::OsStr;
use std::fmt::Debug;
use std::io::{BufRead, BufReader};
use std::path::PathBuf;
use std::process::{Child, Command, Stdio};

#[derive(Debug, Clone)]
pub struct CliConfig {
    short_args: bool,
}

impl CliConfig {
    pub fn arg<S: Into<String>>(&self, short: char, long: S) -> String {
        if self.short_args {
            format!("-{short}")
        } else {
            format!("--{}", long.into())
        }
    }
}

pub trait Cli {
    fn run<'a, T: DeserializeOwned, S: AsRef<OsStr> + Debug>(
        &self,
        args: &[S],
    ) -> Result<T, Failed>;
    fn run_json<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<Value, Failed>;
    fn run_unit<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<(), Failed>;
    fn run_stdout<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<Child, Failed>;
}

#[derive(Debug, Clone)]
pub struct CliLive {
    pub config: CliConfig,
    golem_template_port: u16,
    golem_worker_port: u16,
    golem_cli_path: PathBuf,
}

impl CliLive {
    pub fn with_short_args(&self) -> Self {
        CliLive {
            config: CliConfig { short_args: true },
            golem_template_port: self.golem_template_port,
            golem_worker_port: self.golem_worker_port,
            golem_cli_path: self.golem_cli_path.clone(),
        }
    }

    pub fn with_long_args(&self) -> Self {
        CliLive {
            config: CliConfig { short_args: false },
            golem_template_port: self.golem_template_port,
            golem_worker_port: self.golem_worker_port,
            golem_cli_path: self.golem_cli_path.clone(),
        }
    }

    // TODO; Use NginxInfo
    pub fn make(context: &ContextInfo) -> Result<CliLive, Failed> {
        let golem_cli_path = PathBuf::from("./target/debug/golem-cli");

        println!(
            "CLI with template port {} and worker port {}",
            context.golem_template_service.local_http_port,
            context.golem_worker_service.local_http_port
        );

        if golem_cli_path.exists() {
            Ok(CliLive {
                config: CliConfig { short_args: false },
                golem_template_port: context.golem_template_service.local_http_port,
                golem_worker_port: context.golem_worker_service.local_http_port,
                golem_cli_path,
            })
        } else {
            Err(format!(
                "Expected to have precompiled Golem CLI at {}",
                golem_cli_path.to_str().unwrap_or("")
            )
            .into())
        }
    }

    fn template_base_url(&self) -> String {
        format!("http://localhost:{}", self.golem_template_port)
    }

    fn worker_base_url(&self) -> String {
        format!("http://localhost:{}", self.golem_worker_port)
    }

    fn run_inner<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<String, Failed> {
        println!(
            "Executing Golem CLI command: {} {args:?}",
            self.golem_cli_path.to_str().unwrap_or("")
        );

        let output = Command::new(&self.golem_cli_path)
            .env("GOLEM_TEMPLATE_BASE_URL", self.template_base_url())
            .env("GOLEM_WORKER_BASE_URL", self.worker_base_url())
            .arg(self.config.arg('F', "format"))
            .arg("json")
            .arg("-v")
            .args(args)
            .output()?;

        let stdout = String::from_utf8_lossy(output.stdout.as_slice()).to_string();
        let stderr = String::from_utf8_lossy(output.stderr.as_slice()).to_string();

        println!("CLI stdout: {stdout} for command {args:?}");
        println!("CLI stderr: {stderr} for command {args:?}");

        if !output.status.success() {
            return Err(format!(
                "golem cli failed with exit code: {:?}",
                output.status.code()
            )
            .into());
        }

        Ok(stdout)
    }
}

impl Cli for CliLive {
    fn run<'a, T: DeserializeOwned, S: AsRef<OsStr> + Debug>(
        &self,
        args: &[S],
    ) -> Result<T, Failed> {
        let stdout = self.run_inner(args)?;

        Ok(serde_json::from_str(&stdout)?)
    }

    fn run_json<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<Value, Failed> {
        let stdout = self.run_inner(args)?;

        Ok(serde_json::from_str(&stdout)?)
    }

    fn run_unit<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<(), Failed> {
        let _ = self.run_inner(args)?;
        Ok(())
    }

    fn run_stdout<S: AsRef<OsStr> + Debug>(&self, args: &[S]) -> Result<Child, Failed> {
        println!(
            "Executing Golem CLI command: {} {args:?}",
            self.golem_cli_path.to_str().unwrap_or("")
        );

        let mut child = Command::new(&self.golem_cli_path)
            .env("GOLEM_TEMPLATE_BASE_URL", self.template_base_url())
            .env("GOLEM_WORKER_BASE_URL", self.worker_base_url())
            .arg(self.config.arg('F', "format"))
            .arg("json")
            .args(args)
            .stdin(Stdio::piped())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped())
            .spawn()?;

        let stderr = child
            .stderr
            .take()
            .ok_or::<Failed>("Can't get golem cli stderr".into())?;

        std::thread::spawn(move || {
            let reader = BufReader::new(stderr);
            for line in reader.lines() {
                eprintln!("[golem-cloud-cli-stderr]   {}", line.unwrap())
            }
        });

        Ok(child)
    }
}