cronback_cli/
args.rs

1use anyhow::{Context, Result};
2use async_trait::async_trait;
3use clap::clap_derive::Parser;
4use cronback_client::{
5    ClientBuilder,
6    Response,
7    BASE_URL_ENV,
8    DEFAULT_BASE_URL,
9};
10use once_cell::sync::OnceCell;
11use url::Url;
12
13#[cfg(feature = "admin")]
14use crate::admin;
15#[cfg(feature = "admin")]
16use crate::admin::RunAdminCommand;
17use crate::client::WrappedClient;
18use crate::ui::FancyToString;
19use crate::{runs, triggers, whoami, Command};
20
21const CRONBACK_SECRET_TOKEN_VAR: &str = "CRONBACK_SECRET_TOKEN";
22
23#[derive(Parser, Debug, Clone)]
24/// Command-line utility to manage cronback projects
25pub struct Cli {
26    #[clap(flatten)]
27    pub common: CommonOptions,
28    #[clap(flatten)]
29    pub verbose: clap_verbosity_flag::Verbosity,
30
31    #[command(subcommand)]
32    pub command: CliCommand,
33}
34
35#[derive(Parser, Debug, Clone)]
36pub struct CommonOptions {
37    #[arg(long, global = true)]
38    /// Connect to a local cronback service (http://localhost:8888)
39    pub localhost: bool,
40    #[arg(long, global = true, value_name = "URL", env(BASE_URL_ENV))]
41    pub base_url: Option<Url>,
42    // Unfortunately, we can't make this required **and** global at the same
43    // time. See [https://github.com/clap-rs/clap/issues/1546]
44    #[arg(
45        long,
46        value_name = "TOKEN",
47        env(CRONBACK_SECRET_TOKEN_VAR),
48        hide_env_values = true
49    )]
50    /// The API secret token. We attempt to read from `.env` if environment
51    /// variable is not set
52    pub secret_token: String,
53
54    #[arg(long, global = true)]
55    /// Displays a table with meta information about the response
56    pub show_meta: bool,
57    /// Ignore the confirmation prompt and always answer "yes"
58    #[arg(long, short, global = true)]
59    pub yes: bool,
60}
61
62#[derive(Parser, Debug, Clone)]
63pub enum CliCommand {
64    /// Commands for triggers
65    Triggers {
66        #[command(subcommand)]
67        command: TriggerCommand,
68    },
69    /// Commands for trigger runs
70    Runs {
71        #[command(subcommand)]
72        command: RunsCommand,
73    },
74    #[command(name = "whoami")]
75    /// Prints information about the current context/environment
76    WhoAmI(whoami::WhoAmI),
77
78    /// Set of commands that require admin privillages.
79    #[cfg(feature = "admin")]
80    Admin {
81        #[clap(flatten)]
82        admin_options: admin::AdminOptions,
83        #[command(subcommand)]
84        command: admin::AdminCommand,
85    },
86}
87
88#[derive(Parser, Debug, Clone)]
89pub enum TriggerCommand {
90    /// List triggers
91    #[command(visible_alias = "ls")]
92    List(triggers::List),
93    /// List runs of a trigger
94    #[command(visible_alias = "lr")]
95    ListRuns(triggers::ListRuns),
96    /// Create a new trigger
97    Create(triggers::Create),
98    /// View details about a given trigger
99    #[command(visible_alias = "v")]
100    View(triggers::View),
101    /// Cancel a scheduled trigger.
102    Cancel(triggers::Cancel),
103    /// Invoke an adhoc run for a given trigger
104    Run(triggers::Run),
105    /// Pause a scheduled trigger.
106    Pause(triggers::Pause),
107    /// Resume a paused trigger
108    Resume(triggers::Resume),
109    /// Delete a trigger
110    Delete(triggers::Delete),
111}
112
113#[derive(Parser, Debug, Clone)]
114pub enum RunsCommand {
115    /// View details about a given trigger run
116    View(runs::View),
117}
118
119impl CommonOptions {
120    pub fn base_url(&self) -> &Url {
121        if self.localhost {
122            static LOCALHOST_URL: OnceCell<Url> = OnceCell::new();
123            LOCALHOST_URL
124                .get_or_init(|| Url::parse("http://localhost:8888").unwrap())
125        } else {
126            self.base_url.as_ref().unwrap_or(&DEFAULT_BASE_URL)
127        }
128    }
129
130    pub fn new_client(&self) -> Result<WrappedClient> {
131        let base_url = self.base_url();
132        let inner = ClientBuilder::new()
133            .base_url(base_url.clone())
134            .context("Error while parsing base url")?
135            .secret_token(self.secret_token.clone())
136            .build()?;
137        Ok(WrappedClient {
138            common_options: self.clone(),
139            inner,
140        })
141    }
142
143    pub fn show_response_meta<T>(&self, response: &Response<T>) {
144        use colored::Colorize;
145        // Print extra information.
146        if self.show_meta {
147            eprintln!();
148            eprintln!(
149                "{}",
150                "<<-------------------------------------------------".green()
151            );
152            eprintln!("Path: {}", response.url());
153            eprintln!("Status Code: {}", response.status_code().fancy());
154            eprintln!(
155                "Request Id: {}",
156                response.request_id().clone().unwrap_or_default().green()
157            );
158            eprintln!(
159                "Project Id: {}",
160                response.project_id().clone().unwrap_or_default().green()
161            );
162            eprintln!(
163                "{}",
164                "-------------------------------------------------".green()
165            );
166            eprintln!();
167        }
168    }
169}
170
171// TODO: Macro-fy this.
172#[async_trait]
173impl Command for CliCommand {
174    async fn run<
175        A: tokio::io::AsyncWrite + Send + Sync + Unpin,
176        B: tokio::io::AsyncWrite + Send + Sync + Unpin,
177    >(
178        &self,
179        out: &mut tokio::io::BufWriter<A>,
180        err: &mut tokio::io::BufWriter<B>,
181        common_options: &CommonOptions,
182    ) -> Result<()> {
183        match self {
184            | CliCommand::Triggers { command } => {
185                command.run(out, err, common_options).await
186            }
187            | CliCommand::Runs { command } => {
188                command.run(out, err, common_options).await
189            }
190            #[cfg(feature = "admin")]
191            | CliCommand::Admin {
192                admin_options,
193                command,
194            } => command.run(out, err, common_options, admin_options).await,
195            | CliCommand::WhoAmI(c) => c.run(out, err, common_options).await,
196        }
197    }
198}