wasmer_cli/commands/app/secrets/
import.rs

1use 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/// Import app secrets from a file.
28#[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}