faucet_common_redshift/
pool.rs1use faucet_core::FaucetError;
8use sqlx::PgPool;
9use sqlx::postgres::{PgConnectOptions, PgPoolOptions, PgSslMode};
10
11use crate::config::{RedshiftConnection, RedshiftCredentials};
12
13pub fn resolve_password(creds: &RedshiftCredentials) -> Result<&str, FaucetError> {
18 match creds {
19 RedshiftCredentials::Password { password } => Ok(password.as_str()),
20 RedshiftCredentials::Iam { .. } => Err(FaucetError::Config(
21 "redshift: IAM authentication is not yet supported (v1 supports password auth only) \
22 — use credentials: { type: password, config: { password: … } }"
23 .into(),
24 )),
25 RedshiftCredentials::RedshiftDataApi { .. } => Err(FaucetError::Config(
26 "redshift: Redshift Data API authentication is not yet supported (v1 supports \
27 password auth only) — use credentials: { type: password, config: { password: … } }"
28 .into(),
29 )),
30 }
31}
32
33pub fn build_connect_options(conn: &RedshiftConnection) -> Result<PgConnectOptions, FaucetError> {
39 if conn.host.trim().is_empty() {
40 return Err(FaucetError::Config(
41 "redshift: `host` must not be empty".into(),
42 ));
43 }
44 if conn.database.trim().is_empty() {
45 return Err(FaucetError::Config(
46 "redshift: `database` must not be empty".into(),
47 ));
48 }
49 if conn.user.trim().is_empty() {
50 return Err(FaucetError::Config(
51 "redshift: `user` must not be empty".into(),
52 ));
53 }
54 let password = resolve_password(&conn.credentials)?;
55 let ssl_mode = if conn.tls {
56 PgSslMode::Require
57 } else {
58 PgSslMode::Prefer
59 };
60 Ok(PgConnectOptions::new()
61 .host(&conn.host)
62 .port(conn.port)
63 .database(&conn.database)
64 .username(&conn.user)
65 .password(password)
66 .ssl_mode(ssl_mode)
67 .application_name("faucet"))
68}
69
70pub fn build_pool_lazy(
75 conn: &RedshiftConnection,
76 max_connections: u32,
77) -> Result<PgPool, FaucetError> {
78 let opts = build_connect_options(conn)?;
79 Ok(PgPoolOptions::new()
80 .max_connections(max_connections.max(1))
81 .connect_lazy_with(opts))
82}
83
84pub async fn build_pool(
87 conn: &RedshiftConnection,
88 max_connections: u32,
89) -> Result<PgPool, FaucetError> {
90 let opts = build_connect_options(conn)?;
91 PgPoolOptions::new()
92 .max_connections(max_connections.max(1))
93 .connect_with(opts)
94 .await
95 .map_err(|e| FaucetError::Config(format!("redshift connection failed: {e}")))
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101
102 fn conn() -> RedshiftConnection {
103 RedshiftConnection::new("host.example.com", "dev", "admin", "pw")
104 }
105
106 #[test]
107 fn build_options_succeeds_for_password() {
108 assert!(build_connect_options(&conn()).is_ok());
109 }
110
111 #[test]
112 fn build_options_rejects_empty_host() {
113 let mut c = conn();
114 c.host = " ".into();
115 assert!(matches!(
116 build_connect_options(&c),
117 Err(FaucetError::Config(_))
118 ));
119 }
120
121 #[test]
122 fn build_options_rejects_empty_database() {
123 let mut c = conn();
124 c.database = String::new();
125 assert!(matches!(
126 build_connect_options(&c),
127 Err(FaucetError::Config(_))
128 ));
129 }
130
131 #[test]
132 fn build_options_rejects_empty_user() {
133 let mut c = conn();
134 c.user = String::new();
135 assert!(matches!(
136 build_connect_options(&c),
137 Err(FaucetError::Config(_))
138 ));
139 }
140
141 #[test]
142 fn resolve_password_returns_password() {
143 let creds = RedshiftCredentials::Password {
144 password: "hunter2".into(),
145 };
146 assert_eq!(resolve_password(&creds).unwrap(), "hunter2");
147 }
148
149 #[test]
150 fn resolve_password_rejects_iam_with_typed_error() {
151 let creds = RedshiftCredentials::Iam {
152 region: None,
153 cluster_identifier: None,
154 db_user: None,
155 };
156 match resolve_password(&creds) {
157 Err(FaucetError::Config(m)) => assert!(m.contains("IAM"), "got: {m}"),
158 other => panic!("expected Config error, got {other:?}"),
159 }
160 }
161
162 #[test]
163 fn resolve_password_rejects_data_api_with_typed_error() {
164 let creds = RedshiftCredentials::RedshiftDataApi {
165 region: None,
166 cluster_identifier: None,
167 workgroup_name: None,
168 secret_arn: None,
169 db_user: None,
170 };
171 match resolve_password(&creds) {
172 Err(FaucetError::Config(m)) => assert!(m.contains("Data API"), "got: {m}"),
173 other => panic!("expected Config error, got {other:?}"),
174 }
175 }
176
177 #[test]
178 fn build_connect_options_surfaces_unsupported_credentials() {
179 let mut c = conn();
180 c.credentials = RedshiftCredentials::Iam {
181 region: None,
182 cluster_identifier: None,
183 db_user: None,
184 };
185 assert!(build_connect_options(&c).is_err());
186 }
187
188 #[tokio::test]
189 async fn build_pool_lazy_does_no_io() {
190 let pool = build_pool_lazy(&conn(), 4).unwrap();
194 assert_eq!(pool.size(), 0);
195 }
196}