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 std::io::IsTerminal as _;
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
165 let should_redeploy = self.redeploy || {
166 if !self.non_interactive && self.from_file.is_some() {
167 let theme = ColorfulTheme::default();
168 dialoguer::Confirm::with_theme(&theme)
169 .with_prompt("Do you want to redeploy your app?")
170 .interact()?
171 } else {
172 false
173 }
174 };
175
176 if should_redeploy {
177 wasmer_backend_api::query::redeploy_app_by_id(client, app_id).await?;
178 if !self.quiet {
179 eprintln!("{} Deployment complete", "ð–¥”".yellow().bold());
180 }
181 } else if !self.quiet {
182 eprintln!(
183 "{}: In order for secrets to appear in your app, re-deploy it.",
184 "Info".bold()
185 );
186 }
187
188 Ok(())
189 }
190 }
191
192 async fn update_from_file(
193 &self,
194 client: &WasmerClient,
195 path: &Path,
196 app_id: &str,
197 ) -> anyhow::Result<(), anyhow::Error> {
198 let secrets = super::utils::read_secrets_from_file(path).await?;
199
200 let secrets = self.filter_secrets(client, app_id, secrets).await?;
201 self.update(client, app_id, secrets).await?;
202
203 Ok(())
204 }
205}
206
207#[async_trait::async_trait]
208impl AsyncCliCommand for CmdAppSecretsUpdate {
209 type Output = ();
210
211 async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
212 let client = self.env.client()?;
213 let app_id = super::utils::get_app_id(
214 &client,
215 self.app_id.app.as_ref(),
216 self.app_dir_path.as_ref(),
217 self.quiet,
218 self.non_interactive,
219 )
220 .await?;
221
222 if let Some(file) = &self.from_file {
223 self.update_from_file(&client, file, &app_id).await
224 } else {
225 let name = self.get_secret_name()?;
226 let value = self.get_secret_value()?;
227 let secret = Secret { name, value };
228 self.update(&client, &app_id, vec![secret]).await
229 }
230 }
231}