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

1pub(super) mod rotate_secrets;
2
3use crate::{
4    commands::{AsyncCliCommand, app::util::AppIdentOpts},
5    config::WasmerEnv,
6};
7
8/// Retrieve access credentials for the volumes of an app.
9///
10/// The credentials can be used to access volumes with any S3 client, for example rclone. Note:
11/// using --format=rclone - which is the default - will output an rclone configuration snippet.
12#[derive(clap::Parser, Debug)]
13pub struct CmdAppVolumesCredentials {
14    #[clap(flatten)]
15    pub env: WasmerEnv,
16
17    #[clap(flatten)]
18    pub fmt: ItemFormatOpts,
19
20    #[clap(flatten)]
21    pub ident: AppIdentOpts,
22}
23
24#[async_trait::async_trait]
25impl AsyncCliCommand for CmdAppVolumesCredentials {
26    type Output = ();
27
28    async fn run_async(self) -> Result<Self::Output, anyhow::Error> {
29        let client = self.env.client()?;
30        let (_ident, app) = self.ident.load_app(&client).await?;
31
32        // S3 credentials are per-volume; fetch every volume and print the
33        // credentials of those that have S3 enabled.
34        let volumes = super::list_volumes(&client, &app.owner.global_name, &app.name).await?;
35
36        let with_creds: Vec<_> = volumes
37            .iter()
38            .filter_map(|volume| volume.s3.as_ref().map(|s3| (volume, s3)))
39            .collect();
40
41        if with_creds.is_empty() {
42            // Hint on stderr, but still render below so json/yaml emit `[]`.
43            eprintln!(
44                "App {} has no S3-enabled volumes with credentials. \
45                 Enable S3 with `wasmer app volume enable-s3`.",
46                app.name
47            );
48        }
49
50        let records: Vec<S3CredentialRecord> = with_creds
51            .into_iter()
52            .map(|(volume, s3)| S3CredentialRecord {
53                app_name: app.name.clone(),
54                volume: volume.mount_path.clone(),
55                access_key: s3.access_key.clone(),
56                secret_key: s3.secret_key.clone(),
57                endpoint: s3.endpoint.clone(),
58            })
59            .collect();
60
61        println!("{}", render_s3_credentials(self.fmt.format, &records));
62
63        Ok(())
64    }
65}
66
67/// One volume's S3 credentials, ready to be rendered.
68#[derive(Debug, serde::Serialize)]
69pub(crate) struct S3CredentialRecord {
70    pub app_name: String,
71    /// The mount path the credentials belong to.
72    pub volume: String,
73    pub access_key: String,
74    pub secret_key: String,
75    pub endpoint: String,
76}
77
78/// Render a set of volumes' S3 credentials in the requested format.
79///
80/// For `json`/`yaml` the records are emitted as a single array/list so that the
81/// output is one valid document no matter how many volumes there are; for
82/// `rclone`/`table` each record is rendered in turn (concatenated sections, one
83/// table with a row per volume). The returned string is ready to print as-is.
84pub(crate) fn render_s3_credentials(
85    format: CredsItemFormat,
86    records: &[S3CredentialRecord],
87) -> String {
88    match format {
89        CredsItemFormat::Rclone => records.iter().map(render_rclone_section).collect(),
90        CredsItemFormat::Json => serde_json::to_string_pretty(records).unwrap(),
91        CredsItemFormat::Yaml => serde_yaml::to_string(records).unwrap(),
92        CredsItemFormat::Table => {
93            let mut table = comfy_table::Table::new();
94            table.add_row(vec![
95                "App name",
96                "Volume",
97                "Access key",
98                "Secret key",
99                "Endpoint",
100            ]);
101            for record in records {
102                table.add_row(vec![
103                    record.app_name.as_str(),
104                    record.volume.as_str(),
105                    record.access_key.as_str(),
106                    record.secret_key.as_str(),
107                    record.endpoint.as_str(),
108                ]);
109            }
110            table.to_string()
111        }
112    }
113}
114
115/// Render one volume's credentials as an rclone configuration section.
116fn render_rclone_section(record: &S3CredentialRecord) -> String {
117    let S3CredentialRecord {
118        app_name,
119        volume,
120        access_key,
121        secret_key,
122        endpoint,
123    } = record;
124    let section = format!(
125        "edge-{app_name}-{}",
126        volume.trim_start_matches('/').replace('/', "-")
127    );
128    format!(
129        r#"
130[{section}]
131# rclone configuration for volume {volume} of {app_name}
132type = s3
133provider = Other
134acl = private
135access_key_id = {access_key}
136secret_access_key = {secret_key}
137endpoint = {endpoint}
138
139"#
140    )
141}
142
143/*
144 * The following is a copy of [`crate::opts::ItemFormatOpts`] with an
145 * additional formatting - rclone - that only makes sense in this context.
146 */
147
148/// The possible formats to output the credentials in.
149#[derive(Clone, Copy, Debug, PartialEq, Eq, clap::ValueEnum)]
150pub enum CredsItemFormat {
151    Json,
152    Yaml,
153    Table,
154    Rclone,
155}
156
157/// Formatting options for credentials.
158#[derive(clap::Parser, Debug)]
159pub struct ItemFormatOpts {
160    /// Output format
161    #[clap(short = 'f', long, default_value = "rclone")]
162    pub format: CredsItemFormat,
163}