merge: general cli improvements
This commit is contained in:
10
README.md
10
README.md
@@ -10,10 +10,14 @@ You will need rust installed. Clone repo and run:
|
|||||||
cargo install --path .
|
cargo install --path .
|
||||||
```
|
```
|
||||||
|
|
||||||
To print the underlying docker commands as they run, build with debug
|
## seeing what it runs
|
||||||
assertions:
|
|
||||||
|
`-v` prints each docker command as it runs, and `--dry-run` prints the ones it
|
||||||
|
would run without running them. Both work before or after the subcommand.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo install --path . --debug
|
ahab -v django test
|
||||||
|
ahab --dry-run postgres import ./dump
|
||||||
```
|
```
|
||||||
|
|
||||||
## shell completion
|
## shell completion
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ use super::{Django, Link, Postgres};
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
|
|
||||||
/// A program for interacting with various dockerized applications.
|
/// A program for interacting with various dockerized applications
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(author, version, about, long_about=None)]
|
#[command(author, version, about, long_about=None)]
|
||||||
pub struct Ahab {
|
pub struct Ahab {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Commands,
|
pub command: Commands,
|
||||||
}
|
|
||||||
|
|
||||||
|
/// Print each docker command as it runs
|
||||||
|
#[arg(short, long, global = true)]
|
||||||
|
pub verbose: bool,
|
||||||
|
|
||||||
|
/// Print the docker commands that would run, without running them
|
||||||
|
#[arg(long, global = true)]
|
||||||
|
pub dry_run: bool,
|
||||||
|
}
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Django related subcommands
|
/// Django related subcommands
|
||||||
|
|||||||
@@ -9,19 +9,19 @@ pub enum Django {
|
|||||||
/// Start a bash session in a fresh django container
|
/// Start a bash session in a fresh django container
|
||||||
Bash,
|
Bash,
|
||||||
|
|
||||||
/// Prepare empty management command 'command' in app 'app'.
|
/// Prepare empty management command 'command' in app 'app'
|
||||||
MakeCommand { app: PathBuf, name: String },
|
MakeCommand { app: PathBuf, name: String },
|
||||||
|
|
||||||
/// Run Django's manage.py makemigrations.
|
/// Run Django's manage.py makemigrations
|
||||||
Makemigrations,
|
Makemigrations,
|
||||||
|
|
||||||
/// Pass arguments to Django's manage.py.
|
/// Pass arguments to Django's manage.py
|
||||||
Manage {
|
Manage {
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
rest: Vec<String>,
|
rest: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Run Django's manage.py migrate.
|
/// Run Django's manage.py migrate
|
||||||
Migrate {
|
Migrate {
|
||||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
rest: Vec<String>,
|
rest: Vec<String>,
|
||||||
@@ -33,9 +33,9 @@ pub enum Django {
|
|||||||
rest: Vec<String>,
|
rest: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Run Django's manage.py shell.
|
/// Run Django's manage.py shell
|
||||||
Shell,
|
Shell,
|
||||||
|
|
||||||
/// Run Django's manage.py test.
|
/// Run Django's manage.py test
|
||||||
Test,
|
Test,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ pub enum Link {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
porcelain: bool,
|
porcelain: bool,
|
||||||
|
|
||||||
|
/// Exit with 1 when anything is outside the store, for scripts
|
||||||
|
#[arg(long)]
|
||||||
|
exit_code: bool,
|
||||||
|
|
||||||
/// Terminate porcelain entries with NUL
|
/// Terminate porcelain entries with NUL
|
||||||
#[arg(short = 'z')]
|
#[arg(short = 'z')]
|
||||||
null: bool,
|
null: bool,
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use std::{
|
use std::{
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, ExitStatus, Stdio},
|
||||||
|
sync::OnceLock,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::debug_eprintln;
|
|
||||||
|
|
||||||
pub struct Args(Vec<String>);
|
pub struct Args(Vec<String>);
|
||||||
|
|
||||||
impl From<&str> for Args {
|
impl From<&str> for Args {
|
||||||
@@ -14,12 +13,6 @@ impl From<&str> for Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&String> for Args {
|
|
||||||
fn from(value: &String) -> Self {
|
|
||||||
Self(Vec::from_iter(value.split_whitespace().map(String::from)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&[String]> for Args {
|
impl From<&[String]> for Args {
|
||||||
fn from(value: &[String]) -> Self {
|
fn from(value: &[String]) -> Self {
|
||||||
Self(value.to_vec())
|
Self(value.to_vec())
|
||||||
@@ -46,8 +39,8 @@ impl CommandBuilder {
|
|||||||
Self::default().args("docker compose")
|
Self::default().args("docker compose")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn arg(mut self, arg: impl Into<String>) -> Self {
|
pub fn arg(mut self, arg: impl AsRef<str>) -> Self {
|
||||||
self.args.push(arg.into());
|
self.args.push(arg.as_ref().to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,8 +53,9 @@ impl CommandBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn build(self) -> Result<Command> {
|
pub fn build(self) -> Result<Command> {
|
||||||
debug_eprintln!("running `{self}`");
|
if options().verbose {
|
||||||
|
eprintln!("running `{self}`");
|
||||||
|
}
|
||||||
let (first, rest) = self.args.split_first().context("empty args")?;
|
let (first, rest) = self.args.split_first().context("empty args")?;
|
||||||
let mut command = Command::new(first);
|
let mut command = Command::new(first);
|
||||||
command.args(rest);
|
command.args(rest);
|
||||||
@@ -70,12 +64,23 @@ impl CommandBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_get_stdout(self) -> Result<String> {
|
pub fn exec_get_stdout(self) -> Result<String> {
|
||||||
Ok(String::from_utf8(self.build()?.output()?.stdout)?)
|
let shown = self.to_string();
|
||||||
|
let out = self.build()?.output()?;
|
||||||
|
|
||||||
|
check(&shown, out.status)?;
|
||||||
|
Ok(String::from_utf8(out.stdout)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(self) -> Result<()> {
|
pub fn exec(self) -> Result<()> {
|
||||||
self.build()?.spawn()?.wait()?;
|
if options().dry_run {
|
||||||
Ok(())
|
eprintln!("would run `{self}`");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let shown = self.to_string();
|
||||||
|
let status = self.build()?.spawn()?.wait()?;
|
||||||
|
|
||||||
|
check(&shown, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn(self) -> Result<Child> {
|
pub fn spawn(self) -> Result<Child> {
|
||||||
@@ -83,7 +88,45 @@ impl CommandBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_redirect_stdout(self, stdio: Stdio) -> Result<()> {
|
pub fn exec_redirect_stdout(self, stdio: Stdio) -> Result<()> {
|
||||||
self.build()?.stdout(stdio).spawn()?.wait()?;
|
if options().dry_run {
|
||||||
Ok(())
|
eprintln!("would run `{self}`");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let shown = self.to_string();
|
||||||
|
let status = self.build()?.stdout(stdio).spawn()?.wait()?;
|
||||||
|
|
||||||
|
check(&shown, status)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn check(command: &str, status: ExitStatus) -> Result<()> {
|
||||||
|
if status.success() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
match status.code() {
|
||||||
|
Some(code) => bail!("`{command}` exited with {code}"),
|
||||||
|
None => bail!("`{command}` was killed by a signal"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Clone, Copy)]
|
||||||
|
pub struct Options {
|
||||||
|
pub verbose: bool,
|
||||||
|
pub dry_run: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
static OPTIONS: OnceLock<Options> = OnceLock::new();
|
||||||
|
|
||||||
|
pub fn set_options(options: Options) {
|
||||||
|
let _ = OPTIONS.set(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn options() -> Options {
|
||||||
|
OPTIONS.get().copied().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_dry_run() -> bool {
|
||||||
|
options().dry_run
|
||||||
|
}
|
||||||
|
|||||||
10
src/main.rs
10
src/main.rs
@@ -1,4 +1,4 @@
|
|||||||
use ahab::{cli, scripts};
|
use ahab::{cli, command_builder, scripts};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
@@ -6,6 +6,11 @@ use clap::Parser;
|
|||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
let args = cli::Ahab::parse();
|
let args = cli::Ahab::parse();
|
||||||
|
|
||||||
|
command_builder::set_options(command_builder::Options {
|
||||||
|
verbose: args.verbose,
|
||||||
|
dry_run: args.dry_run,
|
||||||
|
});
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
cli::Commands::Django { command } => match command {
|
cli::Commands::Django { command } => match command {
|
||||||
cli::Django::Bash => scripts::django::bash(),
|
cli::Django::Bash => scripts::django::bash(),
|
||||||
@@ -29,7 +34,8 @@ fn main() -> Result<()> {
|
|||||||
paths,
|
paths,
|
||||||
porcelain,
|
porcelain,
|
||||||
null,
|
null,
|
||||||
} => scripts::link::check(&paths, porcelain, null),
|
exit_code,
|
||||||
|
} => scripts::link::check(&paths, porcelain, null, exit_code),
|
||||||
},
|
},
|
||||||
cli::Commands::Completions { shell } => scripts::completions::completions(shell),
|
cli::Commands::Completions { shell } => scripts::completions::completions(shell),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use std::path::{Path, PathBuf};
|
|||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
use crate::command_builder::CommandBuilder;
|
|
||||||
use crate::compose::Compose;
|
use crate::compose::Compose;
|
||||||
use crate::scripts::docker_compose;
|
use crate::scripts::docker_compose;
|
||||||
use crate::{create_file, safe_create_file};
|
use crate::{create_file, safe_create_file};
|
||||||
@@ -65,10 +64,10 @@ pub fn run(rest: &[String]) -> Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn manage(rest: &[String]) -> Result<()> {
|
pub fn manage(rest: &[String]) -> Result<()> {
|
||||||
let container = Compose::resolve()?.django()?;
|
let mut args = vec!["python".to_string(), "manage.py".to_string()];
|
||||||
let joined = rest.join(" ");
|
args.extend_from_slice(rest);
|
||||||
let command = format!("run --rm {container} python manage.py {joined}");
|
|
||||||
CommandBuilder::docker_compose().args(&command).exec()
|
run(&args)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcuts
|
// shortcuts
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
|
|
||||||
use crate::command_builder::CommandBuilder;
|
|
||||||
|
|
||||||
pub fn stop_all() -> Result<()> {
|
|
||||||
let running_containers = CommandBuilder::docker().args("ps -q").exec_get_stdout()?;
|
|
||||||
|
|
||||||
if running_containers.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
CommandBuilder::docker()
|
|
||||||
.args(&format!("stop {running_containers}"))
|
|
||||||
.exec()
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ pub fn down() -> Result<()> {
|
|||||||
pub fn run(service: &str, rest: &[String]) -> Result<()> {
|
pub fn run(service: &str, rest: &[String]) -> Result<()> {
|
||||||
CommandBuilder::docker_compose()
|
CommandBuilder::docker_compose()
|
||||||
.args("run --rm")
|
.args("run --rm")
|
||||||
.args(service)
|
.arg(service)
|
||||||
.args(rest)
|
.args(rest)
|
||||||
.exec()
|
.exec()
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,7 @@ pub fn run(service: &str, rest: &[String]) -> Result<()> {
|
|||||||
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
|
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
|
||||||
CommandBuilder::docker_compose()
|
CommandBuilder::docker_compose()
|
||||||
.args("exec")
|
.args("exec")
|
||||||
.args(service)
|
.arg(service)
|
||||||
.args(rest)
|
.args(rest)
|
||||||
.exec()
|
.exec()
|
||||||
}
|
}
|
||||||
@@ -30,9 +30,13 @@ pub fn ps() -> Result<()> {
|
|||||||
CommandBuilder::docker_compose().args("ps").exec()
|
CommandBuilder::docker_compose().args("ps").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start(containers: Option<&str>) -> Result<()> {
|
pub fn start(service: Option<&str>) -> Result<()> {
|
||||||
let args = format!("start {}", containers.unwrap_or(""));
|
let mut command = CommandBuilder::docker_compose().args("start");
|
||||||
CommandBuilder::docker_compose().args(&args).exec()
|
if let Some(service) = service {
|
||||||
|
command = command.arg(service);
|
||||||
|
}
|
||||||
|
|
||||||
|
command.exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stop() -> Result<()> {
|
pub fn stop() -> Result<()> {
|
||||||
|
|||||||
@@ -2,8 +2,10 @@ use std::cell::Cell;
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{self, Write};
|
||||||
use std::os::unix::fs::symlink;
|
use std::os::unix::fs::symlink;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
use anyhow::{Context, Result, anyhow};
|
use anyhow::{Context, Result, anyhow};
|
||||||
@@ -62,7 +64,7 @@ fn warn(msg: impl std::fmt::Display) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// list untracked paths not in the store, i.e. what a sandbox can still read
|
// list untracked paths not in the store, i.e. what a sandbox can still read
|
||||||
pub fn check(paths: &[PathBuf], porcelain: bool, null: bool) -> Result<()> {
|
pub fn check(paths: &[PathBuf], porcelain: bool, null: bool, exit_code: bool) -> Result<()> {
|
||||||
let repo = Repo::discover()?;
|
let repo = Repo::discover()?;
|
||||||
let pathspecs = relative_pathspecs(&repo, paths)?;
|
let pathspecs = relative_pathspecs(&repo, paths)?;
|
||||||
|
|
||||||
@@ -89,6 +91,12 @@ pub fn check(paths: &[PathBuf], porcelain: bool, null: bool) -> Result<()> {
|
|||||||
print_listing(&repo, &exposed);
|
print_listing(&repo, &exposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// git's --exit-code convention: nothing to report is 0, anything is 1
|
||||||
|
if exit_code && !exposed.is_empty() {
|
||||||
|
io::stdout().flush().context("writing the listing")?;
|
||||||
|
process::exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
pub mod completions;
|
pub mod completions;
|
||||||
pub mod django;
|
pub mod django;
|
||||||
pub mod docker;
|
|
||||||
pub mod docker_compose;
|
pub mod docker_compose;
|
||||||
pub mod link;
|
pub mod link;
|
||||||
pub mod postgres;
|
pub mod postgres;
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
use anyhow::{Context, Result, anyhow, bail};
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
use std::{
|
use std::{
|
||||||
fs::File,
|
fs::{self, File},
|
||||||
io::{self, Read, Write},
|
io::{self, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
thread,
|
thread,
|
||||||
time::Duration,
|
time::{Duration, Instant},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::docker_compose;
|
use super::docker_compose;
|
||||||
use crate::cli::Format;
|
use crate::cli::Format;
|
||||||
use crate::{command_builder::CommandBuilder, compose::Compose, debug_eprintln};
|
use crate::command_builder;
|
||||||
|
use crate::{command_builder::CommandBuilder, compose::Compose};
|
||||||
|
|
||||||
const CUSTOM_MAGIC: &[u8] = b"PGDMP";
|
const CUSTOM_MAGIC: &[u8] = b"PGDMP";
|
||||||
const TAR_MAGIC: &[u8] = b"toc.dat";
|
const TAR_MAGIC: &[u8] = b"toc.dat";
|
||||||
const GZIP_MAGIC: &[u8] = b"\x1f\x8b";
|
const GZIP_MAGIC: &[u8] = b"\x1f\x8b";
|
||||||
const CLUSTER_MARKER: &str = "PostgreSQL database cluster dump";
|
const CLUSTER_MARKER: &str = "PostgreSQL database cluster dump";
|
||||||
|
const READY_TIMEOUT: Duration = Duration::from_secs(60);
|
||||||
|
const POLL_INTERVAL: Duration = Duration::from_secs(1);
|
||||||
|
|
||||||
// wide enough for the cluster marker, which sits a few bytes into the file
|
// wide enough for the cluster marker, which sits a few bytes into the file
|
||||||
const HEADER_LEN: usize = 512;
|
const HEADER_LEN: usize = 512;
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ impl Database {
|
|||||||
|
|
||||||
let container = CommandBuilder::docker_compose()
|
let container = CommandBuilder::docker_compose()
|
||||||
.args("ps -q")
|
.args("ps -q")
|
||||||
.args(&service)
|
.arg(&service)
|
||||||
.exec_get_stdout()?
|
.exec_get_stdout()?
|
||||||
.trim()
|
.trim()
|
||||||
.to_string();
|
.to_string();
|
||||||
@@ -157,6 +161,13 @@ fn restore_cluster(db: &Database, script: &str, file: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn suffixed(path: &Path, suffix: &str) -> PathBuf {
|
||||||
|
let mut out = path.as_os_str().to_owned();
|
||||||
|
out.push(suffix);
|
||||||
|
|
||||||
|
PathBuf::from(out)
|
||||||
|
}
|
||||||
|
|
||||||
fn read_header(path: &Path) -> Result<Vec<u8>> {
|
fn read_header(path: &Path) -> Result<Vec<u8>> {
|
||||||
let mut file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
|
let mut file = File::open(path).with_context(|| format!("opening {}", path.display()))?;
|
||||||
let mut header = vec![0; HEADER_LEN];
|
let mut header = vec![0; HEADER_LEN];
|
||||||
@@ -173,7 +184,7 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result<std::process::Comm
|
|||||||
let file = File::open(input).with_context(|| format!("opening {}", input.display()))?;
|
let file = File::open(input).with_context(|| format!("opening {}", input.display()))?;
|
||||||
let mut command = CommandBuilder::docker()
|
let mut command = CommandBuilder::docker()
|
||||||
.args("exec -i")
|
.args("exec -i")
|
||||||
.args(&db.container)
|
.arg(&db.container)
|
||||||
.args("sh -c")
|
.args("sh -c")
|
||||||
.arg(script)
|
.arg(script)
|
||||||
.build()?;
|
.build()?;
|
||||||
@@ -183,27 +194,49 @@ fn piped(db: &Database, script: &str, input: &Path) -> Result<std::process::Comm
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_ready(db: &Database) -> Result<()> {
|
fn wait_until_ready(db: &Database) -> Result<()> {
|
||||||
debug_eprintln!("waiting until pg_isready");
|
if command_builder::is_dry_run() {
|
||||||
while !CommandBuilder::docker()
|
return Ok(());
|
||||||
.args(&format!(
|
}
|
||||||
"exec {} pg_isready -U {} -d {}",
|
|
||||||
db.container, db.user, db.name
|
let deadline = Instant::now() + READY_TIMEOUT;
|
||||||
))
|
|
||||||
|
loop {
|
||||||
|
let ready = CommandBuilder::docker()
|
||||||
|
.args("exec")
|
||||||
|
.arg(&db.container)
|
||||||
|
.args("pg_isready -U")
|
||||||
|
.arg(&db.user)
|
||||||
|
.args("-d")
|
||||||
|
.arg(&db.name)
|
||||||
.build()?
|
.build()?
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
.spawn()?
|
.spawn()?
|
||||||
.wait()?
|
.wait()?
|
||||||
.success()
|
.success();
|
||||||
{
|
|
||||||
thread::sleep(Duration::from_secs(1));
|
if ready {
|
||||||
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
if Instant::now() >= deadline {
|
||||||
|
bail!(
|
||||||
|
"{} did not accept connections within {} seconds",
|
||||||
|
db.service,
|
||||||
|
READY_TIMEOUT.as_secs()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_when_ready(db: &Database, command: &str) -> Result<()> {
|
thread::sleep(POLL_INTERVAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn when_ready(db: &Database, command: CommandBuilder) -> Result<()> {
|
||||||
wait_until_ready(db)?;
|
wait_until_ready(db)?;
|
||||||
CommandBuilder::docker().args(command).exec()
|
command.exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_container(db: &Database) -> CommandBuilder {
|
||||||
|
CommandBuilder::docker().args("exec").arg(&db.container)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import(file: &Path) -> Result<()> {
|
pub fn import(file: &Path) -> Result<()> {
|
||||||
@@ -220,9 +253,12 @@ pub fn import(file: &Path) -> Result<()> {
|
|||||||
|
|
||||||
// a directory cannot be streamed, so it is the one shape that gets copied in
|
// a directory cannot be streamed, so it is the one shape that gets copied in
|
||||||
if matches!(dump, Dump::Directory) {
|
if matches!(dump, Dump::Directory) {
|
||||||
run_when_ready(
|
when_ready(
|
||||||
&db,
|
&db,
|
||||||
&format!("cp -L {} {}:{remote}", file.display(), db.container),
|
CommandBuilder::docker()
|
||||||
|
.args("cp -L")
|
||||||
|
.arg(file.to_string_lossy())
|
||||||
|
.arg(format!("{}:{remote}", db.container)),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,24 +300,32 @@ pub fn import(file: &Path) -> Result<()> {
|
|||||||
|
|
||||||
let tool = kind.tool();
|
let tool = kind.tool();
|
||||||
eprintln!("restoring database with {tool}");
|
eprintln!("restoring database with {tool}");
|
||||||
run_when_ready(
|
|
||||||
|
when_ready(
|
||||||
&db,
|
&db,
|
||||||
&format!("exec {} dropdb -U {} {}", db.container, db.user, db.name),
|
in_container(&db)
|
||||||
|
.args("dropdb -U")
|
||||||
|
.arg(&db.user)
|
||||||
|
.arg(&db.name),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// a cluster dump creates the database itself, and would trip over one that
|
// a cluster dump creates the database itself, and would trip over one that
|
||||||
// is already there
|
// is already there
|
||||||
if kind != Kind::Cluster {
|
if kind != Kind::Cluster {
|
||||||
run_when_ready(
|
when_ready(
|
||||||
&db,
|
&db,
|
||||||
&format!(
|
in_container(&db)
|
||||||
"exec {} createdb -U {} -E utf8 -T template0 {}",
|
.args("createdb -U")
|
||||||
db.container, db.user, db.name
|
.arg(&db.user)
|
||||||
),
|
.args("-E utf8 -T template0")
|
||||||
|
.arg(&db.name),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_until_ready(&db)?;
|
wait_until_ready(&db)?;
|
||||||
|
if command_builder::is_dry_run() {
|
||||||
|
eprintln!("would restore with {tool}");
|
||||||
|
} else {
|
||||||
match (kind, restore.as_deref()) {
|
match (kind, restore.as_deref()) {
|
||||||
// psql's output is read rather than streamed here, to keep the expected
|
// psql's output is read rather than streamed here, to keep the expected
|
||||||
// role errors out of the way
|
// role errors out of the way
|
||||||
@@ -289,11 +333,11 @@ pub fn import(file: &Path) -> Result<()> {
|
|||||||
(_, restore) => {
|
(_, restore) => {
|
||||||
let status = match restore {
|
let status = match restore {
|
||||||
Some(script) => piped(&db, script, file)?.spawn()?.wait()?,
|
Some(script) => piped(&db, script, file)?.spawn()?.wait()?,
|
||||||
None => CommandBuilder::docker()
|
None => in_container(&db)
|
||||||
.args(&format!(
|
.args("pg_restore -U")
|
||||||
"exec {} pg_restore -U {} --dbname={} {remote}",
|
.arg(&db.user)
|
||||||
db.container, db.user, db.name
|
.arg(format!("--dbname={}", db.name))
|
||||||
))
|
.arg(&remote)
|
||||||
.build()?
|
.build()?
|
||||||
.spawn()?
|
.spawn()?
|
||||||
.wait()?,
|
.wait()?,
|
||||||
@@ -304,11 +348,10 @@ pub fn import(file: &Path) -> Result<()> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(dump, Dump::Directory) {
|
if matches!(dump, Dump::Directory) {
|
||||||
let _ = CommandBuilder::docker()
|
let _ = in_container(&db).args("rm -rf").arg(&remote).exec();
|
||||||
.args(&format!("exec {} rm -rf {remote}", db.container))
|
|
||||||
.exec();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eprintln!("restarting containers");
|
eprintln!("restarting containers");
|
||||||
@@ -331,22 +374,42 @@ pub fn dump(file: &PathBuf, format: Format, gzip: bool) -> Result<()> {
|
|||||||
|
|
||||||
eprintln!("dumping to local file {}", file.to_string_lossy());
|
eprintln!("dumping to local file {}", file.to_string_lossy());
|
||||||
|
|
||||||
let stdout = Stdio::from(File::create(file)?);
|
// written beside the target and renamed once the dump succeeds, so a failure
|
||||||
let dumping = dump_command(&db, format);
|
// cannot destroy the dump that is already there
|
||||||
|
let partial = suffixed(file, ".partial");
|
||||||
|
let stdout = Stdio::from(
|
||||||
|
File::create(&partial).with_context(|| format!("creating {}", partial.display()))?,
|
||||||
|
);
|
||||||
|
|
||||||
if gzip {
|
let dumped = if gzip {
|
||||||
|
// the whole pipeline has to arrive as one shell argument
|
||||||
CommandBuilder::docker()
|
CommandBuilder::docker()
|
||||||
.args("exec")
|
.args("exec")
|
||||||
.args(&db.container)
|
.arg(&db.container)
|
||||||
.args("sh -c")
|
.args("sh -c")
|
||||||
.arg(format!("{dumping} | gzip"))
|
.arg(format!("{} | gzip", dump_command(&db, format)))
|
||||||
.exec_redirect_stdout(stdout)?;
|
.exec_redirect_stdout(stdout)
|
||||||
} else {
|
} else {
|
||||||
CommandBuilder::docker()
|
let dumping = match format.flag() {
|
||||||
.args(&format!("exec {} {dumping}", db.container))
|
Some(flag) => in_container(&db)
|
||||||
.exec_redirect_stdout(stdout)?;
|
.args("pg_dump -U")
|
||||||
|
.arg(&db.user)
|
||||||
|
.arg(format!("--format={flag}"))
|
||||||
|
.arg(&db.name),
|
||||||
|
None => in_container(&db).args("pg_dumpall -U").arg(&db.user),
|
||||||
|
};
|
||||||
|
|
||||||
|
dumping.exec_redirect_stdout(stdout)
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Err(e) = dumped {
|
||||||
|
let _ = fs::remove_file(&partial);
|
||||||
|
return Err(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fs::rename(&partial, file)
|
||||||
|
.with_context(|| format!("renaming {} to {}", partial.display(), file.display()))?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,24 +434,21 @@ fn dump_directory(db: &Database, target: &Path) -> Result<()> {
|
|||||||
eprintln!("dumping to local directory {}", target.display());
|
eprintln!("dumping to local directory {}", target.display());
|
||||||
let remote = remote_dump();
|
let remote = remote_dump();
|
||||||
|
|
||||||
CommandBuilder::docker()
|
in_container(db)
|
||||||
.args(&format!(
|
.args("pg_dump -U")
|
||||||
"exec {} pg_dump -U {} --format=d -f {remote} {}",
|
.arg(&db.user)
|
||||||
db.container, db.user, db.name
|
.args("--format=d -f")
|
||||||
))
|
.arg(&remote)
|
||||||
|
.arg(&db.name)
|
||||||
.exec()?;
|
.exec()?;
|
||||||
|
|
||||||
let copied = CommandBuilder::docker()
|
let copied = CommandBuilder::docker()
|
||||||
.args(&format!(
|
.args("cp")
|
||||||
"cp {}:{remote} {}",
|
.arg(format!("{}:{remote}", db.container))
|
||||||
db.container,
|
.arg(target.to_string_lossy())
|
||||||
target.display()
|
|
||||||
))
|
|
||||||
.exec();
|
.exec();
|
||||||
|
|
||||||
let _ = CommandBuilder::docker()
|
let _ = in_container(db).args("rm -rf").arg(&remote).exec();
|
||||||
.args(&format!("exec {} rm -rf {remote}", db.container))
|
|
||||||
.exec();
|
|
||||||
|
|
||||||
copied
|
copied
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user