wasmer_cli/commands/app/secrets/
create.rs

1use super::utils::Secret;
2use crate::{
3    commands::{AsyncCliCommand, app::util::AppIdentFlag},
4    config::WasmerEnv,
5};
6use anyhow::Context;
7use colored::Colorize;
8use dialoguer::theme::ColorfulTheme;
9use is_terminal::IsTerminal;
10use std::{
11    collections::{HashMap, HashSet},
12    path::{Path, PathBuf},
13};
14use wasmer_backend_api::WasmerClient;
15
16/// Create a new app secret.
17#[derive(clap::Parser, Debug)]
18pub struct CmdAppSecretsCreate {
19    /* --- Common flags --- */
20    #[clap(flatten)]
21    pub env: WasmerEnv,
22
23    /// Don't print any message.
24    #[clap(long)]
25    pub quiet: bool,
26
27    /// Do not prompt for user input.
28    #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
29    pub non_interactive: bool,
30
31    /* --- Flags --- */
32    /// The path to the directory where the config file for the application will be written to.
33    #[clap(long = "app-dir", conflicts_with = "app")]
34    pub app_dir_path: Option<PathBuf>,
35
36    #[clap(flatten)]
37    pub app_id: AppIdentFlag,
38
39    /// Path to a file with secrets stored in JSON format to create secrets from.
40    #[clap(long, name = "from-file", conflicts_with = "name")]
41    pub from_file: Option<PathBuf>,
42
43    /// Whether or not to redeploy the app after creating the secrets.
44    #[clap(long)]
45    pub redeploy: bool,
46
47    /* --- Parameters --- */
48    /// The name of the secret to create.
49    #[clap(name = "name")]
50    pub secret_name: Option<String>,
51
52    /// The value of the secret to create.
53    #[clap(name = "value")]
54    pub secret_value: Option<String>,
55}
56
57impl CmdAppSecretsCreate {
58    fn get_secret_name(&self) -> anyhow::Result<String> {
59        if let Some(name) = &self.secret_name {
60            return Ok(name.clone());
61        }
62
63        if self.non_interactive {
64            anyhow::bail!("No secret name given. Provide one as a positional argument.")
65        } else {
66            let theme = ColorfulTheme::default();
67            Ok(dialoguer::Input::with_theme(&theme)
68                .with_prompt("Enter the name of the secret")
69                .interact_text()?)
70        }
71    }
72
73    fn get_secret_value(&self) -> anyhow::Result<String> {
74        if let Some(value) = &self.secret_value {
75            return Ok(value.clone());
76        }
77
78        if self.non_interactive {
79            anyhow::bail!("No secret value given. Provide one as a positional argument.")
80        } else {
81            let theme = ColorfulTheme::default();
82            Ok(dialoguer::Input::with_theme(&theme)
83                .with_prompt("Enter the value of the secret")
84                .interact_text()?)
85        }
86    }
87
88    /// Given a list of secrets, checks if the given secrets already exist for the given app and
89    /// returns a list of secrets that must be upserted.
90    async fn filter_secrets(
91        &self,
92        client: &WasmerClient,
93        app_id: &str,
94        secrets: Vec<Secret>,
95    ) -> anyhow::Result<Vec<Secret>> {
96        let names = secrets.iter().map(|s| &s.name);
97        let app_secrets =
98            wasmer_backend_api::query::get_all_app_secrets_filtered(client, app_id, names).await?;
99        let mut sset = HashSet::<String>::from_iter(app_secrets.iter().map(|s| s.name.clone()));
100        let mut ret = HashMap::new();
101
102        for secret in secrets {
103            if sset.contains(&secret.name) {
104                if self.non_interactive {
105                    anyhow::bail!(
106                        "Cannot create secret '{}' as it already exists. Use the `update` command instead.",
107                        secret.name.bold()
108                    );
109                } else {
110                    if ret.contains_key(&secret.name) {
111                        eprintln!(
112                            "Secret '{}' appears twice in the input file.",
113                            secret.name.bold()
114                        );
115                    } else {
116                        eprintln!(
117                            "Secret '{}' already exists for the selected app.",
118                            secret.name.bold()
119                        );
120                    }
121                    let theme = ColorfulTheme::default();
122                    let res = dialoguer::Confirm::with_theme(&theme)
123                        .with_prompt("Do you want to update it?")
124                        .interact()?;
125
126                    if !res {
127                        eprintln!(
128                            "Cannot create secret '{}' as it already exists. Use the `update` command instead.",
129                            secret.name.bold()
130                        );
131                    }
132                }
133            }
134
135            sset.insert(secret.name.clone());
136            ret.insert(secret.name, secret.value);
137        }
138
139        Ok(ret
140            .into_iter()
141            .map(|(name, value)| Secret { name, value })
142            .collect())
143    }
144
145    async fn create(
146        &self,
147        client: &WasmerClient,
148        app_id: &str,
149        secrets: Vec<Secret>,
150    ) -> Result<(), anyhow::Error> {
151        let res = wasmer_backend_api::query::upsert_app_secrets(
152            client,
153            app_id,
154            secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())),
155        )
156        .await?;
157        let res = res.context(
158            "Backend did not return any payload to confirm the successful creation of the secret!",
159        )?;
160
161        if !res.success {
162            anyhow::bail!("Secret creation failed!")
163        } else {
164            if !self.quiet {
165                eprintln!("Succesfully created secret(s):");
166                for secret in &secrets {
167                    eprintln!("{}", secret.name.bold());
168                }
169
170                let should_redeploy = self.redeploy || {
171                    if !self.non_interactive && self.from_file.is_some() {
172                        let theme = ColorfulTheme::default();
173                        dialoguer::Confirm::with_theme(&theme)
174                            .with_prompt("Do you want to redeploy your app?")
175                            .interact()?
176                    } else {
177                        false
178                    }
179                };
180
181                if should_redeploy {
182                    wasmer_backend_api::query::redeploy_app_by_id(client, app_id).await?;
183                    eprintln!("{} Deployment complete", "ð–¥”".yellow().bold());
184                } else {
185                    eprintln!(
186                        "{}: In order for secrets to appear in your app, re-deploy it.",
187                        "Info".bold()
188                    );
189                }
190            }
191
192            Ok(())
193        }
194    }
195
196    async fn create_from_file(
197        &self,
198        client: &WasmerClient,
199        path: &Path,
200        app_id: &str,
201    ) -> anyhow::Result<(), anyhow::Error> {
202        let secrets = super::utils::read_secrets_from_file(path).await?;
203
204        let secrets = self.filter_secrets(client, app_id, secrets).await?;
205        self.create(client, app_id, secrets).await?;
206
207        Ok(())
208    }
209}
210
211#[async_trait::async_trait]
212impl AsyncCliCommand for CmdAppSecretsCreate {
213    type Output = ();
214
215    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
216        let client = self.env.client()?;
217        let app_id = super::utils::get_app_id(
218            &client,
219            self.app_id.app.as_ref(),
220            self.app_dir_path.as_ref(),
221            self.quiet,
222            self.non_interactive,
223        )
224        .await?;
225        if let Some(file) = &self.from_file {
226            self.create_from_file(&client, file, &app_id).await
227        } else {
228            let name = self.get_secret_name()?;
229            let value = self.get_secret_value()?;
230            let secret = Secret { name, value };
231            self.create(&client, &app_id, vec![secret]).await
232        }
233    }
234}