1use serde::{Deserialize, Serialize};
16use serde_json::{Value, json};
17
18use super::{AppSpec, EnvValue};
19use crate::error::{Error, Result};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum Engine {
25 Postgres,
26 Mysql,
27 Mariadb,
28 #[serde(alias = "mongo")]
29 Mongodb,
30 Redis,
31}
32
33pub const ENGINES: [Engine; 5] = [
34 Engine::Postgres,
35 Engine::Mysql,
36 Engine::Mariadb,
37 Engine::Mongodb,
38 Engine::Redis,
39];
40
41pub const DATA_VOLUME: &str = "data";
43
44impl Engine {
45 pub fn name(self) -> &'static str {
46 match self {
47 Engine::Postgres => "postgres",
48 Engine::Mysql => "mysql",
49 Engine::Mariadb => "mariadb",
50 Engine::Mongodb => "mongodb",
51 Engine::Redis => "redis",
52 }
53 }
54
55 pub fn parse(s: &str) -> Result<Engine> {
56 match s.to_ascii_lowercase().as_str() {
57 "postgres" | "postgresql" | "pg" => Ok(Engine::Postgres),
58 "mysql" => Ok(Engine::Mysql),
59 "mariadb" => Ok(Engine::Mariadb),
60 "mongodb" | "mongo" => Ok(Engine::Mongodb),
61 "redis" => Ok(Engine::Redis),
62 _ => Err(Error::invalid(format!(
63 "engine {s:?}: postgres, mysql, mariadb, mongodb or redis"
64 ))),
65 }
66 }
67
68 pub fn default_version(self) -> &'static str {
70 match self {
71 Engine::Postgres => "17",
72 Engine::Mysql => "8.4",
73 Engine::Mariadb => "11.4",
74 Engine::Mongodb => "8.0",
75 Engine::Redis => "7.4",
76 }
77 }
78
79 fn repository(self) -> &'static str {
81 match self {
82 Engine::Mongodb => "mongo",
83 e => e.name(),
84 }
85 }
86
87 pub fn port(self) -> u16 {
88 match self {
89 Engine::Postgres => 5432,
90 Engine::Mysql | Engine::Mariadb => 3306,
91 Engine::Mongodb => 27017,
92 Engine::Redis => 6379,
93 }
94 }
95
96 pub fn data_path(self) -> &'static str {
97 match self {
98 Engine::Postgres => "/var/lib/postgresql/data",
99 Engine::Mysql | Engine::Mariadb => "/var/lib/mysql",
100 Engine::Mongodb => "/data/db",
101 Engine::Redis => "/data",
102 }
103 }
104
105 pub fn has_root_password(self) -> bool {
107 matches!(self, Engine::Mysql | Engine::Mariadb)
108 }
109
110 pub fn has_database(self) -> bool {
112 !matches!(self, Engine::Redis)
113 }
114
115 fn scheme(self) -> &'static str {
117 match self {
118 Engine::Postgres => "postgres",
119 Engine::Mysql | Engine::Mariadb => "mysql",
120 Engine::Mongodb => "mongodb",
121 Engine::Redis => "redis",
122 }
123 }
124
125 pub fn dump_extension(self) -> &'static str {
127 match self {
128 Engine::Postgres => "pgdump",
129 Engine::Mysql | Engine::Mariadb => "sql",
130 Engine::Mongodb => "archive",
131 Engine::Redis => "rdb",
132 }
133 }
134
135 pub fn restores_from(self, from: Engine) -> bool {
137 self == from
138 || matches!(
139 (self, from),
140 (Engine::Mysql, Engine::Mariadb) | (Engine::Mariadb, Engine::Mysql)
141 )
142 }
143
144 pub fn dump_command(self) -> Vec<String> {
148 let line = match self {
149 Engine::Postgres => {
150 r#"PGPASSWORD="$POSTGRES_PASSWORD" exec pg_dump -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" --format=custom -Z0"#
151 }
152 Engine::Mysql => {
153 r#"MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysqldump -h 127.0.0.1 -uroot --single-transaction --routines --triggers --events --no-tablespaces --set-gtid-purged=OFF "$MYSQL_DATABASE""#
154 }
155 Engine::Mariadb => {
156 r#"MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb-dump -h 127.0.0.1 -uroot --single-transaction --routines --triggers --events "$MARIADB_DATABASE""#
157 }
158 Engine::Mongodb => {
159 r#"exec mongodump --quiet --archive --host 127.0.0.1 --username "$MONGO_INITDB_ROOT_USERNAME" --password "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --db "$MONGO_INITDB_DATABASE""#
160 }
161 Engine::Redis => {
163 r#"redis-cli -h 127.0.0.1 --no-auth-warning SAVE >/dev/null && exec cat /data/dump.rdb"#
164 }
165 };
166 vec!["/bin/sh".into(), "-c".into(), line.into()]
167 }
168
169 pub fn restore_command(self) -> Vec<String> {
173 let line = match self {
174 Engine::Postgres => {
175 r#"PGPASSWORD="$POSTGRES_PASSWORD" exec pg_restore -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB" --clean --if-exists --no-owner --no-privileges"#
176 }
177 Engine::Mysql => {
178 r#"MYSQL_PWD="$MYSQL_ROOT_PASSWORD" exec mysql -h 127.0.0.1 -uroot "$MYSQL_DATABASE""#
179 }
180 Engine::Mariadb => {
181 r#"MYSQL_PWD="$MARIADB_ROOT_PASSWORD" exec mariadb -h 127.0.0.1 -uroot "$MARIADB_DATABASE""#
182 }
183 Engine::Mongodb => {
184 r#"exec mongorestore --quiet --archive --drop --host 127.0.0.1 --username "$MONGO_INITDB_ROOT_USERNAME" --password "$MONGO_INITDB_ROOT_PASSWORD" --authenticationDatabase admin --nsFrom "$ISB_SOURCE_DB.*" --nsTo "$MONGO_INITDB_DATABASE.*""#
185 }
186 Engine::Redis => {
189 r#"cat > /data/dump.rdb.isb-restore && mv /data/dump.rdb.isb-restore /data/dump.rdb && chown redis:redis /data/dump.rdb 2>/dev/null; redis-cli -h 127.0.0.1 --no-auth-warning SHUTDOWN NOSAVE >/dev/null 2>&1; exit 0"#
190 }
191 };
192 vec!["/bin/sh".into(), "-c".into(), line.into()]
193 }
194}
195
196impl std::fmt::Display for Engine {
197 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
198 f.write_str(self.name())
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
204#[serde(deny_unknown_fields)]
205pub struct DatabaseSource {
206 pub engine: Engine,
207 #[serde(default, skip_serializing_if = "String::is_empty")]
211 pub version: String,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub database: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
218 pub user: Option<String>,
219}
220
221fn validate_ident(what: &str, s: &str) -> Result<()> {
223 let ok = !s.is_empty()
224 && s.len() <= 63
225 && s.starts_with(|c: char| c.is_ascii_alphabetic() || c == '_')
226 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_');
227 if ok {
228 Ok(())
229 } else {
230 Err(Error::invalid(format!(
231 "{what} {s:?}: up to 63 characters of [A-Za-z0-9_], not starting with a digit"
232 )))
233 }
234}
235
236fn validate_version(v: &str) -> Result<()> {
237 let ok = !v.is_empty()
238 && v.len() <= 64
239 && v.starts_with(|c: char| c.is_ascii_alphanumeric())
240 && v.chars()
241 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'));
242 if ok {
243 Ok(())
244 } else {
245 Err(Error::invalid(format!(
246 "version {v:?}: an image tag such as 17 or 8.4"
247 )))
248 }
249}
250
251impl DatabaseSource {
252 pub fn version(&self) -> &str {
253 if self.version.is_empty() {
254 self.engine.default_version()
255 } else {
256 &self.version
257 }
258 }
259
260 pub fn image(&self) -> String {
262 format!("docker:{}:{}", self.engine.repository(), self.version())
263 }
264
265 pub fn normalize(&mut self, app: &str) {
267 if self.version.is_empty() {
268 self.version = self.engine.default_version().into();
269 }
270 if self.engine.has_database() {
271 if self.database.is_none() {
272 self.database = Some(app.replace('-', "_"));
273 }
274 if self.user.is_none() {
275 self.user = Some(default_user(app));
276 }
277 }
278 }
279
280 pub fn database_name(&self, app: &str) -> String {
281 self.database
282 .clone()
283 .unwrap_or_else(|| app.replace('-', "_"))
284 }
285
286 pub fn user_name(&self, app: &str) -> String {
287 self.user.clone().unwrap_or_else(|| default_user(app))
288 }
289
290 pub fn validate(&self, app: &str) -> Result<()> {
291 validate_version(self.version())?;
292 if self.engine.has_database() {
293 validate_ident("database", &self.database_name(app))?;
294 validate_ident("user", &self.user_name(app))?;
295 if self.engine == Engine::Postgres && self.user_name(app).starts_with("pg_") {
296 return Err(Error::invalid(format!(
297 "user {:?}: Postgres reserves role names starting with pg_",
298 self.user_name(app)
299 )));
300 }
301 if self.engine.has_root_password() && self.user_name(app) == "root" {
302 return Err(Error::invalid(
303 "user: root is the engine's own administrator; pick another name",
304 ));
305 }
306 } else if self.database.is_some() || self.user.is_some() {
307 return Err(Error::invalid(format!(
308 "{} has no database or user to set",
309 self.engine
310 )));
311 }
312 Ok(())
313 }
314}
315
316pub fn password_secret(app: &str) -> String {
318 format!("db.{app}.password")
319}
320
321pub fn root_password_secret(app: &str) -> String {
323 format!("db.{app}.root-password")
324}
325
326pub fn url_secret(app: &str) -> String {
329 format!("db.{app}.url")
330}
331
332fn default_user(app: &str) -> String {
336 let base = app.replace('-', "_");
337 if base.starts_with("pg_") {
338 format!("app_{base}")
339 } else {
340 base
341 }
342}
343
344pub fn secrets(app: &str, engine: Engine) -> Vec<String> {
346 let mut v = vec![password_secret(app), url_secret(app)];
347 if engine.has_root_password() {
348 v.push(root_password_secret(app));
349 }
350 v
351}
352
353fn engine_env(app: &str, db: &DatabaseSource) -> Vec<(String, EnvValue)> {
355 let s = |n: String| EnvValue::Secret { secret: n };
356 let p = |v: &str| EnvValue::Plain(v.to_string());
357 let (name, user) = (db.database_name(app), db.user_name(app));
358 match db.engine {
359 Engine::Postgres => vec![
360 ("POSTGRES_DB".into(), p(&name)),
361 ("POSTGRES_USER".into(), p(&user)),
362 ("POSTGRES_PASSWORD".into(), s(password_secret(app))),
363 ("PGDATA".into(), p("/var/lib/postgresql/data/pgdata")),
365 ],
366 Engine::Mysql => vec![
367 ("MYSQL_DATABASE".into(), p(&name)),
368 ("MYSQL_USER".into(), p(&user)),
369 ("MYSQL_PASSWORD".into(), s(password_secret(app))),
370 ("MYSQL_ROOT_PASSWORD".into(), s(root_password_secret(app))),
371 ],
372 Engine::Mariadb => vec![
373 ("MARIADB_DATABASE".into(), p(&name)),
374 ("MARIADB_USER".into(), p(&user)),
375 ("MARIADB_PASSWORD".into(), s(password_secret(app))),
376 ("MARIADB_ROOT_PASSWORD".into(), s(root_password_secret(app))),
377 ],
378 Engine::Mongodb => vec![
379 ("MONGO_INITDB_DATABASE".into(), p(&name)),
380 ("MONGO_INITDB_ROOT_USERNAME".into(), p(&user)),
381 ("MONGO_INITDB_ROOT_PASSWORD".into(), s(password_secret(app))),
382 ],
383 Engine::Redis => vec![
384 ("REDIS_PASSWORD".into(), s(password_secret(app))),
385 ("REDISCLI_AUTH".into(), s(password_secret(app))),
387 ],
388 }
389}
390
391fn healthcheck(engine: Engine) -> Value {
392 let test = match engine {
393 Engine::Postgres => r#"pg_isready -q -h 127.0.0.1 -U "$POSTGRES_USER" -d "$POSTGRES_DB""#,
394 Engine::Mysql => {
395 r#"MYSQL_PWD="$MYSQL_ROOT_PASSWORD" mysqladmin ping -h 127.0.0.1 -uroot --silent"#
396 }
397 Engine::Mariadb => {
398 r#"MYSQL_PWD="$MARIADB_ROOT_PASSWORD" mariadb-admin ping -h 127.0.0.1 -uroot --silent"#
399 }
400 Engine::Mongodb => {
401 r#"mongosh --quiet --host 127.0.0.1 --eval "db.adminCommand('ping').ok""#
402 }
403 Engine::Redis => r#"redis-cli -h 127.0.0.1 --no-auth-warning ping | grep -q PONG"#,
404 };
405 json!({
406 "test": ["CMD-SHELL", test],
407 "interval": "5s",
408 "timeout": "5s",
409 "retries": 6,
410 "start_period": "120s",
412 })
413}
414
415pub fn effective(spec: &AppSpec, db: &DatabaseSource) -> AppSpec {
419 let mut s = spec.clone();
420 let mut env = super::EnvFile::default();
421 for (k, v) in engine_env(&spec.name, db) {
422 env.set(&k, v);
423 }
424 for (k, v) in spec.env.vars() {
425 env.set(k, v.clone());
426 }
427 s.env = env;
428 s.volumes
429 .insert(0, format!("{DATA_VOLUME}:{}", db.engine.data_path()));
430 if s.healthcheck.is_none() {
431 s.healthcheck = Some(healthcheck(db.engine));
432 }
433 if s.port.is_none() {
434 s.port = Some(db.engine.port());
435 }
436 if db.engine == Engine::Redis && s.command.is_none() {
437 s.command = Some(json!([
438 "/bin/sh",
439 "-c",
440 r#"exec docker-entrypoint.sh redis-server --requirepass "$REDIS_PASSWORD" --save "60 1" --appendonly no"#
441 ]));
442 }
443 s.replicas = s.replicas.min(1);
444 s
445}
446
447pub fn validate(spec: &AppSpec, db: &DatabaseSource) -> Result<()> {
449 db.validate(&spec.name)?;
450 if spec.build.is_some() {
451 return Err(Error::invalid("a database is not built; drop `build`"));
452 }
453 if spec.replicas > 1 {
454 return Err(Error::invalid(
455 "a database runs one replica (two writers on one data volume corrupt it)",
456 ));
457 }
458 for v in &spec.volumes {
459 if v.split(':').next() == Some(DATA_VOLUME) {
460 return Err(Error::invalid(format!(
461 "volume name {DATA_VOLUME:?} is the database's own"
462 )));
463 }
464 }
465 Ok(())
466}
467
468pub fn connection(
471 spec: &AppSpec,
472 db: &DatabaseSource,
473 org: &crate::org::OrgId,
474 password: Option<&str>,
475) -> Value {
476 let stack = spec.stack().unwrap_or_default();
477 let host = format!("{}.{stack}", spec.name);
478 let fqdn = format!("{host}.{org}.isb");
479 let port = db.engine.port();
480 let (user, name) = if db.engine.has_database() {
481 (
482 Some(db.user_name(&spec.name)),
483 Some(db.database_name(&spec.name)),
484 )
485 } else {
486 (None, None)
487 };
488 let url = |pw: &str, host: &str, port: u16| -> String {
489 let auth = match &user {
490 Some(u) => format!("{u}:{pw}@"),
491 None => format!("default:{pw}@"),
492 };
493 let mut u = format!("{}://{auth}{host}:{port}", db.engine.scheme());
494 if let Some(n) = &name {
495 u.push_str(&format!("/{n}"));
496 }
497 if db.engine == Engine::Mongodb {
498 u.push_str("?authSource=admin");
499 }
500 u
501 };
502 let reference = format!("${{{{secret.{}}}}}", password_secret(&spec.name));
503 let external: Vec<String> = spec
505 .ports
506 .iter()
507 .filter_map(|p| {
508 let parts: Vec<&str> = p.split(':').collect();
509 match parts.as_slice() {
510 [ip, host_port, _] => Some(format!("{ip}:{host_port}")),
511 [host_port, _] => Some(format!("127.0.0.1:{host_port}")),
512 _ => None,
513 }
514 })
515 .collect();
516 let mut v = json!({
517 "engine": db.engine,
518 "version": db.version(),
519 "image": db.image(),
520 "host": host,
521 "fqdn": fqdn,
522 "port": port,
523 "password": {"secret": password_secret(&spec.name)},
524 "url": url(&reference, &host, port),
525 "url_secret": url_secret(&spec.name),
526 "volume": format!("{stack}_{}_{DATA_VOLUME}", spec.name),
527 });
528 if let Some(u) = &user {
529 v["user"] = json!(u);
530 }
531 if let Some(n) = &name {
532 v["database"] = json!(n);
533 }
534 if db.engine.has_root_password() {
535 v["root_password"] = json!({"secret": root_password_secret(&spec.name)});
536 }
537 if !external.is_empty() {
538 v["external"] = json!(
539 external
540 .iter()
541 .map(|hp| {
542 let (h, p) = hp.rsplit_once(':').unwrap_or((hp, ""));
543 url(&reference, h, p.parse().unwrap_or(port))
544 })
545 .collect::<Vec<_>>()
546 );
547 }
548 if let Some(pw) = password {
549 v["password_value"] = json!(pw);
550 v["url_value"] = json!(url(pw, &host, port));
551 }
552 v
553}
554
555pub fn internal_url(spec: &AppSpec, db: &DatabaseSource, password: &str) -> String {
557 connection(spec, db, &crate::org::OrgId::default_org(), Some(password))["url_value"]
558 .as_str()
559 .unwrap_or_default()
560 .to_string()
561}
562
563#[cfg(test)]
564mod tests {
565
566 #[test]
567 fn postgres_users_never_start_with_pg() {
568 let mut d: DatabaseSource =
569 serde_json::from_value(serde_json::json!({"engine": "postgres"})).unwrap();
570 assert_eq!(d.user_name("pg-main"), "app_pg_main");
571 assert_eq!(
572 d.database_name("pg-main"),
573 "pg_main",
574 "databases may start with pg_"
575 );
576 assert_eq!(d.user_name("pgcopy"), "pgcopy");
577 assert!(d.validate("pg-main").is_ok());
578 d.user = Some("pg_admin".into());
579 assert!(d.validate("x").is_err());
580 }
581 use super::*;
582 use crate::app::{Rendered, Source, render};
583
584 fn db_spec(engine: &str) -> AppSpec {
585 serde_json::from_value(json!({
586 "name": "main-db", "project": "shop",
587 "source": {"database": {"engine": engine}},
588 }))
589 .unwrap()
590 }
591
592 fn render_db(spec: &AppSpec) -> Rendered {
593 let Source::Database(db) = &spec.source else {
594 panic!()
595 };
596 let mut notes = vec![];
597 render(spec, &db.image(), &mut notes).unwrap()
598 }
599
600 #[test]
601 fn defaults_and_validation() {
602 let mut s = db_spec("postgres");
603 let Source::Database(db) = &mut s.source else {
604 panic!()
605 };
606 db.normalize("main-db");
607 assert_eq!(db.version, "17");
608 assert_eq!(db.database.as_deref(), Some("main_db"));
609 assert_eq!(db.user.as_deref(), Some("main_db"));
610 assert_eq!(db.image(), "docker:postgres:17");
611 s.validate().unwrap();
612 let mut bad = s.clone();
613 bad.replicas = 2;
614 assert!(bad.validate().is_err(), "one replica");
615 let mut bad = s.clone();
616 bad.volumes = vec!["data:/x".into()];
617 assert!(bad.validate().is_err(), "data is the database's");
618 let mut bad = s.clone();
619 if let Source::Database(d) = &mut bad.source {
620 d.database = Some("x; drop".into());
621 }
622 assert!(bad.validate().is_err());
623 let mut bad = s.clone();
624 if let Source::Database(d) = &mut bad.source {
625 d.version = "17 && rm".into();
626 }
627 assert!(bad.validate().is_err());
628 let r = db_spec("redis");
629 r.validate().unwrap();
630 let mut bad = r.clone();
631 if let Source::Database(d) = &mut bad.source {
632 d.user = Some("u".into());
633 }
634 assert!(bad.validate().is_err(), "redis has no user");
635 let m: AppSpec = serde_json::from_value(json!({
636 "name": "m", "project": "p", "source": {"database": {"engine": "mysql", "user": "root"}},
637 }))
638 .unwrap();
639 assert!(m.validate().is_err(), "root is mysql's own");
640 assert_eq!(Engine::parse("mongo").unwrap(), Engine::Mongodb);
641 assert!(Engine::parse("oracle").is_err());
642 let e: Engine = serde_json::from_value(json!("mongo")).unwrap();
643 assert_eq!(e, Engine::Mongodb);
644 }
645
646 #[test]
647 fn credentials_render_as_secret_references() {
648 for engine in ENGINES {
649 let s = db_spec(engine.name());
650 let r = render_db(&s);
651 let svc = &r.service;
652 assert_eq!(
653 svc.image,
654 format!(
655 "docker:{}:{}",
656 engine.repository(),
657 engine.default_version()
658 )
659 );
660 let pw = password_secret("main-db");
662 assert!(
663 svc.env
664 .secrets
665 .values()
666 .any(|k| k == &format!("main-db.{pw}")),
667 "{engine}: {:?}",
668 svc.env.secrets
669 );
670 assert_eq!(
671 r.secrets[&format!("main-db.{pw}")].name.as_deref(),
672 Some(pw.as_str())
673 );
674 assert!(r.secrets.values().all(|d| d.external));
675 for v in svc.env.vars.values() {
676 assert!(!v.contains("password"), "{engine}: {v}");
677 }
678 assert_eq!(svc.volumes[0].source, "main-db_data");
680 assert_eq!(svc.volumes[0].target, engine.data_path());
681 assert!(r.volumes.contains_key("main-db_data"));
682 let v = serde_json::to_value(svc).unwrap();
683 assert_eq!(
684 v["deploy"]["update_config"]["order"], "stop-first",
685 "{engine}"
686 );
687 assert_eq!(svc.replicas(), 1);
688 assert!(svc.healthcheck.is_some());
689 assert!(svc.ports.is_empty(), "not published by default");
690 if engine.has_root_password() {
691 assert_eq!(svc.env.secrets.len(), 2, "{engine}");
692 }
693 }
694 let pg = render_db(&db_spec("postgres"));
695 assert_eq!(pg.service.env["POSTGRES_DB"], "main_db");
696 assert_eq!(pg.service.env["POSTGRES_USER"], "main_db");
697 assert_eq!(
698 pg.service.env.secrets["POSTGRES_PASSWORD"],
699 "main-db.db.main-db.password"
700 );
701 let redis = render_db(&db_spec("redis"));
702 assert!(redis.service.command.is_some());
703 assert_eq!(redis.service.env.secrets.len(), 2);
704 }
705
706 #[test]
707 fn app_env_adds_to_the_engine_env() {
708 let mut s = db_spec("postgres");
709 s.env.set(
710 "POSTGRES_INITDB_ARGS",
711 EnvValue::Plain("--data-checksums".into()),
712 );
713 s.ports = vec!["127.0.0.1:15432:5432".into()];
714 let r = render_db(&s);
715 assert_eq!(r.service.env["POSTGRES_INITDB_ARGS"], "--data-checksums");
716 assert_eq!(r.service.ports.len(), 1);
717 }
718
719 #[test]
720 fn connection_names_the_secret() {
721 let mut s = db_spec("postgres");
722 s.ports = vec!["127.0.0.1:15432:5432".into()];
723 let Source::Database(db) = &s.source else {
724 panic!()
725 };
726 let org = crate::org::OrgId::new("acme").unwrap();
727 let c = connection(&s, db, &org, None);
728 assert_eq!(c["host"], "main-db.shop-production");
729 assert_eq!(c["fqdn"], "main-db.shop-production.acme.isb");
730 assert_eq!(c["port"], 5432);
731 assert_eq!(c["password"]["secret"], "db.main-db.password");
732 assert_eq!(
733 c["url"],
734 "postgres://main_db:${{secret.db.main-db.password}}@main-db.shop-production:5432/main_db"
735 );
736 assert_eq!(
737 c["external"][0],
738 "postgres://main_db:${{secret.db.main-db.password}}@127.0.0.1:15432/main_db"
739 );
740 assert!(c.get("password_value").is_none());
741 let shown = connection(&s, db, &org, Some("pw1"));
742 assert_eq!(shown["password_value"], "pw1");
743 assert_eq!(
744 internal_url(&s, db, "pw1"),
745 "postgres://main_db:pw1@main-db.shop-production:5432/main_db"
746 );
747 let r = db_spec("redis");
748 let Source::Database(rdb) = &r.source else {
749 panic!()
750 };
751 assert_eq!(
752 internal_url(&r, rdb, "x"),
753 "redis://default:x@main-db.shop-production:6379"
754 );
755 let m = db_spec("mongodb");
756 let Source::Database(mdb) = &m.source else {
757 panic!()
758 };
759 assert!(internal_url(&m, mdb, "x").ends_with("/main_db?authSource=admin"));
760 }
761
762 #[test]
763 fn dump_and_restore_take_credentials_from_the_instance() {
764 for e in ENGINES {
765 for argv in [e.dump_command(), e.restore_command()] {
766 assert_eq!(argv.len(), 3);
767 assert_eq!(argv[0], "/bin/sh");
768 if e != Engine::Redis {
770 assert!(argv[2].contains("PASSWORD\""), "{e}: {}", argv[2]);
771 }
772 }
773 }
774 assert!(Engine::Mysql.restores_from(Engine::Mariadb));
775 assert!(!Engine::Postgres.restores_from(Engine::Mysql));
776 }
777}