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