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