11 Commits

Author SHA1 Message Date
7bf76d3ff2 release: v0.3.0
Various qol improvements and custom CommandBuilder mark v0.3.0 release
2023-06-02 17:20:19 +02:00
316b37cd05 feat: custom command builder
Implemented custom command builder with nicer api suited for my needs.
Also returned error if there are no containers in
`scripts::postgres::get_containers`.
2023-06-02 17:18:25 +02:00
e3306c494d feat: prepulate command in make-command
Fixed bug where created command file didn't end with `.py` and
prepopulate created file with minimal example of django management command.
2023-06-02 17:16:15 +02:00
5d45eccfef feat: add django test command
Added django's manage.py test command as a shortcut
2023-06-02 17:15:39 +02:00
251826c048 feat: print $DJANGO_SETTINGS_MODULE when it's needed 2023-06-02 17:07:13 +02:00
63b4b60f9a fix: duplicated help message
Django subcommand had the same help message as compose.
2023-06-02 16:56:26 +02:00
55d4ae3f6f release: version 0.2.0
Command renamings and compltion generation marks v0.2.0 of `ahab`.
2023-05-27 13:32:46 +02:00
de5e6b4d10 feat: rename commands and remove aliases in favour of completion
BREAKING CHANGE: changed commands as follows
D -> Docker
Dc -> Compose
Dj -> Django
Pg -> Postgres
2023-05-27 13:30:09 +02:00
61e7556997 feat: add clap_complete for generating completion files
`ahab generate <shell>` can now be used to generte completion files for
this tool.
2023-05-27 13:28:05 +02:00
a9a5e401dc refactor: create library with simple entrypoint in main
Create a library entrypoint 'src/lib.rs' and refactor binary entrypoint
'src/main.rs' accordingly.
2023-05-27 12:52:10 +02:00
423badfe07 fix: all django management command related subcommands
We just called `manage` inside docker instead of
`exec appserver python manage.py`. Now we make assumption that django
app is running in docker-compose in service named `appserver`.

While fixing this bug we also refactored for `shell`, `makemigrations`
and `migrate` to all call `manage` under the hood to avoid repeating code.
2023-05-24 00:21:04 +02:00
15 changed files with 217 additions and 135 deletions

12
Cargo.lock generated
View File

@@ -4,10 +4,11 @@ version = 3
[[package]]
name = "ahab"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"anyhow",
"clap",
"clap_complete",
"dotenvy",
]
@@ -102,6 +103,15 @@ dependencies = [
"strsim",
]
[[package]]
name = "clap_complete"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a04ddfaacc3bc9e6ea67d024575fafc2a813027cf374b8f24f7bc233c6b6be12"
dependencies = [
"clap",
]
[[package]]
name = "clap_derive"
version = "4.3.0"

View File

@@ -2,7 +2,7 @@
name = "ahab"
description = "docker cli wrapper"
readme = "README.md"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
license = "MIT"
authors = ["Matej Janežič <janezic.mj@gmail.com>"]
@@ -12,5 +12,6 @@ repository = "https://github.com/janezicmatej/ahab.git"
[dependencies]
clap = { version = "4.3.0", features = ["derive"] }
clap_complete = "4.3.0"
anyhow = "1.0.71"
dotenvy = "0.15.7"

View File

@@ -1,5 +1,6 @@
use super::{Django, Docker, DockerCompose, Postgres};
use clap::{Parser, Subcommand};
use clap_complete::Shell;
/// A program for interacting with various dockerized applications.
#[derive(Parser, Debug)]
@@ -11,26 +12,32 @@ pub struct Ahab {
#[derive(Debug, Subcommand)]
pub enum Commands {
/// Generate completion files
Completion {
#[arg(value_enum)]
shell: Shell,
},
/// Docker related subcommands
D {
Docker {
#[command(subcommand)]
command: Docker,
},
/// Docker compose related subcommands
Dc {
Compose {
#[command(subcommand)]
command: DockerCompose,
},
/// Docker compose related subcommands
Dj {
/// Django related subcommands
Django {
#[command(subcommand)]
command: Django,
},
/// Postgres related subcommands
Pg {
Postgres {
#[command(subcommand)]
command: Postgres,
},

View File

@@ -7,7 +7,6 @@ use clap::Parser;
#[derive(Parser, Debug)]
pub enum Django {
/// Prepare empty management command 'command' in app 'app'.
#[clap(alias("mc"))]
MakeCommand {
#[arg(value_enum)]
app: PathBuf,
@@ -16,24 +15,23 @@ pub enum Django {
},
/// Run Django's manage.py makemigrations.
#[clap(alias("mm"))]
Makemigrations,
/// Pass arguments to Django's manage.py.
#[clap(alias("m"))]
Manage {
#[arg(value_enum)]
rest: Vec<String>,
},
/// Run Django's manage.py migrate.
#[clap(alias("mg"))]
Migrate {
#[arg(value_enum)]
rest: Vec<String>,
},
/// Run Django's manage.py shell.
#[clap(alias("s"))]
Shell,
/// Run Django's manage.py test.
Test,
}

View File

@@ -3,6 +3,5 @@ use clap::Parser;
#[derive(Parser, Debug)]
pub enum Docker {
/// Stop all containers via `docker stop $(docker ps -q)`
#[clap(alias("sa"))]
StopAll,
}

View File

@@ -6,30 +6,23 @@ use clap::Subcommand;
#[derive(Subcommand, Debug)]
pub enum DockerCompose {
/// Build containers.
#[clap(alias("b"))]
Build,
/// Down containers.
#[clap(alias("d"))]
Down,
/// Stop, build and start containers.
#[clap(alias("rb"))]
Rebuild,
/// Stop and start containers.
#[clap(alias("rs"))]
Restart,
/// Start containers.
#[clap(alias("st"))]
Start,
/// Stop containers.
#[clap(alias("s"))]
Stop,
/// Up containers.
#[clap(alias("u"))]
Up,
}

View File

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

82
src/command_builder.rs Normal file
View File

@@ -0,0 +1,82 @@
use anyhow::{anyhow, Context, Result};
use std::{
collections::VecDeque,
ffi::OsStr,
fmt::Display,
fs::{File, OpenOptions},
path::PathBuf,
process::{Command, Stdio},
};
use crate::debug_println;
pub struct Args(Vec<String>);
impl From<&str> for Args {
fn from(value: &str) -> Self {
Self(Vec::from_iter(value.split_whitespace().map(String::from)))
}
}
impl From<&String> for Args {
fn from(value: &String) -> Self {
Self(Vec::from_iter(value.split_whitespace().map(String::from)))
}
}
#[derive(Default)]
pub struct CommandBuilder {
args: Vec<String>,
}
impl Display for CommandBuilder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.args.join(" "))
}
}
impl CommandBuilder {
pub fn new(args: &str) -> Self {
Self::default().args("docker")
}
pub fn docker() -> Self {
Self::default().args("docker")
}
pub fn docker_compose() -> Self {
Self::default().args("docker compose -f docker/local/docker-compose.yaml")
}
pub fn args<T>(mut self, args: T) -> Self
where
Args: From<T>,
{
self.args.extend(Args::from(args).0);
self
}
fn build(self) -> Result<Command> {
debug_println!("\nran {self}\n");
let (first, rest) = self.args.split_first().context("empty args")?;
let mut command = Command::new(first);
command.args(rest);
Ok(command)
}
pub fn exec_get_stdout(mut self) -> Result<String> {
Ok(String::from_utf8(self.build()?.output()?.stdout)?)
}
pub fn exec(mut self) -> Result<()> {
self.build()?.spawn()?.wait()?;
Ok(())
}
pub fn exec_redirect_stdout(mut self, stdio: Stdio) -> Result<()> {
self.build()?.stdout(stdio).spawn()?.wait()?;
Ok(())
}
}

24
src/lib.rs Normal file
View File

@@ -0,0 +1,24 @@
#![allow(unused)]
use std::{
fs::{File, OpenOptions},
path::PathBuf,
};
pub mod cli;
pub mod command_builder;
pub mod scripts;
// NOTE: stolen from https://docs.rs/debug_print/latest/debug_print/
#[macro_export]
macro_rules! debug_println {
($($arg:tt)*) => (if ::std::cfg!(debug_assertions) { ::std::println!($($arg)*); })
}
fn safe_create_file(path: PathBuf) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create_new(true).open(path)
}
fn create_file(path: PathBuf) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create(true).open(path)
}

View File

@@ -1,11 +1,7 @@
#![allow(unused)]
mod cli;
mod scripts;
use ahab::{cli, scripts};
use anyhow::Result;
use clap::Parser;
use std::env;
use clap::{CommandFactory, Parser};
fn main() -> Result<()> {
// always load dotenv on start
@@ -17,10 +13,19 @@ fn main() -> Result<()> {
let args = cli::Ahab::parse();
match args.command {
cli::Commands::D { command } => match command {
cli::Commands::Completion { shell } => {
clap_complete::generate(
shell,
&mut cli::Ahab::command(),
"ahab",
&mut std::io::stdout(),
);
Ok(())
}
cli::Commands::Docker { command } => match command {
cli::Docker::StopAll => scripts::docker::stop_all(),
},
cli::Commands::Dc { command } => match command {
cli::Commands::Compose { command } => match command {
cli::DockerCompose::Build => scripts::docker_compose::build(),
cli::DockerCompose::Down => scripts::docker_compose::down(),
cli::DockerCompose::Rebuild => scripts::docker_compose::rebuild(),
@@ -29,14 +34,15 @@ fn main() -> Result<()> {
cli::DockerCompose::Stop => scripts::docker_compose::stop(),
cli::DockerCompose::Up => scripts::docker_compose::up(),
},
cli::Commands::Dj { command } => match command {
cli::Commands::Django { command } => match command {
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),
cli::Django::Migrate { rest } => scripts::django::migrate(&rest),
cli::Django::Shell => scripts::django::shell(),
cli::Django::Test => scripts::django::test(),
},
cli::Commands::Pg { command } => match command {
cli::Commands::Postgres { command } => match command {
cli::Postgres::Import { path } => scripts::postgres::import(&path),
cli::Postgres::Dump { path } => scripts::postgres::dump(&path),
},

View File

@@ -1,15 +1,28 @@
use crate::scripts::{create_file, safe_create_file};
use super::DockerCommand;
use anyhow::{anyhow, Result};
use std::env;
use std::fs::create_dir;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Result};
use crate::command_builder::CommandBuilder;
use crate::{create_file, safe_create_file};
fn get_django_settings_module() -> Result<String> {
Ok(env::var("DJANGO_SETTINGS_MODULE")?)
let dsm = env::var("DJANGO_SETTINGS_MODULE")?;
println!("USING: {dsm}");
Ok(dsm)
}
const DEBUG_TEMPLATE: &str = r#"from django.core.management.base import BaseCommand
class Command(BaseCommand):
def handle(self, *args, **options):
pass
"#;
pub fn make_command(app: &PathBuf, name: &str) -> Result<()> {
let app_name = app.to_string_lossy();
let app_dir = Path::new(&app);
@@ -41,33 +54,36 @@ pub fn make_command(app: &PathBuf, name: &str) -> Result<()> {
println!("created module {app_name}.management.commands")
};
safe_create_file(commands_dir.join(name))?;
let mut file = safe_create_file(commands_dir.join(format!("{name}.py")))?;
file.write_all(DEBUG_TEMPLATE.as_bytes())?;
println!("created command {app_name}.management.commands.{name}");
Ok(())
}
pub fn makemigrations() -> Result<()> {
let dsm = get_django_settings_module()?;
let command = format!("manage makemigrations --settings={dsm}");
DockerCommand::docker_compose().args(&command).spawn_wait()
}
pub fn manage(rest: &[String]) -> Result<()> {
let dsm = get_django_settings_module()?;
let joined = rest.join(" ");
let command = format!("manage {joined} --settings={dsm}");
DockerCommand::docker_compose().args(&command).spawn_wait()
let command = format!("exec appserver python manage.py {joined} --settings={dsm}");
CommandBuilder::docker_compose().args(&command).exec()
}
// shortcuts
pub fn makemigrations() -> Result<()> {
manage(&["makemigrations".to_string()])
}
pub fn migrate(rest: &[String]) -> Result<()> {
let dsm = get_django_settings_module()?;
let joined = rest.join(" ");
let command = format!("manage migrate {joined} --settings={dsm}");
DockerCommand::docker_compose().args(&command).spawn_wait()
let mut full_rest = vec!["migrate".to_string()];
full_rest.extend_from_slice(rest);
manage(&full_rest)
}
pub fn shell() -> Result<()> {
let dsm = get_django_settings_module()?;
let command = format!("manage shell --settings={dsm}");
DockerCommand::docker_compose().args(&command).spawn_wait()
manage(&["shell".to_string()])
}
pub fn test() -> Result<()> {
manage(&["test".to_string()])
}

View File

@@ -1,14 +1,15 @@
use super::DockerCommand;
use anyhow::Result;
use crate::command_builder::CommandBuilder;
pub fn stop_all() -> Result<()> {
let running_containers = DockerCommand::docker().args("ps -q").stdout()?;
let running_containers = CommandBuilder::docker().args("ps -q").exec_get_stdout()?;
if running_containers.is_empty() {
return Ok(());
}
DockerCommand::docker()
CommandBuilder::docker()
.args(&format!("stop {running_containers}"))
.spawn_wait()
.exec()
}

View File

@@ -1,13 +1,14 @@
use super::DockerCommand;
use anyhow::Result;
use crate::command_builder::CommandBuilder;
// simple commands
pub fn build() -> Result<()> {
DockerCommand::docker_compose().args("build").spawn_wait()
CommandBuilder::docker_compose().args("build").exec()
}
pub fn down() -> Result<()> {
DockerCommand::docker_compose().args("down").spawn_wait()
CommandBuilder::docker_compose().args("down").exec()
}
/// Start containers via `docker compose start`. Optionally pass containers to be started.
@@ -18,25 +19,25 @@ pub fn down() -> Result<()> {
/// `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(""));
DockerCommand::docker_compose().args("start").spawn_wait()
CommandBuilder::docker_compose().args("start").exec()
}
pub fn stop() -> Result<()> {
DockerCommand::docker_compose().args("stop").spawn_wait()
CommandBuilder::docker_compose().args("stop").exec()
}
pub fn up() -> Result<()> {
DockerCommand::docker_compose().args("up -d").spawn_wait()
CommandBuilder::docker_compose().args("up -d").exec()
}
// shortcuts
pub fn rebuild() -> Result<()> {
stop()?;
build()?;
start(None)
up()
}
pub fn restart() -> Result<()> {
stop()?;
start(None)
up()
}

View File

@@ -2,63 +2,3 @@ pub mod django;
pub mod docker;
pub mod docker_compose;
pub mod postgres;
use std::{
ffi::OsStr,
fs::{File, OpenOptions},
path::PathBuf,
process::{Command, Stdio},
};
use anyhow::{Context, Result};
struct DockerCommand {
command: Command,
}
impl DockerCommand {
fn new<T>(program: T) -> Self
where
T: AsRef<OsStr>,
{
DockerCommand {
command: Command::new(program),
}
}
fn docker() -> Self {
Self::new("docker")
}
fn docker_compose() -> Self {
Self::new("docker").args("compose -f docker/local/docker-compose.yaml")
}
fn args(mut self, args: &str) -> Self {
self.command.args(args.split_whitespace());
self
}
fn stdout(mut self) -> Result<String> {
Ok(String::from_utf8(self.command.output()?.stdout)?)
}
fn spawn_wait(mut self) -> Result<()> {
self.command.spawn()?.wait()?;
Ok(())
}
fn write_stdout(mut self, stdio: Stdio) -> Result<()> {
self.command.stdout(stdio);
self.spawn_wait()?;
Ok(())
}
}
fn safe_create_file(path: PathBuf) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create_new(true).open(path)
}
fn create_file(path: PathBuf) -> Result<File, std::io::Error> {
OpenOptions::new().write(true).create(true).open(path)
}

View File

@@ -1,25 +1,31 @@
use super::{docker_compose, DockerCommand};
use anyhow::{Context, Result};
use anyhow::{anyhow, Context, Result};
use std::{
fs::File,
path::{Path, PathBuf},
process::Stdio,
};
use super::docker_compose;
use crate::command_builder::CommandBuilder;
fn get_containers() -> Result<[String; 2]> {
// get db container
// FIX: we assume we are running db in service named "postgresbd"
let db_container = DockerCommand::docker_compose()
let db_container = CommandBuilder::docker_compose()
.args("ps -q postgresdb")
.stdout()?
.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 = DockerCommand::docker_compose()
let app_containers = CommandBuilder::docker_compose()
.args("ps -q")
.stdout()?
.exec_get_stdout()?
.split_whitespace()
.filter(|x| x != &db_container)
.collect::<Vec<&str>>()
@@ -46,7 +52,7 @@ pub fn import(file: &Path) -> Result<()> {
format!("exec {db_container} pg_restore -U db --dbname=db /tmp/dbdump"),
];
for command in commands {
DockerCommand::docker().args(&command).spawn_wait()?;
CommandBuilder::docker().args(&command).exec()?;
}
println!("restarting containers");
@@ -65,9 +71,9 @@ pub fn dump(file: &PathBuf) -> Result<()> {
let stdout = Stdio::from(file);
let command = format!("exec {db_container} pg_dump -U db --format=c db");
DockerCommand::docker()
CommandBuilder::docker()
.args(&command)
.write_stdout(stdout)?;
.exec_redirect_stdout(stdout)?;
Ok(())
}