wasmer_cli/commands/app/volumes/credentials/
rotate_secrets.rs

1use super::{ItemFormatOpts, S3CredentialRecord, render_s3_credentials};
2use crate::{
3    commands::{AsyncCliCommand, app::util::AppIdentOpts},
4    config::WasmerEnv,
5};
6
7/// Rotate the secrets linked to volumes of an app.
8#[derive(clap::Parser, Debug)]
9pub struct CmdAppVolumesRotateSecrets {
10    #[clap(flatten)]
11    pub env: WasmerEnv,
12
13    #[clap(flatten)]
14    pub ident: AppIdentOpts,
15
16    #[clap(flatten)]
17    pub fmt: ItemFormatOpts,
18
19    /// Only rotate this volume's credentials, by name, mount path, or volume id.
20    /// Without it, every S3-enabled volume of the app is rotated.
21    #[clap(long)]
22    pub volume: Option<String>,
23}
24
25#[async_trait::async_trait]
26impl AsyncCliCommand for CmdAppVolumesRotateSecrets {
27    type Output = ();
28
29    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
30        let client = self.env.client()?;
31        let (_ident, app) = self.ident.load_app(&client).await?;
32
33        // rotateS3Credentials operates per AppVolume, so resolve the app's
34        // volumes and rotate each one.
35        let volumes =
36            super::super::list_volumes(&client, &app.owner.global_name, &app.name).await?;
37
38        let selected: Vec<&_> = match &self.volume {
39            Some(sel) => vec![super::super::select_volume_by_selector(
40                &app.name, &volumes, sel,
41            )?],
42            // Rotate every S3-enabled volume; volumes without S3 have no S3
43            // credentials to rotate.
44            None => volumes.iter().filter(|v| v.s3_enabled).collect(),
45        };
46
47        if selected.is_empty() {
48            // Hint on stderr, but still render below so json/yaml emit `[]`.
49            eprintln!(
50                "App {} has no S3-enabled volumes to rotate secrets for.",
51                app.name
52            );
53        }
54
55        // Rotate every selected volume, recording failures instead of aborting
56        // so that a single failing volume does not leave the rest un-rotated
57        // and unreported.
58        let mut records = Vec::new();
59        let mut failures = Vec::new();
60        for volume in selected {
61            let result =
62                wasmer_backend_api::query::rotate_s3_credentials(&client, volume.id.clone()).await;
63
64            match result {
65                Ok(payload) if payload.success => {
66                    // Per-volume status on stderr, so the rotated credentials on
67                    // stdout stay pipeable.
68                    eprintln!("Rotated S3 credentials for volume {}", volume.label());
69                    records.push(S3CredentialRecord {
70                        app_name: app.name.clone(),
71                        volume: volume.mount_path.clone(),
72                        access_key: payload.access_key,
73                        secret_key: payload.secret_key,
74                        endpoint: payload.endpoint,
75                    });
76                }
77                Ok(_) => {
78                    eprintln!(
79                        "Failed to rotate S3 credentials for volume {}",
80                        volume.label()
81                    );
82                    failures.push(volume.label());
83                }
84                Err(err) => {
85                    eprintln!(
86                        "Failed to rotate S3 credentials for volume {}: {err:#}",
87                        volume.label()
88                    );
89                    failures.push(volume.label());
90                }
91            }
92        }
93
94        // Emit the rotated credentials as a single document; json/yaml stay one
95        // array/list (empty `[]` when nothing rotated).
96        println!("{}", render_s3_credentials(self.fmt.format, &records));
97
98        if !failures.is_empty() {
99            anyhow::bail!(
100                "Failed to rotate S3 credentials for {} volume(s): {}",
101                failures.len(),
102                failures.join(", ")
103            );
104        }
105
106        Ok(())
107    }
108}