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