99 lines
3.0 KiB
Rust
99 lines
3.0 KiB
Rust
use std::thread;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use anyhow::{Result, bail};
|
|
|
|
use super::shape::Kind;
|
|
use crate::cmd::{Cmd, PgIsReady, PgRestore, Ps, Psql};
|
|
use crate::ctx::Ctx;
|
|
use crate::project::Project;
|
|
|
|
const READY_TIMEOUT: Duration = Duration::from_secs(60);
|
|
const POLL_INTERVAL: Duration = Duration::from_secs(1);
|
|
// stands in for an id a dry run has no running container to look up
|
|
const PLANNED_CONTAINER: &str = "<db container>";
|
|
|
|
pub(super) struct Database {
|
|
pub(super) service: String,
|
|
pub(super) container: String,
|
|
pub(super) user: String,
|
|
pub(super) name: String,
|
|
}
|
|
|
|
impl Database {
|
|
pub(super) fn resolve(ctx: &Ctx) -> Result<Self> {
|
|
let compose = Project::resolve(ctx)?;
|
|
let service = compose.postgres()?;
|
|
let (user, name) = compose.postgres_credentials(&service)?;
|
|
|
|
let listed = Ps::id_of(&service).capture(ctx)?;
|
|
let mut ids = listed.lines().map(str::trim).filter(|id| !id.is_empty());
|
|
|
|
let container = match (ids.next(), ids.next()) {
|
|
(Some(id), None) => id.to_string(),
|
|
// one id per line: a scaled service has several, and the whole
|
|
// listing would go to docker exec as though it were a single id
|
|
(Some(_), Some(_)) => bail!(
|
|
"service {service} has more than one container running; \
|
|
scale it to one first"
|
|
),
|
|
// a dry run only prints a plan, and it is worth printing with the
|
|
// project down, which is when it is most likely to be asked for
|
|
(None, _) if ctx.dry_run => PLANNED_CONTAINER.to_string(),
|
|
(None, _) => bail!("service {service} has no running container"),
|
|
};
|
|
|
|
Ok(Self {
|
|
service,
|
|
container,
|
|
user,
|
|
name,
|
|
})
|
|
}
|
|
|
|
// what reads this shape of dump back in
|
|
pub(super) fn restore_with(&self, kind: Kind) -> Box<dyn Cmd + '_> {
|
|
match kind {
|
|
Kind::Archive => Box::new(PgRestore::new(&self.user, &self.name)),
|
|
Kind::Sql => Box::new(Psql::new(&self.user, &self.name).quiet().atomic()),
|
|
Kind::Cluster => Box::new(Psql::new(&self.user, "postgres").quiet()),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn wait_until_ready(ctx: &Ctx, db: &Database) -> Result<()> {
|
|
if ctx.dry_run {
|
|
return Ok(());
|
|
}
|
|
|
|
let deadline = Instant::now() + READY_TIMEOUT;
|
|
|
|
loop {
|
|
let ready = PgIsReady {
|
|
username: &db.user,
|
|
dbname: &db.name,
|
|
}
|
|
.in_container(&db.container)
|
|
.quietly_succeeds(ctx)?;
|
|
|
|
if ready {
|
|
return Ok(());
|
|
}
|
|
|
|
if Instant::now() >= deadline {
|
|
bail!(
|
|
"{} did not accept connections within {} seconds",
|
|
db.service,
|
|
READY_TIMEOUT.as_secs()
|
|
);
|
|
}
|
|
|
|
thread::sleep(POLL_INTERVAL);
|
|
}
|
|
}
|
|
|
|
pub(super) fn when_ready(ctx: &Ctx, db: &Database, command: &dyn Cmd) -> Result<()> {
|
|
wait_until_ready(ctx, db)?;
|
|
command.run(ctx)
|
|
}
|