wasmer_cli/commands/app/secrets/
create.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::{HashMap, HashSet},
12 path::{Path, PathBuf},
13};
14use wasmer_backend_api::WasmerClient;
15
16#[derive(clap::Parser, Debug)]
18pub struct CmdAppSecretsCreate {
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(long, name = "from-file", conflicts_with = "name")]
41 pub from_file: Option<PathBuf>,
42
43 #[clap(long)]
45 pub redeploy: bool,
46
47 #[clap(name = "name")]
50 pub secret_name: Option<String>,
51
52 #[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 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}