1use super::BindingValue;
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
14#[serde(tag = "service", rename_all = "lowercase")]
18pub enum PostgresBinding {
19 Aurora(AuroraPostgresBinding),
21 #[serde(rename = "cloud-sql")]
23 CloudSql(CloudSqlPostgresBinding),
24 #[serde(rename = "flexible-server")]
26 FlexibleServer(FlexibleServerPostgresBinding),
27 External(ExternalPostgresBinding),
29 #[serde(rename = "local-postgres")]
31 Local(LocalPostgresBinding),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
37#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
38#[serde(rename_all = "camelCase")]
39pub struct AuroraPostgresBinding {
40 pub cluster_endpoint: BindingValue<String>,
41 pub port: BindingValue<u16>,
42 pub database: BindingValue<String>,
43 pub username: BindingValue<String>,
44 pub password_secret_arn: BindingValue<String>,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
51#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
52#[serde(rename_all = "camelCase")]
53pub struct CloudSqlPostgresBinding {
54 pub host: BindingValue<String>,
55 pub port: BindingValue<u16>,
56 pub database: BindingValue<String>,
57 pub username: BindingValue<String>,
58 pub server_ca_certificates: BindingValue<Vec<String>>,
61 pub password_secret_name: BindingValue<String>,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
68#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
69#[serde(rename_all = "camelCase")]
70pub struct FlexibleServerPostgresBinding {
71 pub host: BindingValue<String>,
72 pub port: BindingValue<u16>,
73 pub database: BindingValue<String>,
74 pub username: BindingValue<String>,
75 pub password_secret_uri: BindingValue<String>,
77}
78
79#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
84#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
86#[serde(rename_all = "kebab-case")]
87pub enum ExternalPostgresSslMode {
88 #[default]
90 VerifyFull,
91 Disable,
93}
94
95#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
98#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
99#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
100#[serde(rename_all = "camelCase")]
101pub struct ExternalPostgresBinding {
102 pub host: BindingValue<String>,
103 pub port: BindingValue<u16>,
104 pub database: BindingValue<String>,
105 pub username: BindingValue<String>,
106 pub password: String,
110 #[serde(default)]
112 pub ssl_mode: ExternalPostgresSslMode,
113}
114
115#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
118#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
119#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
120#[serde(rename_all = "camelCase")]
121pub struct LocalPostgresBinding {
122 pub host: BindingValue<String>,
123 pub port: BindingValue<u16>,
124 pub database: BindingValue<String>,
125 pub username: BindingValue<String>,
126 pub password: String,
127}
128
129impl std::fmt::Debug for ExternalPostgresBinding {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("ExternalPostgresBinding")
134 .field("host", &self.host)
135 .field("port", &self.port)
136 .field("database", &self.database)
137 .field("username", &self.username)
138 .field("password", &"<redacted>")
139 .field("ssl_mode", &self.ssl_mode)
140 .finish()
141 }
142}
143
144impl std::fmt::Debug for LocalPostgresBinding {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 f.debug_struct("LocalPostgresBinding")
147 .field("host", &self.host)
148 .field("port", &self.port)
149 .field("database", &self.database)
150 .field("username", &self.username)
151 .field("password", &"<redacted>")
152 .finish()
153 }
154}
155
156impl PostgresBinding {
157 pub fn local(
159 host: impl Into<BindingValue<String>>,
160 port: u16,
161 database: impl Into<BindingValue<String>>,
162 username: impl Into<BindingValue<String>>,
163 password: impl Into<String>,
164 ) -> Self {
165 Self::Local(LocalPostgresBinding {
166 host: host.into(),
167 port: BindingValue::value(port),
168 database: database.into(),
169 username: username.into(),
170 password: password.into(),
171 })
172 }
173
174 pub fn external(
176 host: impl Into<BindingValue<String>>,
177 port: u16,
178 database: impl Into<BindingValue<String>>,
179 username: impl Into<BindingValue<String>>,
180 password: impl Into<String>,
181 ) -> Self {
182 Self::External(ExternalPostgresBinding {
183 host: host.into(),
184 port: BindingValue::value(port),
185 database: database.into(),
186 username: username.into(),
187 password: password.into(),
188 ssl_mode: ExternalPostgresSslMode::default(),
189 })
190 }
191}
192
193#[cfg(test)]
194mod tests {
195 use super::*;
196
197 #[test]
198 fn local_binding_uses_local_postgres_tag() {
199 let binding = PostgresBinding::local("127.0.0.1", 5432, "db", "alien", "secret");
200 let json = serde_json::to_string(&binding).unwrap();
201 assert!(json.contains(r#""service":"local-postgres""#));
202 let deserialized: PostgresBinding = serde_json::from_str(&json).unwrap();
203 assert_eq!(binding, deserialized);
204 }
205
206 #[test]
207 fn external_binding_uses_external_tag() {
208 let binding = PostgresBinding::external("db.internal", 5432, "app", "alien", "secret");
209 let json = serde_json::to_string(&binding).expect("external binding serializes");
210 assert!(json.contains(r#""service":"external""#));
211 assert!(json.contains(r#""sslMode":"verify-full""#));
212 let deserialized: PostgresBinding =
213 serde_json::from_str(&json).expect("external binding deserializes");
214 assert_eq!(binding, deserialized);
215 }
216
217 #[test]
218 fn external_binding_without_ssl_mode_defaults_to_verified_tls() {
219 let json = r#"{
220 "service": "external",
221 "host": "db.internal",
222 "port": 5432,
223 "database": "app",
224 "username": "alien",
225 "password": "secret"
226 }"#;
227
228 let binding: PostgresBinding =
229 serde_json::from_str(json).expect("legacy external binding deserializes");
230
231 let PostgresBinding::External(binding) = binding else {
232 panic!("expected external Postgres binding");
233 };
234 assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::VerifyFull);
235 }
236
237 #[test]
238 fn external_binding_accepts_explicit_plaintext_opt_out() {
239 let json = r#"{
240 "service": "external",
241 "host": "db.internal",
242 "port": 5432,
243 "database": "app",
244 "username": "alien",
245 "password": "secret",
246 "sslMode": "disable"
247 }"#;
248
249 let binding: PostgresBinding =
250 serde_json::from_str(json).expect("plaintext external binding deserializes");
251
252 let PostgresBinding::External(binding) = binding else {
253 panic!("expected external Postgres binding");
254 };
255 assert_eq!(binding.ssl_mode, ExternalPostgresSslMode::Disable);
256 }
257
258 #[test]
259 fn cloud_variants_keep_hyphenated_tags() {
260 let aurora = PostgresBinding::Aurora(AuroraPostgresBinding {
261 cluster_endpoint: "cluster.rds.amazonaws.com".into(),
262 port: BindingValue::value(5432),
263 database: "db".into(),
264 username: "alien".into(),
265 password_secret_arn: "arn:aws:secretsmanager:...".into(),
266 });
267 assert!(serde_json::to_string(&aurora)
268 .unwrap()
269 .contains(r#""service":"aurora""#));
270
271 let cloud_sql = PostgresBinding::CloudSql(CloudSqlPostgresBinding {
272 host: "10.0.0.5".into(),
273 port: BindingValue::value(5432),
274 database: "db".into(),
275 username: "alien".into(),
276 server_ca_certificates: BindingValue::value(vec!["cloud-sql-root".to_string()]),
277 password_secret_name: "pg-credentials".into(),
278 });
279 assert!(serde_json::to_string(&cloud_sql)
280 .unwrap()
281 .contains(r#""service":"cloud-sql""#));
282
283 let flexible = PostgresBinding::FlexibleServer(FlexibleServerPostgresBinding {
284 host: "10.0.0.6".into(),
285 port: BindingValue::value(5432),
286 database: "db".into(),
287 username: "alien".into(),
288 password_secret_uri: "https://vault.vault.azure.net/secrets/pg".into(),
289 });
290 assert!(serde_json::to_string(&flexible)
291 .unwrap()
292 .contains(r#""service":"flexible-server""#));
293 }
294}