wasmer_cli/commands/app/secrets/
import.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, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
17pub enum SecretImportFormat {
18 #[default]
19 #[clap(name = "env")]
20 Env,
21 #[clap(name = "json")]
22 Json,
23 #[clap(name = "yaml")]
24 Yaml,
25}
26
27#[derive(clap::Parser, Debug)]
29pub struct CmdAppSecretsImport {
30 #[clap(flatten)]
31 pub env: WasmerEnv,
32
33 #[clap(long)]
34 pub quiet: bool,
35
36 #[clap(long, default_value_t = !std::io::stdin().is_terminal())]
37 pub non_interactive: bool,
38
39 #[clap(long = "app-dir", conflicts_with = "app")]
40 pub app_dir_path: Option<PathBuf>,
41
42 #[clap(flatten)]
43 pub app_id: AppIdentFlag,
44
45 #[clap(long, name = "from-file", required = true)]
46 pub from_file: PathBuf,
47
48 #[clap(short = 'f', long, default_value = "env")]
49 pub format: SecretImportFormat,
50
51 #[clap(long)]
52 pub update_existing: bool,
53
54 #[clap(long)]
55 pub redeploy: bool,
56}
57
58impl CmdAppSecretsImport {
59 fn read_secrets(&self, path: &Path) -> anyhow::Result<Vec<Secret>> {
60 match self.format {
61 SecretImportFormat::Env => {
62 let mut ret = vec![];
63 for item in dotenvy::from_path_iter(path)? {
64 let (name, value) = item?;
65 ret.push(Secret { name, value })
66 }
67 Ok(ret)
68 }
69 SecretImportFormat::Json => {
70 let content = std::fs::read_to_string(path)?;
71 let secrets: Vec<Secret> = serde_json::from_str(&content)?;
72 Ok(secrets)
73 }
74 SecretImportFormat::Yaml => {
75 let content = std::fs::read_to_string(path)?;
76 let secrets: Vec<Secret> = serde_yaml::from_str(&content)?;
77 Ok(secrets)
78 }
79 }
80 }
81
82 async fn filter_secrets(
83 &self,
84 client: &WasmerClient,
85 app_id: &str,
86 secrets: Vec<Secret>,
87 ) -> anyhow::Result<Vec<Secret>> {
88 let names = secrets.iter().map(|s| &s.name);
89 let app_secrets =
90 wasmer_backend_api::query::get_all_app_secrets_filtered(client, app_id, names).await?;
91 let existing = HashSet::<String>::from_iter(app_secrets.iter().map(|s| s.name.clone()));
92 let mut ret = HashMap::new();
93
94 for secret in secrets {
95 if existing.contains(&secret.name) {
96 if self.update_existing {
97 ret.insert(secret.name, secret.value);
98 } else if self.non_interactive {
99 anyhow::bail!(
100 "Secret '{}' already exists. Use --update-existing to update it.",
101 secret.name.bold()
102 );
103 } else {
104 eprintln!(
105 "Secret '{}' already exists for the selected app.",
106 secret.name.bold()
107 );
108 let theme = ColorfulTheme::default();
109 let res = dialoguer::Confirm::with_theme(&theme)
110 .with_prompt("Do you want to update it?")
111 .interact()?;
112
113 if res {
114 ret.insert(secret.name, secret.value);
115 }
116 }
117 } else {
118 ret.insert(secret.name, secret.value);
119 }
120 }
121
122 Ok(ret
123 .into_iter()
124 .map(|(name, value)| Secret { name, value })
125 .collect())
126 }
127
128 async fn import(
129 &self,
130 client: &WasmerClient,
131 app_id: &str,
132 secrets: Vec<Secret>,
133 ) -> Result<(), anyhow::Error> {
134 if secrets.is_empty() {
135 if !self.quiet {
136 eprintln!("No secrets to import.");
137 }
138 return Ok(());
139 }
140
141 let res = wasmer_backend_api::query::upsert_app_secrets(
142 client,
143 app_id,
144 secrets.iter().map(|s| (s.name.as_str(), s.value.as_str())),
145 )
146 .await?;
147 let res = res.context(
148 "Backend did not return any payload to confirm the successful import of secrets!",
149 )?;
150
151 if !res.success {
152 anyhow::bail!("Secret import failed!")
153 } else {
154 if !self.quiet {
155 let action = if self.update_existing {
156 "imported/updated"
157 } else {
158 "imported"
159 };
160 eprintln!("Successfully {action} secret(s):");
161 for secret in &secrets {
162 eprintln!("{}", secret.name.bold());
163 }
164 }
165
166 let should_redeploy = self.redeploy || {
167 if !self.non_interactive {
168 let theme = ColorfulTheme::default();
169 dialoguer::Confirm::with_theme(&theme)
170 .with_prompt("Do you want to redeploy your app?")
171 .interact()?
172 } else {
173 false
174 }
175 };
176
177 if should_redeploy {
178 wasmer_backend_api::query::redeploy_app_by_id(client, app_id).await?;
179 if !self.quiet {
180 eprintln!("{} Deployment complete", "ð–¥”".yellow().bold());
181 }
182 } else if !self.quiet {
183 eprintln!(
184 "{}: In order for secrets to appear in your app, re-deploy it.",
185 "Info".bold()
186 );
187 }
188
189 Ok(())
190 }
191 }
192}
193
194#[async_trait::async_trait]
195impl AsyncCliCommand for CmdAppSecretsImport {
196 type Output = ();
197
198 async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
199 let client = self.env.client()?;
200 let app_id = super::utils::get_app_id(
201 &client,
202 self.app_id.app.as_ref(),
203 self.app_dir_path.as_ref(),
204 self.quiet,
205 self.non_interactive,
206 )
207 .await?;
208
209 let secrets = self.read_secrets(&self.from_file)?;
210 let secrets = self.filter_secrets(&client, &app_id, secrets).await?;
211 self.import(&client, &app_id, secrets).await
212 }
213}