merge: compose subcommand deprecation
This commit is contained in:
68
Cargo.lock
generated
68
Cargo.lock
generated
@@ -9,7 +9,7 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
"dotenvy",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -123,12 +123,6 @@ version = "1.0.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dotenvy"
|
|
||||||
version = "0.15.7"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -141,6 +135,18 @@ version = "1.70.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itoa"
|
||||||
|
version = "1.0.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell_polyfill"
|
name = "once_cell_polyfill"
|
||||||
version = "1.70.2"
|
version = "1.70.2"
|
||||||
@@ -165,6 +171,48 @@ dependencies = [
|
|||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
|
||||||
|
dependencies = [
|
||||||
|
"serde_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_core"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
|
||||||
|
dependencies = [
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_derive"
|
||||||
|
version = "1.0.229"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_json"
|
||||||
|
version = "1.0.151"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
|
||||||
|
dependencies = [
|
||||||
|
"itoa",
|
||||||
|
"memchr",
|
||||||
|
"serde",
|
||||||
|
"serde_core",
|
||||||
|
"zmij",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strsim"
|
name = "strsim"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
@@ -208,3 +256,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zmij"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ build = "build.rs"
|
|||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive"] }
|
||||||
clap_complete = "4.6.9"
|
clap_complete = "4.6.9"
|
||||||
anyhow = "1.0.104"
|
anyhow = "1.0.104"
|
||||||
dotenvy = "0.15.7"
|
serde_json = "1.0.145"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
clap = { version = "4.6.6", features = ["derive"] }
|
clap = { version = "4.6.6", features = ["derive"] }
|
||||||
|
|||||||
94
README.md
94
README.md
@@ -1,5 +1,8 @@
|
|||||||
# ahab
|
# ahab
|
||||||
|
|
||||||
|
A wrapper around `docker compose` for our dockerized django projects, so the
|
||||||
|
same commands work in every repository.
|
||||||
|
|
||||||
## installing
|
## installing
|
||||||
|
|
||||||
You will need rust installed. Clone repo and run:
|
You will need rust installed. Clone repo and run:
|
||||||
@@ -7,20 +10,101 @@ You will need rust installed. Clone repo and run:
|
|||||||
cargo install --path .
|
cargo install --path .
|
||||||
```
|
```
|
||||||
|
|
||||||
To print underlying commands build with `debug` flag.
|
To print the underlying docker commands as they run, build with debug
|
||||||
|
assertions:
|
||||||
```bash
|
```bash
|
||||||
cargo install --path . --debug
|
cargo install --path . --debug
|
||||||
```
|
```
|
||||||
|
|
||||||
## shell completion
|
## shell completion
|
||||||
|
|
||||||
Completion files are generated **during build process** in `target/*/build/*/out/*`. Depending on your shell of choice move generated file to correct place in you file system.
|
`ahab completions <shell>` writes a completion script to stdout, for bash,
|
||||||
|
elvish, fish, powershell or zsh:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ahab completions zsh > ~/.local/share/zsh/completions/_ahab
|
||||||
|
eval "$(ahab completions zsh)" # or one line in .zshrc, never goes stale
|
||||||
|
```
|
||||||
|
|
||||||
|
Completions are also generated during the build. They land in
|
||||||
|
`target/*/build/*/out/` by default, and `SHELL_COMPLETIONS_DIR_<SHELL>`
|
||||||
|
installs a single shell's file straight into place:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SHELL_COMPLETIONS_DIR_ZSH=~/.local/share/zsh/completions \
|
||||||
|
SHELL_COMPLETIONS_DIR_FISH=~/.config/fish/completions \
|
||||||
|
cargo install --path .
|
||||||
|
```
|
||||||
|
|
||||||
|
`SHELL_COMPLETIONS_DIR` writes every shell into one directory instead.
|
||||||
|
|
||||||
|
## the compose file
|
||||||
|
|
||||||
|
`ahab` does not pass `-f`. docker compose finds the file itself, so set
|
||||||
|
docker's own `COMPOSE_FILE` when it is not in the working directory, including
|
||||||
|
its `base.yaml:override.yaml` form. A project's `.env` is a good place for it,
|
||||||
|
since docker reads that too:
|
||||||
|
|
||||||
|
```
|
||||||
|
COMPOSE_FILE=docker/docker-compose.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## service detection
|
||||||
|
|
||||||
|
`ahab` finds the services it needs in `docker compose config`, so they can be
|
||||||
|
named anything:
|
||||||
|
|
||||||
|
- **postgres**: the service whose image is a postgres flavour, matching
|
||||||
|
`postg`, `timescale`, `pgvector` or `citus`
|
||||||
|
- **django**: the service that both builds an image and sets
|
||||||
|
`DJANGO_SETTINGS_MODULE`. A worker sharing the same build and `env_file`
|
||||||
|
matches too, so the one publishing ports wins
|
||||||
|
|
||||||
|
If nothing matches, or two candidates cannot be told apart, `ahab` says so and
|
||||||
|
lists the services it looked at rather than guessing.
|
||||||
|
|
||||||
|
`POSTGRES_USER` and `POSTGRES_DB` are read off the detected postgres service, so
|
||||||
|
`dropdb`, `createdb`, `pg_restore` and `pg_dump` use the role and database the
|
||||||
|
project declares, falling back to `db` when it declares neither.
|
||||||
|
|
||||||
|
## django
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ahab django run <cmd> # in a fresh container, through the entrypoint
|
||||||
|
ahab django bash # shell in a fresh container
|
||||||
|
ahab django manage <args> # manage.py
|
||||||
|
ahab django makemigrations
|
||||||
|
ahab django migrate <args>
|
||||||
|
ahab django shell
|
||||||
|
ahab django test
|
||||||
|
ahab django make-command <app> <name>
|
||||||
|
```
|
||||||
|
|
||||||
|
## postgres
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ahab postgres dump <path> # pg_dump, custom format
|
||||||
|
ahab postgres import <path> # drop, create, pg_restore
|
||||||
|
```
|
||||||
|
|
||||||
|
## link
|
||||||
|
|
||||||
|
`ahab link` moves untracked paths out of the repository into an out-of-repo
|
||||||
|
store and symlinks them back, so a sandbox that mounts the repository sees a
|
||||||
|
dangling symlink instead of the contents, while the host resolves it as before.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ahab link add .env secrets/ # move out, leave symlinks behind
|
||||||
|
ahab link check # what a sandbox can still read
|
||||||
|
ahab link check --porcelain # `<code> <path>`, for scripts
|
||||||
|
```
|
||||||
|
|
||||||
|
The store lives under
|
||||||
|
`${XDG_DATA_HOME:-$HOME/.local/share}/ahab/<host>/<owner>/<repo>/`, derived
|
||||||
|
from the git `origin` remote.
|
||||||
|
|
||||||
## configuration
|
## configuration
|
||||||
|
|
||||||
Currently `ahab` respects the following environment variables.
|
Currently `ahab` respects the following environment variables.
|
||||||
|
|
||||||
- `COMPOSE_FILE`: control which docker-compose file is used - defaults to `docker/local/docker-compose.yaml`
|
|
||||||
- `AHAB_DJANGO_CONTAINER`: control which compose service is used for sending django commands - defaults to `django`
|
|
||||||
- `AHAB_POSTGRES_CONTAINER`: control which compose service is used for sending postgres commands - defaults to `db`
|
|
||||||
- `AHAB_LINK_ROOT`: root of the out-of-repo store `ahab link` moves paths into - defaults to `${XDG_DATA_HOME:-$HOME/.local/share}/ahab`
|
- `AHAB_LINK_ROOT`: root of the out-of-repo store `ahab link` moves paths into - defaults to `${XDG_DATA_HOME:-$HOME/.local/share}/ahab`
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use super::{Django, DockerCompose, Link, Postgres};
|
use super::{Django, Link, Postgres};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use clap_complete::Shell;
|
use clap_complete::Shell;
|
||||||
|
|
||||||
@@ -12,12 +12,6 @@ pub struct Ahab {
|
|||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
#[derive(Debug, Subcommand)]
|
||||||
pub enum Commands {
|
pub enum Commands {
|
||||||
/// Docker compose related subcommands
|
|
||||||
Compose {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: DockerCompose,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Django related subcommands
|
/// Django related subcommands
|
||||||
Django {
|
Django {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
|
|||||||
@@ -1,31 +1,35 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Subcommand;
|
||||||
|
|
||||||
// TODO: (matej) dsu template command
|
// TODO: (matej) dsu template command
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Django {
|
pub enum Django {
|
||||||
|
/// Start a bash session in a fresh django container
|
||||||
|
Bash,
|
||||||
|
|
||||||
/// Prepare empty management command 'command' in app 'app'.
|
/// Prepare empty management command 'command' in app 'app'.
|
||||||
MakeCommand {
|
MakeCommand { app: PathBuf, name: String },
|
||||||
#[arg(value_enum)]
|
|
||||||
app: PathBuf,
|
|
||||||
#[arg(value_enum)]
|
|
||||||
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(value_enum)]
|
#[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(value_enum)]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
|
rest: Vec<String>,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Run a command in a fresh django container, through its entrypoint
|
||||||
|
Run {
|
||||||
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
rest: Vec<String>,
|
rest: Vec<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
use clap::Subcommand;
|
|
||||||
|
|
||||||
// TODO: (matej) add Exec, Bash
|
|
||||||
|
|
||||||
/// Wraper for docker compose; autodiscover compose file and source .env file.
|
|
||||||
#[derive(Subcommand, Debug)]
|
|
||||||
pub enum DockerCompose {
|
|
||||||
/// Start bash session inside container
|
|
||||||
Bash,
|
|
||||||
|
|
||||||
/// Build containers.
|
|
||||||
Build,
|
|
||||||
|
|
||||||
/// Down containers.
|
|
||||||
Down,
|
|
||||||
|
|
||||||
/// Exec command inside container.
|
|
||||||
Exec {
|
|
||||||
#[arg(value_enum)]
|
|
||||||
rest: Vec<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Print services
|
|
||||||
Ps,
|
|
||||||
|
|
||||||
/// Stop, build and start containers.
|
|
||||||
Rebuild,
|
|
||||||
|
|
||||||
/// Stop and start containers.
|
|
||||||
Restart,
|
|
||||||
|
|
||||||
/// Start containers.
|
|
||||||
Start,
|
|
||||||
|
|
||||||
/// Stop containers.
|
|
||||||
Stop,
|
|
||||||
|
|
||||||
/// Up containers.
|
|
||||||
Up,
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
mod ahab;
|
mod ahab;
|
||||||
mod django;
|
mod django;
|
||||||
mod docker_compose;
|
|
||||||
mod link;
|
mod link;
|
||||||
mod postgres;
|
mod postgres;
|
||||||
|
|
||||||
pub use ahab::{Ahab, Commands};
|
pub use ahab::{Ahab, Commands};
|
||||||
pub use django::Django;
|
pub use django::Django;
|
||||||
pub use docker_compose::DockerCompose;
|
|
||||||
pub use link::Link;
|
pub use link::Link;
|
||||||
pub use postgres::Postgres;
|
pub use postgres::Postgres;
|
||||||
|
|||||||
@@ -5,14 +5,8 @@ use clap::Subcommand;
|
|||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
pub enum Postgres {
|
pub enum Postgres {
|
||||||
/// Import dump via pg_restore
|
/// Import dump via pg_restore
|
||||||
Import {
|
Import { path: PathBuf },
|
||||||
#[arg(value_enum)]
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Dump via pg_dump with format=c
|
/// Dump via pg_dump with format=c
|
||||||
Dump {
|
Dump { path: PathBuf },
|
||||||
#[arg(value_enum)]
|
|
||||||
path: PathBuf,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
|
||||||
fmt::Display,
|
fmt::Display,
|
||||||
process::{Child, Command, Stdio},
|
process::{Child, Command, Stdio},
|
||||||
};
|
};
|
||||||
@@ -27,11 +26,6 @@ impl From<&[String]> for Args {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_compose_file() -> Result<String> {
|
|
||||||
let cf = env::var("COMPOSE_FILE")?;
|
|
||||||
Ok(cf)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct CommandBuilder {
|
pub struct CommandBuilder {
|
||||||
args: Vec<String>,
|
args: Vec<String>,
|
||||||
@@ -49,9 +43,7 @@ impl CommandBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn docker_compose() -> Self {
|
pub fn docker_compose() -> Self {
|
||||||
let cf =
|
Self::default().args("docker compose")
|
||||||
get_compose_file().unwrap_or_else(|_| "docker/local/docker-compose.yaml".to_string());
|
|
||||||
Self::default().args("docker compose -f").args(&cf)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn args<T>(mut self, args: T) -> Self
|
pub fn args<T>(mut self, args: T) -> Self
|
||||||
|
|||||||
261
src/compose.rs
Normal file
261
src/compose.rs
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
use anyhow::{Context, Result, anyhow, bail};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::command_builder::CommandBuilder;
|
||||||
|
|
||||||
|
const POSTGRES_IMAGES: [&str; 4] = ["postg", "timescale", "pgvector", "citus"];
|
||||||
|
const DJANGO_SETTINGS_MODULE: &str = "DJANGO_SETTINGS_MODULE";
|
||||||
|
|
||||||
|
pub struct Compose {
|
||||||
|
services: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Compose {
|
||||||
|
pub fn resolve() -> Result<Self> {
|
||||||
|
let out = CommandBuilder::docker_compose()
|
||||||
|
.args("config --format json")
|
||||||
|
.build()?
|
||||||
|
.output()
|
||||||
|
.context("running docker compose config")?;
|
||||||
|
|
||||||
|
if !out.status.success() {
|
||||||
|
bail!(
|
||||||
|
"docker compose config failed: {}",
|
||||||
|
String::from_utf8_lossy(&out.stderr).trim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let config: Value =
|
||||||
|
serde_json::from_slice(&out.stdout).context("parsing docker compose config")?;
|
||||||
|
let services = config
|
||||||
|
.get("services")
|
||||||
|
.cloned()
|
||||||
|
.context("compose config has no services")?;
|
||||||
|
|
||||||
|
Ok(Self { services })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn django(&self) -> Result<String> {
|
||||||
|
let candidates: Vec<&str> = self
|
||||||
|
.names()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|name| {
|
||||||
|
let service = &self.services[name];
|
||||||
|
service.get("build").is_some() && env_var(service, DJANGO_SETTINGS_MODULE).is_some()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match candidates.as_slice() {
|
||||||
|
[] => Err(anyhow!(
|
||||||
|
"no service both builds an image and sets {DJANGO_SETTINGS_MODULE}, looked at {}",
|
||||||
|
self.names().join(", ")
|
||||||
|
)),
|
||||||
|
[only] => Ok(only.to_string()),
|
||||||
|
several => {
|
||||||
|
let serving: Vec<&&str> = several
|
||||||
|
.iter()
|
||||||
|
.filter(|name| self.publishes_ports(name))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match serving.as_slice() {
|
||||||
|
[only] => Ok(only.to_string()),
|
||||||
|
_ => Err(anyhow!(
|
||||||
|
"cannot tell which service runs django, {} all build an image, set \
|
||||||
|
{DJANGO_SETTINGS_MODULE} and publish ports",
|
||||||
|
several.join(", ")
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postgres(&self) -> Result<String> {
|
||||||
|
let candidates: Vec<&str> = self
|
||||||
|
.names()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|name| {
|
||||||
|
self.services[name]
|
||||||
|
.get("image")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.is_some_and(is_postgres_image)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
match candidates.as_slice() {
|
||||||
|
[] => Err(anyhow!(
|
||||||
|
"no service uses a postgres image, looked at {}",
|
||||||
|
self.names().join(", ")
|
||||||
|
)),
|
||||||
|
[only] => Ok(only.to_string()),
|
||||||
|
several => Err(anyhow!(
|
||||||
|
"cannot tell which service is the database, {} all use a postgres image",
|
||||||
|
several.join(", ")
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn postgres_credentials(&self, service: &str) -> (String, String) {
|
||||||
|
let service = &self.services[service];
|
||||||
|
|
||||||
|
let user = env_var(service, "POSTGRES_USER").unwrap_or_else(|| "db".to_string());
|
||||||
|
let database = env_var(service, "POSTGRES_DB").unwrap_or_else(|| "db".to_string());
|
||||||
|
|
||||||
|
(user, database)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn names(&self) -> Vec<&str> {
|
||||||
|
let mut names: Vec<&str> = self
|
||||||
|
.services
|
||||||
|
.as_object()
|
||||||
|
.map(|services| services.keys().map(String::as_str).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
names.sort_unstable();
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn publishes_ports(&self, service: &str) -> bool {
|
||||||
|
self.services[service]
|
||||||
|
.get("ports")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.is_some_and(|ports| !ports.is_empty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn env_var(service: &Value, key: &str) -> Option<String> {
|
||||||
|
service
|
||||||
|
.get("environment")?
|
||||||
|
.get(key)?
|
||||||
|
.as_str()
|
||||||
|
.map(str::to_string)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_postgres_image(image: &str) -> bool {
|
||||||
|
let image = image.to_lowercase();
|
||||||
|
POSTGRES_IMAGES.iter().any(|kind| image.contains(kind))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::{Compose, is_postgres_image};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
fn compose(services: serde_json::Value) -> Compose {
|
||||||
|
Compose { services }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recognises_postgres_flavours() {
|
||||||
|
for image in [
|
||||||
|
"postgres:18",
|
||||||
|
"postgis/postgis:16-3.4",
|
||||||
|
"timescale/timescaledb:latest-pg16",
|
||||||
|
"pgvector/pgvector:pg16",
|
||||||
|
"citusdata/citus:12",
|
||||||
|
"PostgreSQL:16",
|
||||||
|
] {
|
||||||
|
assert!(is_postgres_image(image), "{image} should count as postgres");
|
||||||
|
}
|
||||||
|
|
||||||
|
for image in ["redis:7-alpine", "mysql:8", "nginx", "local-django"] {
|
||||||
|
assert!(!is_postgres_image(image), "{image} should not");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn picks_the_service_that_builds_and_has_the_settings_module() {
|
||||||
|
let compose = compose(json!({
|
||||||
|
"db": {"image": "postgres:18", "environment": {"POSTGRES_DB": "n"}},
|
||||||
|
"django": {
|
||||||
|
"build": {"context": "."},
|
||||||
|
"environment": {"DJANGO_SETTINGS_MODULE": "config.settings.local"},
|
||||||
|
"ports": [{"target": 8000}],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(compose.django().unwrap(), "django");
|
||||||
|
assert_eq!(compose.postgres().unwrap(), "db");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn breaks_a_worker_tie_on_published_ports() {
|
||||||
|
let compose = compose(json!({
|
||||||
|
"django": {
|
||||||
|
"build": {"context": "."},
|
||||||
|
"environment": {"DJANGO_SETTINGS_MODULE": "config.settings.local"},
|
||||||
|
"ports": [{"target": 8000}],
|
||||||
|
},
|
||||||
|
"worker": {
|
||||||
|
"build": {"context": "."},
|
||||||
|
"environment": {"DJANGO_SETTINGS_MODULE": "config.settings.local"},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert_eq!(compose.django().unwrap(), "django");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuses_when_two_candidates_both_serve() {
|
||||||
|
let serving = json!({
|
||||||
|
"build": {"context": "."},
|
||||||
|
"environment": {"DJANGO_SETTINGS_MODULE": "config.settings.local"},
|
||||||
|
"ports": [{"target": 8000}],
|
||||||
|
});
|
||||||
|
let compose = compose(json!({"api": serving, "web": serving}));
|
||||||
|
|
||||||
|
let error = compose.django().unwrap_err().to_string();
|
||||||
|
assert!(error.contains("api, web"), "{error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuses_when_nothing_matches() {
|
||||||
|
let compose = compose(json!({
|
||||||
|
"db": {"image": "postgres:18"},
|
||||||
|
"cache": {"image": "redis:7"},
|
||||||
|
}));
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
compose
|
||||||
|
.django()
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("cache, db")
|
||||||
|
);
|
||||||
|
assert_eq!(compose.postgres().unwrap(), "db");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refuses_two_postgres_services() {
|
||||||
|
let compose = compose(json!({
|
||||||
|
"db": {"image": "postgres:18"},
|
||||||
|
"replica": {"image": "postgres:18"},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let error = compose.postgres().unwrap_err().to_string();
|
||||||
|
assert!(error.contains("db, replica"), "{error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reads_credentials_off_the_detected_service() {
|
||||||
|
let compose = compose(json!({
|
||||||
|
"db": {
|
||||||
|
"image": "postgres:18",
|
||||||
|
"environment": {"POSTGRES_USER": "myproject", "POSTGRES_DB": "myproject_db"},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
let (user, database) = compose.postgres_credentials("db");
|
||||||
|
assert_eq!(
|
||||||
|
(user.as_str(), database.as_str()),
|
||||||
|
("myproject", "myproject_db")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn falls_back_to_db_when_the_service_says_nothing() {
|
||||||
|
let compose = compose(json!({"db": {"image": "postgres:18"}}));
|
||||||
|
|
||||||
|
let (user, database) = compose.postgres_credentials("db");
|
||||||
|
assert_eq!((user.as_str(), database.as_str()), ("db", "db"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ use std::{
|
|||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod command_builder;
|
pub mod command_builder;
|
||||||
|
pub mod compose;
|
||||||
pub mod scripts;
|
pub mod scripts;
|
||||||
|
|
||||||
// NOTE: stolen from https://docs.rs/debug_print/latest/debug_print/
|
// NOTE: stolen from https://docs.rs/debug_print/latest/debug_print/
|
||||||
|
|||||||
33
src/main.rs
33
src/main.rs
@@ -4,30 +4,12 @@ use anyhow::Result;
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
// always load dotenv on start
|
|
||||||
dotenvy::dotenv().ok();
|
|
||||||
|
|
||||||
let args = cli::Ahab::parse();
|
let args = cli::Ahab::parse();
|
||||||
|
|
||||||
match args.command {
|
match args.command {
|
||||||
cli::Commands::Compose { command } => {
|
|
||||||
eprintln!(
|
|
||||||
"DEPRECATION NOTICE: this is deprecated in favor of docker compose COMPOSE_FILE env"
|
|
||||||
);
|
|
||||||
match command {
|
|
||||||
cli::DockerCompose::Bash => scripts::docker_compose::bash(),
|
|
||||||
cli::DockerCompose::Build => scripts::docker_compose::build(),
|
|
||||||
cli::DockerCompose::Down => scripts::docker_compose::down(),
|
|
||||||
cli::DockerCompose::Exec { rest } => scripts::docker_compose::exec(&rest),
|
|
||||||
cli::DockerCompose::Ps => scripts::docker_compose::ps(),
|
|
||||||
cli::DockerCompose::Rebuild => scripts::docker_compose::rebuild(),
|
|
||||||
cli::DockerCompose::Restart => scripts::docker_compose::restart(),
|
|
||||||
cli::DockerCompose::Start => scripts::docker_compose::start(None),
|
|
||||||
cli::DockerCompose::Stop => scripts::docker_compose::stop(),
|
|
||||||
cli::DockerCompose::Up => scripts::docker_compose::up(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cli::Commands::Django { command } => match command {
|
cli::Commands::Django { command } => match command {
|
||||||
|
cli::Django::Bash => scripts::django::bash(),
|
||||||
|
cli::Django::Run { rest } => scripts::django::run(&rest),
|
||||||
cli::Django::MakeCommand { app, name } => scripts::django::make_command(&app, &name),
|
cli::Django::MakeCommand { app, name } => scripts::django::make_command(&app, &name),
|
||||||
cli::Django::Makemigrations => scripts::django::makemigrations(),
|
cli::Django::Makemigrations => scripts::django::makemigrations(),
|
||||||
cli::Django::Manage { rest } => scripts::django::manage(&rest),
|
cli::Django::Manage { rest } => scripts::django::manage(&rest),
|
||||||
@@ -35,13 +17,10 @@ fn main() -> Result<()> {
|
|||||||
cli::Django::Shell => scripts::django::shell(),
|
cli::Django::Shell => scripts::django::shell(),
|
||||||
cli::Django::Test => scripts::django::test(),
|
cli::Django::Test => scripts::django::test(),
|
||||||
},
|
},
|
||||||
cli::Commands::Postgres { command } => {
|
cli::Commands::Postgres { command } => match command {
|
||||||
let db_container = std::env::var("AHAB_POSTGRES_CONTAINER").unwrap_or("db".to_string());
|
cli::Postgres::Import { path } => scripts::postgres::import(&path),
|
||||||
match command {
|
cli::Postgres::Dump { path } => scripts::postgres::dump(&path),
|
||||||
cli::Postgres::Import { path } => scripts::postgres::import(&db_container, &path),
|
},
|
||||||
cli::Postgres::Dump { path } => scripts::postgres::dump(&db_container, &path),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cli::Commands::Link { command } => match command {
|
cli::Commands::Link { command } => match command {
|
||||||
cli::Link::Add { paths, force } => scripts::link::add(&paths, force),
|
cli::Link::Add { paths, force } => scripts::link::add(&paths, force),
|
||||||
cli::Link::Check {
|
cli::Link::Check {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ mod tests {
|
|||||||
for shell in Shell::value_variants() {
|
for shell in Shell::value_variants() {
|
||||||
let out = String::from_utf8(script(*shell)).expect("script is utf8");
|
let out = String::from_utf8(script(*shell)).expect("script is utf8");
|
||||||
|
|
||||||
for subcommand in ["compose", "django", "postgres", "link", "completions"] {
|
for subcommand in ["django", "postgres", "link", "completions"] {
|
||||||
assert!(
|
assert!(
|
||||||
out.contains(subcommand),
|
out.contains(subcommand),
|
||||||
"{shell:?} script never mentions {subcommand}"
|
"{shell:?} script never mentions {subcommand}"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
use std::env;
|
|
||||||
use std::fs::create_dir;
|
use std::fs::create_dir;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -6,6 +5,8 @@ use std::path::{Path, PathBuf};
|
|||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
|
|
||||||
use crate::command_builder::CommandBuilder;
|
use crate::command_builder::CommandBuilder;
|
||||||
|
use crate::compose::Compose;
|
||||||
|
use crate::scripts::docker_compose;
|
||||||
use crate::{create_file, safe_create_file};
|
use crate::{create_file, safe_create_file};
|
||||||
|
|
||||||
const DEBUG_TEMPLATE: &str = r#"from django.core.management.base import BaseCommand
|
const DEBUG_TEMPLATE: &str = r#"from django.core.management.base import BaseCommand
|
||||||
@@ -54,8 +55,17 @@ pub fn make_command(app: &PathBuf, name: &str) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bash() -> Result<()> {
|
||||||
|
run(&["bash".to_string()])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(rest: &[String]) -> Result<()> {
|
||||||
|
let service = Compose::resolve()?.django()?;
|
||||||
|
docker_compose::run(&service, rest)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn manage(rest: &[String]) -> Result<()> {
|
pub fn manage(rest: &[String]) -> Result<()> {
|
||||||
let container = env::var("AHAB_DJANGO_CONTAINER").unwrap_or("django".to_string());
|
let container = Compose::resolve()?.django()?;
|
||||||
let joined = rest.join(" ");
|
let joined = rest.join(" ");
|
||||||
let command = format!("run --rm {container} python manage.py {joined}");
|
let command = format!("run --rm {container} python manage.py {joined}");
|
||||||
CommandBuilder::docker_compose().args(&command).exec()
|
CommandBuilder::docker_compose().args(&command).exec()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ use anyhow::Result;
|
|||||||
|
|
||||||
use crate::command_builder::CommandBuilder;
|
use crate::command_builder::CommandBuilder;
|
||||||
|
|
||||||
// simple commands
|
|
||||||
pub fn build() -> Result<()> {
|
pub fn build() -> Result<()> {
|
||||||
CommandBuilder::docker_compose().args("build").exec()
|
CommandBuilder::docker_compose().args("build").exec()
|
||||||
}
|
}
|
||||||
@@ -11,11 +10,18 @@ pub fn down() -> Result<()> {
|
|||||||
CommandBuilder::docker_compose().args("down").exec()
|
CommandBuilder::docker_compose().args("down").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec(rest: &[String]) -> Result<()> {
|
pub fn run(service: &str, rest: &[String]) -> Result<()> {
|
||||||
let container = std::env::var("AHAB_DJANGO_CONTAINER").unwrap_or("django".to_string());
|
CommandBuilder::docker_compose()
|
||||||
|
.args("run --rm")
|
||||||
|
.args(service)
|
||||||
|
.args(rest)
|
||||||
|
.exec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
|
||||||
CommandBuilder::docker_compose()
|
CommandBuilder::docker_compose()
|
||||||
.args("exec")
|
.args("exec")
|
||||||
.args(&container)
|
.args(service)
|
||||||
.args(rest)
|
.args(rest)
|
||||||
.exec()
|
.exec()
|
||||||
}
|
}
|
||||||
@@ -24,13 +30,6 @@ pub fn ps() -> Result<()> {
|
|||||||
CommandBuilder::docker_compose().args("ps").exec()
|
CommandBuilder::docker_compose().args("ps").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start containers via `docker compose start`. Optionally pass containers to be started.
|
|
||||||
/// ```
|
|
||||||
/// # use ahab::scripts::docker_compose::start;
|
|
||||||
/// start(None);
|
|
||||||
/// ```
|
|
||||||
/// is roughly the same as
|
|
||||||
/// `docker compose --env-file ./.env -f docker/local/docker-compose.yaml up start`
|
|
||||||
pub fn start(containers: Option<&str>) -> Result<()> {
|
pub fn start(containers: Option<&str>) -> Result<()> {
|
||||||
let args = format!("start {}", containers.unwrap_or(""));
|
let args = format!("start {}", containers.unwrap_or(""));
|
||||||
CommandBuilder::docker_compose().args(&args).exec()
|
CommandBuilder::docker_compose().args(&args).exec()
|
||||||
@@ -44,11 +43,6 @@ pub fn up() -> Result<()> {
|
|||||||
CommandBuilder::docker_compose().args("up -d").exec()
|
CommandBuilder::docker_compose().args("up -d").exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortcuts
|
|
||||||
pub fn bash() -> Result<()> {
|
|
||||||
exec(&["bash".to_string()])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild() -> Result<()> {
|
pub fn rebuild() -> Result<()> {
|
||||||
stop()?;
|
stop()?;
|
||||||
build()?;
|
build()?;
|
||||||
|
|||||||
@@ -8,57 +8,71 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::docker_compose;
|
use super::docker_compose;
|
||||||
use crate::{command_builder::CommandBuilder, debug_eprintln};
|
use crate::{command_builder::CommandBuilder, compose::Compose, debug_eprintln};
|
||||||
|
|
||||||
fn get_containers(container: &str) -> Result<[String; 2]> {
|
struct Database {
|
||||||
// get db container
|
service: String,
|
||||||
let db_container = CommandBuilder::docker_compose()
|
container: String,
|
||||||
.args("ps -q")
|
user: String,
|
||||||
.args(container)
|
name: String,
|
||||||
.exec_get_stdout()?
|
|
||||||
.trim()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let no_result = db_container.is_empty();
|
|
||||||
if no_result {
|
|
||||||
return Err(anyhow!("no container"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all containers and filter out db container
|
|
||||||
let app_containers = CommandBuilder::docker_compose()
|
|
||||||
.args("ps -q")
|
|
||||||
.exec_get_stdout()?
|
|
||||||
.split_whitespace()
|
|
||||||
.filter(|x| x != &db_container)
|
|
||||||
.collect::<Vec<&str>>()
|
|
||||||
.join(" ");
|
|
||||||
|
|
||||||
Ok([db_container, app_containers])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn import(container: &str, file: &Path) -> Result<()> {
|
impl Database {
|
||||||
let [db_container, _] = get_containers(container)?;
|
fn resolve() -> Result<Self> {
|
||||||
|
let compose = Compose::resolve()?;
|
||||||
|
let service = compose.postgres()?;
|
||||||
|
let (user, name) = compose.postgres_credentials(&service);
|
||||||
|
|
||||||
|
let container = CommandBuilder::docker_compose()
|
||||||
|
.args("ps -q")
|
||||||
|
.args(&service)
|
||||||
|
.exec_get_stdout()?
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if container.is_empty() {
|
||||||
|
return Err(anyhow!("service {service} has no running container"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
service,
|
||||||
|
container,
|
||||||
|
user,
|
||||||
|
name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn import(file: &Path) -> Result<()> {
|
||||||
|
let db = Database::resolve()?;
|
||||||
let dump_file = file.to_string_lossy();
|
let dump_file = file.to_string_lossy();
|
||||||
|
|
||||||
eprintln!("stopping all containers");
|
eprintln!("stopping all containers");
|
||||||
docker_compose::stop()?;
|
docker_compose::stop()?;
|
||||||
|
|
||||||
eprintln!("starting db container");
|
eprintln!("starting db container");
|
||||||
docker_compose::start(Some(container))?;
|
docker_compose::start(Some(&db.service))?;
|
||||||
|
|
||||||
eprintln!("restoring database");
|
eprintln!("restoring database");
|
||||||
let commands = [
|
let commands = [
|
||||||
format!("cp -L {dump_file} {db_container}:/tmp/dbdump"),
|
format!("cp -L {dump_file} {}:/tmp/dbdump", db.container),
|
||||||
format!("exec {db_container} dropdb -U db db"),
|
format!("exec {} dropdb -U {} {}", db.container, db.user, db.name),
|
||||||
format!("exec {db_container} createdb -U db -E utf8 -T template0 db"),
|
format!(
|
||||||
format!("exec {db_container} pg_restore -U db --dbname=db /tmp/dbdump"),
|
"exec {} createdb -U {} -E utf8 -T template0 {}",
|
||||||
|
db.container, db.user, db.name
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"exec {} pg_restore -U {} --dbname={} /tmp/dbdump",
|
||||||
|
db.container, db.user, db.name
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
for command in commands {
|
for command in commands {
|
||||||
debug_eprintln!("waiting until pg_isready");
|
debug_eprintln!("waiting until pg_isready");
|
||||||
while !CommandBuilder::docker()
|
while !CommandBuilder::docker()
|
||||||
.args(&format!(
|
.args(&format!(
|
||||||
"exec {db_container} pg_isready -h {container} -p 5432 -U db",
|
"exec {} pg_isready -U {} -d {}",
|
||||||
|
db.container, db.user, db.name
|
||||||
))
|
))
|
||||||
.build()?
|
.build()?
|
||||||
.stdout(Stdio::null())
|
.stdout(Stdio::null())
|
||||||
@@ -78,15 +92,18 @@ pub fn import(container: &str, file: &Path) -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dump(container: &str, file: &PathBuf) -> Result<()> {
|
pub fn dump(file: &PathBuf) -> Result<()> {
|
||||||
let [db_container, _] = get_containers(container)?;
|
let db = Database::resolve()?;
|
||||||
|
|
||||||
eprintln!("dumping to local file {}", file.to_string_lossy());
|
eprintln!("dumping to local file {}", file.to_string_lossy());
|
||||||
|
|
||||||
let file = File::create(file)?;
|
let file = File::create(file)?;
|
||||||
let stdout = Stdio::from(file);
|
let stdout = Stdio::from(file);
|
||||||
|
|
||||||
let command = format!("exec {db_container} pg_dump -U db --format=c db");
|
let command = format!(
|
||||||
|
"exec {} pg_dump -U {} --format=c {}",
|
||||||
|
db.container, db.user, db.name
|
||||||
|
);
|
||||||
CommandBuilder::docker()
|
CommandBuilder::docker()
|
||||||
.args(&command)
|
.args(&command)
|
||||||
.exec_redirect_stdout(stdout)?;
|
.exec_redirect_stdout(stdout)?;
|
||||||
|
|||||||
Reference in New Issue
Block a user