merge: compose subcommand deprecation

This commit is contained in:
2026-09-07 15:14:41 +02:00
16 changed files with 513 additions and 171 deletions

68
Cargo.lock generated
View File

@@ -9,7 +9,7 @@ dependencies = [
"anyhow",
"clap",
"clap_complete",
"dotenvy",
"serde_json",
]
[[package]]
@@ -123,12 +123,6 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
[[package]]
name = "dotenvy"
version = "0.15.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
[[package]]
name = "heck"
version = "0.5.0"
@@ -141,6 +135,18 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "once_cell_polyfill"
version = "1.70.2"
@@ -165,6 +171,48 @@ dependencies = [
"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]]
name = "strsim"
version = "0.11.1"
@@ -208,3 +256,9 @@ checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "zmij"
version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"

View File

@@ -16,7 +16,7 @@ build = "build.rs"
clap = { version = "4.6.6", features = ["derive"] }
clap_complete = "4.6.9"
anyhow = "1.0.104"
dotenvy = "0.15.7"
serde_json = "1.0.145"
[build-dependencies]
clap = { version = "4.6.6", features = ["derive"] }

View File

@@ -1,5 +1,8 @@
# ahab
A wrapper around `docker compose` for our dockerized django projects, so the
same commands work in every repository.
## installing
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 .
```
To print underlying commands build with `debug` flag.
To print the underlying docker commands as they run, build with debug
assertions:
```bash
cargo install --path . --debug
```
## 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
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`

View File

@@ -1,4 +1,4 @@
use super::{Django, DockerCompose, Link, Postgres};
use super::{Django, Link, Postgres};
use clap::{Parser, Subcommand};
use clap_complete::Shell;
@@ -12,12 +12,6 @@ pub struct Ahab {
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Docker compose related subcommands
Compose {
#[command(subcommand)]
command: DockerCompose,
},
/// Django related subcommands
Django {
#[command(subcommand)]

View File

@@ -1,31 +1,35 @@
use std::path::PathBuf;
use clap::Parser;
use clap::Subcommand;
// TODO: (matej) dsu template command
#[derive(Parser, Debug)]
#[derive(Subcommand, Debug)]
pub enum Django {
/// Start a bash session in a fresh django container
Bash,
/// Prepare empty management command 'command' in app 'app'.
MakeCommand {
#[arg(value_enum)]
app: PathBuf,
#[arg(value_enum)]
name: String,
},
MakeCommand { app: PathBuf, name: String },
/// Run Django's manage.py makemigrations.
Makemigrations,
/// Pass arguments to Django's manage.py.
Manage {
#[arg(value_enum)]
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
rest: Vec<String>,
},
/// Run Django's manage.py 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>,
},

View File

@@ -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,
}

View File

@@ -1,11 +1,9 @@
mod ahab;
mod django;
mod docker_compose;
mod link;
mod postgres;
pub use ahab::{Ahab, Commands};
pub use django::Django;
pub use docker_compose::DockerCompose;
pub use link::Link;
pub use postgres::Postgres;

View File

@@ -5,14 +5,8 @@ use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum Postgres {
/// Import dump via pg_restore
Import {
#[arg(value_enum)]
path: PathBuf,
},
Import { path: PathBuf },
/// Dump via pg_dump with format=c
Dump {
#[arg(value_enum)]
path: PathBuf,
},
Dump { path: PathBuf },
}

View File

@@ -1,6 +1,5 @@
use anyhow::{Context, Result};
use std::{
env,
fmt::Display,
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)]
pub struct CommandBuilder {
args: Vec<String>,
@@ -49,9 +43,7 @@ impl CommandBuilder {
}
pub fn docker_compose() -> Self {
let cf =
get_compose_file().unwrap_or_else(|_| "docker/local/docker-compose.yaml".to_string());
Self::default().args("docker compose -f").args(&cf)
Self::default().args("docker compose")
}
pub fn args<T>(mut self, args: T) -> Self

261
src/compose.rs Normal file
View 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"));
}
}

View File

@@ -5,6 +5,7 @@ use std::{
pub mod cli;
pub mod command_builder;
pub mod compose;
pub mod scripts;
// NOTE: stolen from https://docs.rs/debug_print/latest/debug_print/

View File

@@ -4,30 +4,12 @@ use anyhow::Result;
use clap::Parser;
fn main() -> Result<()> {
// always load dotenv on start
dotenvy::dotenv().ok();
let args = cli::Ahab::parse();
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::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::Makemigrations => scripts::django::makemigrations(),
cli::Django::Manage { rest } => scripts::django::manage(&rest),
@@ -35,13 +17,10 @@ fn main() -> Result<()> {
cli::Django::Shell => scripts::django::shell(),
cli::Django::Test => scripts::django::test(),
},
cli::Commands::Postgres { command } => {
let db_container = std::env::var("AHAB_POSTGRES_CONTAINER").unwrap_or("db".to_string());
match command {
cli::Postgres::Import { path } => scripts::postgres::import(&db_container, &path),
cli::Postgres::Dump { path } => scripts::postgres::dump(&db_container, &path),
}
}
cli::Commands::Postgres { command } => match command {
cli::Postgres::Import { path } => scripts::postgres::import(&path),
cli::Postgres::Dump { path } => scripts::postgres::dump(&path),
},
cli::Commands::Link { command } => match command {
cli::Link::Add { paths, force } => scripts::link::add(&paths, force),
cli::Link::Check {

View File

@@ -35,7 +35,7 @@ mod tests {
for shell in Shell::value_variants() {
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!(
out.contains(subcommand),
"{shell:?} script never mentions {subcommand}"

View File

@@ -1,4 +1,3 @@
use std::env;
use std::fs::create_dir;
use std::io::Write;
use std::path::{Path, PathBuf};
@@ -6,6 +5,8 @@ use std::path::{Path, PathBuf};
use anyhow::{Result, anyhow};
use crate::command_builder::CommandBuilder;
use crate::compose::Compose;
use crate::scripts::docker_compose;
use crate::{create_file, safe_create_file};
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(())
}
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<()> {
let container = env::var("AHAB_DJANGO_CONTAINER").unwrap_or("django".to_string());
let container = Compose::resolve()?.django()?;
let joined = rest.join(" ");
let command = format!("run --rm {container} python manage.py {joined}");
CommandBuilder::docker_compose().args(&command).exec()

View File

@@ -2,7 +2,6 @@ use anyhow::Result;
use crate::command_builder::CommandBuilder;
// simple commands
pub fn build() -> Result<()> {
CommandBuilder::docker_compose().args("build").exec()
}
@@ -11,11 +10,18 @@ pub fn down() -> Result<()> {
CommandBuilder::docker_compose().args("down").exec()
}
pub fn exec(rest: &[String]) -> Result<()> {
let container = std::env::var("AHAB_DJANGO_CONTAINER").unwrap_or("django".to_string());
pub fn run(service: &str, rest: &[String]) -> Result<()> {
CommandBuilder::docker_compose()
.args("run --rm")
.args(service)
.args(rest)
.exec()
}
pub fn exec(service: &str, rest: &[String]) -> Result<()> {
CommandBuilder::docker_compose()
.args("exec")
.args(&container)
.args(service)
.args(rest)
.exec()
}
@@ -24,13 +30,6 @@ pub fn ps() -> Result<()> {
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<()> {
let args = format!("start {}", containers.unwrap_or(""));
CommandBuilder::docker_compose().args(&args).exec()
@@ -44,11 +43,6 @@ pub fn up() -> Result<()> {
CommandBuilder::docker_compose().args("up -d").exec()
}
// shortcuts
pub fn bash() -> Result<()> {
exec(&["bash".to_string()])
}
pub fn rebuild() -> Result<()> {
stop()?;
build()?;

View File

@@ -8,57 +8,71 @@ use std::{
};
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]> {
// get db container
let db_container = CommandBuilder::docker_compose()
struct Database {
service: String,
container: String,
user: String,
name: String,
}
impl Database {
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(container)
.args(&service)
.exec_get_stdout()?
.trim()
.to_string();
let no_result = db_container.is_empty();
if no_result {
return Err(anyhow!("no container"));
if container.is_empty() {
return Err(anyhow!("service {service} has no running 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])
Ok(Self {
service,
container,
user,
name,
})
}
}
pub fn import(container: &str, file: &Path) -> Result<()> {
let [db_container, _] = get_containers(container)?;
pub fn import(file: &Path) -> Result<()> {
let db = Database::resolve()?;
let dump_file = file.to_string_lossy();
eprintln!("stopping all containers");
docker_compose::stop()?;
eprintln!("starting db container");
docker_compose::start(Some(container))?;
docker_compose::start(Some(&db.service))?;
eprintln!("restoring database");
let commands = [
format!("cp -L {dump_file} {db_container}:/tmp/dbdump"),
format!("exec {db_container} dropdb -U db db"),
format!("exec {db_container} createdb -U db -E utf8 -T template0 db"),
format!("exec {db_container} pg_restore -U db --dbname=db /tmp/dbdump"),
format!("cp -L {dump_file} {}:/tmp/dbdump", db.container),
format!("exec {} dropdb -U {} {}", db.container, db.user, db.name),
format!(
"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 {
debug_eprintln!("waiting until pg_isready");
while !CommandBuilder::docker()
.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()?
.stdout(Stdio::null())
@@ -78,15 +92,18 @@ pub fn import(container: &str, file: &Path) -> Result<()> {
Ok(())
}
pub fn dump(container: &str, file: &PathBuf) -> Result<()> {
let [db_container, _] = get_containers(container)?;
pub fn dump(file: &PathBuf) -> Result<()> {
let db = Database::resolve()?;
eprintln!("dumping to local file {}", file.to_string_lossy());
let file = File::create(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()
.args(&command)
.exec_redirect_stdout(stdout)?;