wasmer_cli/commands/app/secrets/
update.rs1use 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#[derive(clap::Parser, Debug)]
18pub struct CmdAppSecretsUpdate {
19 #[clap(flatten)]
21 pub env: WasmerEnv,
22
23 #[clap(long)]
25 pub quiet: bool,
26
27 #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
29 pub non_interactive: bool,
30
31 #[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 #[clap(
41 long,
42 name = "from-file",
43 conflicts_with = "value",
44 conflicts_with = "name"
45 )]
46 pub from_file: Option<PathBuf>,
47
48 #[clap(long)]
50 pub redeploy: bool,
51
52 #[clap(name = "name")]
55 pub secret_name: Option<String>,
56
57 #[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 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}