Skip to main content

stack_deploy/
secrets.rs

1pub struct SecretId(pub String);
2
3pub trait SecretType:
4    Clone
5    + std::fmt::Display
6    + std::marker::Send
7    + std::marker::Sync
8    + std::str::FromStr<Err = strum::ParseError>
9    + strum::IntoEnumIterator
10{
11    fn to_arn_output_key(&self) -> crate::types::OutputKey;
12    fn to_env_variable_name(&self) -> &str;
13    fn validate(&self, input: &str) -> Result<(), String>;
14}
15
16pub async fn read_stack_secret_string_json<T: for<'a> serde::Deserialize<'a>, S: SecretType>(
17    secretsmanager: &aws_sdk_secretsmanager::client::Client,
18    stack: &aws_sdk_cloudformation::types::Stack,
19    secret: S,
20) -> T {
21    serde_json::from_str(&read_stack_secret_string(secretsmanager, stack, secret).await).unwrap()
22}
23
24pub async fn read_stack_secret_string<S: SecretType>(
25    secretsmanager: &aws_sdk_secretsmanager::client::Client,
26    stack: &aws_sdk_cloudformation::types::Stack,
27    secret: S,
28) -> String {
29    read_secret_value_string(
30        secretsmanager,
31        &SecretId(crate::stack::fetch_stack_output(
32            stack,
33            &secret.to_arn_output_key(),
34        )),
35    )
36    .await
37}
38
39pub async fn read_env_json<T: for<'a> serde::Deserialize<'a>, S: SecretType>(
40    secretsmanager: &aws_sdk_secretsmanager::client::Client,
41    secret: S,
42) -> T {
43    serde_json::from_str(&read_env_secret_string(secretsmanager, secret).await).unwrap()
44}
45
46pub async fn read_env_secret_string<S: SecretType>(
47    secretsmanager: &aws_sdk_secretsmanager::client::Client,
48    secret: S,
49) -> String {
50    let secret_id = read_env_secret_id(secret);
51
52    read_secret_value_string(secretsmanager, &secret_id).await
53}
54
55pub async fn read_secret_value_string(
56    secretsmanager: &aws_sdk_secretsmanager::client::Client,
57    secret_id: &SecretId,
58) -> String {
59    log::info!("Reading secret: {}", secret_id.0);
60
61    secretsmanager
62        .get_secret_value()
63        .secret_id(&secret_id.0)
64        .send()
65        .await
66        .unwrap()
67        .secret_string()
68        .unwrap()
69        .to_string()
70}
71
72fn read_env_secret_id<S: SecretType>(secret: S) -> SecretId {
73    let env_variable_name = secret.to_env_variable_name();
74
75    SecretId(
76        std::env::var(env_variable_name)
77            .unwrap_or_else(|_| panic!("Secret env variable: {env_variable_name} is not present!"))
78            .to_string(),
79    )
80}
81
82pub async fn put_secret_value_string(
83    secretsmanager: &aws_sdk_secretsmanager::client::Client,
84    secret_id: &SecretId,
85    secret_string: &str,
86) {
87    log::info!("Writing secret: {}", secret_id.0);
88
89    secretsmanager
90        .put_secret_value()
91        .secret_id(&secret_id.0)
92        .secret_string(secret_string)
93        .send()
94        .await
95        .unwrap();
96}
97
98pub mod cli {
99    use crate::secrets::{SecretId, SecretType};
100
101    #[derive(Clone, Debug, Eq, PartialEq, clap::Parser)]
102    pub struct App<T: SecretType + 'static> {
103        #[clap(subcommand)]
104        command: Command<T>,
105        #[clap(skip)]
106        _phantom: std::marker::PhantomData<T>,
107    }
108
109    impl<T: SecretType> App<T> {
110        pub async fn run(
111            &self,
112            cloudformation: &aws_sdk_cloudformation::client::Client,
113            secretsmanager: &aws_sdk_secretsmanager::client::Client,
114        ) {
115            self.command.run(cloudformation, secretsmanager).await
116        }
117    }
118
119    #[derive(Clone, Debug, Eq, PartialEq, clap::Parser)]
120    pub enum Command<T: SecretType + 'static> {
121        /// List registered secrets
122        List,
123        /// Write string to secret
124        PutSecretString {
125            /// Name of the secret to print
126            #[clap(long = "secret")]
127            secret: T,
128            /// Name of the stack providing the secret
129            #[clap(long = "stack-name")]
130            stack_name: crate::types::StackName,
131        },
132        /// Print secret string
133        PrintSecretString {
134            /// Name of the secret to print
135            #[clap(long = "secret")]
136            secret: T,
137            /// Name of the stack providing the secret
138            #[clap(long = "stack-name")]
139            stack_name: crate::types::StackName,
140        },
141    }
142
143    impl<T: SecretType> Command<T> {
144        pub async fn run(
145            &self,
146            cloudformation: &aws_sdk_cloudformation::client::Client,
147            secretsmanager: &aws_sdk_secretsmanager::client::Client,
148        ) {
149            match self {
150                Self::List => list::<T>(),
151                Self::PrintSecretString { secret, stack_name } => {
152                    print_secret_string::<T>(cloudformation, secretsmanager, stack_name, secret)
153                        .await
154                }
155                Self::PutSecretString { secret, stack_name } => {
156                    put_secret_string::<T>(cloudformation, secretsmanager, stack_name, secret).await
157                }
158            }
159        }
160    }
161
162    fn list<T: SecretType>() {
163        for secret in T::iter() {
164            println!("{secret}")
165        }
166    }
167
168    async fn put_secret_string<T: SecretType>(
169        cloudformation: &aws_sdk_cloudformation::client::Client,
170        secretsmanager: &aws_sdk_secretsmanager::client::Client,
171        stack_name: &crate::types::StackName,
172        secret: &T,
173    ) {
174        use std::io::IsTerminal;
175
176        let secret_id = SecretId(
177            crate::stack::read_stack_output(
178                cloudformation,
179                stack_name,
180                &secret.to_arn_output_key(),
181            )
182            .await,
183        );
184
185        let stdin = std::io::stdin();
186        let is_tty = stdin.is_terminal();
187
188        if is_tty {
189            eprintln!("Enter secret (newline terminates). Secret will be echoed:");
190        }
191
192        let mut input = String::new();
193
194        match stdin.read_line(&mut input) {
195            Ok(_) => {
196                if is_tty && input.ends_with('\n') {
197                    input.truncate(input.len() - 1);
198                }
199                match secret.validate(&input) {
200                    Ok(()) => {
201                        crate::secrets::put_secret_value_string(secretsmanager, &secret_id, &input)
202                            .await;
203                    }
204                    Err(error) => {
205                        eprintln!("Validation error: {error}");
206                        std::process::exit(1);
207                    }
208                }
209            }
210            Err(error) => panic!("Error: {error}"),
211        }
212    }
213
214    async fn print_secret_string<T: SecretType>(
215        cloudformation: &aws_sdk_cloudformation::client::Client,
216        secretsmanager: &aws_sdk_secretsmanager::client::Client,
217        stack_name: &crate::types::StackName,
218        secret: &T,
219    ) {
220        let secret_id = SecretId(
221            crate::stack::read_stack_output(
222                cloudformation,
223                stack_name,
224                &secret.to_arn_output_key(),
225            )
226            .await,
227        );
228
229        println!(
230            "{}",
231            crate::secrets::read_secret_value_string(secretsmanager, &secret_id).await
232        );
233    }
234}