implemented proper error handling, added README
This commit is contained in:
@@ -1,2 +1,4 @@
|
||||
/target
|
||||
/conf
|
||||
CLAUDE.md
|
||||
.claude
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# dev_tools
|
||||
|
||||
CLI tool to start and stop a local RCC test environment. It manages a Docker Compose service and toggles environment variable files between local and CI database configurations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Rust toolchain (stable)
|
||||
- Docker with Compose plugin
|
||||
|
||||
## Configuration
|
||||
|
||||
The tool reads from a dotenv-style file at `~/.config/rcc/dev`. The following variables are required:
|
||||
|
||||
| Variable | Description |
|
||||
|---|---|
|
||||
| `DOCKER_SERVICE` | Name of the Docker container to check if it is running |
|
||||
| `DOCKER_DIR` | Path to the Docker Compose directory, relative to `$HOME` |
|
||||
| `<PROJECT>_DIR` | Absolute path to the project directory (one per project, e.g. `FOO_DIR`) |
|
||||
|
||||
Example `~/.config/rcc/dev`:
|
||||
```
|
||||
DOCKER_SERVICE=my-container
|
||||
DOCKER_DIR=repos/docker/local
|
||||
FOO_DIR=/home/user/repos/foo
|
||||
```
|
||||
|
||||
## Build & Install
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
# or install directly into ~/.cargo/bin
|
||||
cargo install --path .
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
dev_tools <project> <up|down> [--legacy]
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
|---|---|
|
||||
| `project` | Project key (matches `<PROJECT>_DIR` in the config, case-insensitive) |
|
||||
| `up` / `down` | Start or stop the environment |
|
||||
| `-l`, `--legacy` | Enable legacy mode: also toggles `conf/global/connections.env` and runs `git update-index` |
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
# Start the environment for project "foo"
|
||||
dev_tools foo up
|
||||
|
||||
# Stop it
|
||||
dev_tools foo down
|
||||
|
||||
# Start with legacy env handling
|
||||
dev_tools foo up --legacy
|
||||
```
|
||||
|
||||
## What it does
|
||||
|
||||
1. Starts or stops the Docker Compose service defined by `DOCKER_SERVICE` / `DOCKER_DIR`.
|
||||
2. Toggles database connection entries in `<PROJECT_DIR>/conf/local/.env` between `staging` and CI values.
|
||||
3. With `--legacy`: additionally modifies `conf/global/connections.env` and marks it with `git update-index --assume-unchanged` to avoid accidental commits.
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
+32
-2
@@ -1,3 +1,4 @@
|
||||
use anyhow::{bail, Result};
|
||||
use clap::Parser;
|
||||
|
||||
#[derive(Parser, Default, Debug)]
|
||||
@@ -22,11 +23,40 @@ pub struct Arguments {
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
if self.action == "up" || self.action == "down" {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
Err("Action values are up or down.".into())
|
||||
bail!("Action values are up or down.")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn args_with_action(action: &str) -> Arguments {
|
||||
Arguments {
|
||||
project: "test".to_string(),
|
||||
action: action.to_string(),
|
||||
legacy: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_up() {
|
||||
assert!(args_with_action("up").validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_down() {
|
||||
assert!(args_with_action("down").validate().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_invalid() {
|
||||
let err = args_with_action("start").validate().unwrap_err();
|
||||
assert!(err.to_string().contains("up or down"));
|
||||
}
|
||||
}
|
||||
|
||||
+77
-24
@@ -3,14 +3,14 @@ use std::{
|
||||
io::{BufRead, BufReader},
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
use crate::env::config::DevToolsConf;
|
||||
pub const GLOBAL_CONNECTION_PATH: &str = "conf/global/connections.env";
|
||||
const LOCAL_CONNECTION_PATH: &str = "conf/local/.env";
|
||||
|
||||
/// writes global connections
|
||||
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<()> {
|
||||
let global_connections_env = format!(
|
||||
"{}{}",
|
||||
config.project_dir_as_string(),
|
||||
@@ -20,37 +20,35 @@ pub fn set_local_db(start: bool, config: &DevToolsConf) -> Result<(), Box<dyn st
|
||||
.with_context(|| format!("could not read file `{}`", global_connections_env))?;
|
||||
let reader: BufReader<File> = BufReader::new(file);
|
||||
|
||||
let modified_lines: Vec<String> = reader
|
||||
.lines()
|
||||
.map(|line| {
|
||||
let line_content: String = line.unwrap();
|
||||
|
||||
if start && connections_env_comment(&line_content) {
|
||||
format!("# {}", line_content)
|
||||
} else if !start && should_commented_out(&line_content) {
|
||||
line_content[2..].to_string()
|
||||
} else {
|
||||
line_content
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut modified_lines: Vec<String> = Vec::new();
|
||||
for line in reader.lines() {
|
||||
let line_content = line?;
|
||||
let modified = if start && connections_env_comment(&line_content) {
|
||||
format!("# {}", line_content)
|
||||
} else if !start && should_commented_out(&line_content) {
|
||||
line_content[2..].to_string()
|
||||
} else {
|
||||
line_content
|
||||
};
|
||||
modified_lines.push(modified);
|
||||
}
|
||||
|
||||
fs::write(global_connections_env, modified_lines.join("\n"))
|
||||
.expect("Cannot write global connections env");
|
||||
.context("Cannot write global connections env")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn set_dot_env(start: bool, config: &DevToolsConf) -> Result<()> {
|
||||
toggle_after_line("#docker", 4, start, config)?;
|
||||
toggle_after_line("#cidb", 2, !start, config)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn connections_env_comment(line: &str) -> bool {
|
||||
pub(crate) fn connections_env_comment(line: &str) -> bool {
|
||||
line.starts_with("ci.db.master.ip=") || line.starts_with("ci.db.master.port=")
|
||||
}
|
||||
|
||||
fn should_commented_out(line: &str) -> bool {
|
||||
pub(crate) fn should_commented_out(line: &str) -> bool {
|
||||
line.starts_with("# ci.db.master.ip=") || line.starts_with("# ci.db.master.port=")
|
||||
}
|
||||
|
||||
@@ -59,7 +57,7 @@ fn toggle_after_line(
|
||||
num_lines: usize,
|
||||
activate: bool,
|
||||
config: &DevToolsConf,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
) -> Result<()> {
|
||||
let local_connection_env = format!(
|
||||
"{}{}",
|
||||
config.project_dir_as_string(),
|
||||
@@ -100,13 +98,13 @@ fn toggle_after_line(
|
||||
}
|
||||
}
|
||||
|
||||
// Write the modified content back to the output file
|
||||
fs::write(local_connection_env, modified_lines.join("\n")).expect("");
|
||||
fs::write(local_connection_env, modified_lines.join("\n"))
|
||||
.context("Cannot write local connection env")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn toggle(line: &str, activate: bool) -> String {
|
||||
pub(crate) fn toggle(line: &str, activate: bool) -> String {
|
||||
if activate {
|
||||
return line.replacen('#', "", 1);
|
||||
}
|
||||
@@ -117,3 +115,58 @@ fn toggle(line: &str, activate: bool) -> String {
|
||||
|
||||
line.to_string()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn toggle_activate_removes_leading_hash() {
|
||||
assert_eq!(toggle("#value=1", true), "value=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_activate_no_hash_unchanged() {
|
||||
assert_eq!(toggle("value=1", true), "value=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_deactivate_adds_hash() {
|
||||
assert_eq!(toggle("value=1", false), "#value=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_deactivate_already_hashed_is_noop() {
|
||||
assert_eq!(toggle("#value=1", false), "#value=1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connections_env_comment_matches_ip() {
|
||||
assert!(connections_env_comment("ci.db.master.ip=127.0.0.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connections_env_comment_matches_port() {
|
||||
assert!(connections_env_comment("ci.db.master.port=5432"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn connections_env_comment_ignores_other() {
|
||||
assert!(!connections_env_comment("ci.db.master.user=admin"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_commented_out_matches_ip() {
|
||||
assert!(should_commented_out("# ci.db.master.ip=127.0.0.1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_commented_out_matches_port() {
|
||||
assert!(should_commented_out("# ci.db.master.port=5432"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn should_commented_out_ignores_uncommented() {
|
||||
assert!(!should_commented_out("ci.db.master.ip=127.0.0.1"));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-14
@@ -3,18 +3,15 @@ use std::{
|
||||
process::{Command, ExitStatus},
|
||||
};
|
||||
|
||||
use anyhow::{bail, Context, Result};
|
||||
use directories::BaseDirs;
|
||||
|
||||
fn is_container_running(container_name: &str) -> Result<bool, Box<dyn std::error::Error>> {
|
||||
// Run the `docker ps` command and capture the output
|
||||
fn is_container_running(container_name: &str) -> Result<bool> {
|
||||
let output = Command::new("docker")
|
||||
.args(["ps", "--format", "{{.Names}}"])
|
||||
.output()?;
|
||||
|
||||
// Convert the output to a string
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
|
||||
// Check if the container name is in the list of running containers
|
||||
let running: bool = output_str.lines().any(|line| line.trim() == container_name);
|
||||
|
||||
Ok(running)
|
||||
@@ -24,11 +21,11 @@ pub fn start_docker_compose(
|
||||
service_name: &str,
|
||||
container_path: &str,
|
||||
start: bool,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let base_dir: BaseDirs = BaseDirs::new().unwrap();
|
||||
) -> Result<()> {
|
||||
let base_dir: BaseDirs = BaseDirs::new().context("Could not determine home directory")?;
|
||||
let absolute = base_dir.home_dir().join(container_path);
|
||||
let compose_path = Path::new(&absolute);
|
||||
println!("{}", compose_path.to_str().unwrap());
|
||||
println!("{}", compose_path.to_str().context("Invalid path")?);
|
||||
|
||||
let running: bool = is_container_running(service_name)?;
|
||||
|
||||
@@ -49,9 +46,7 @@ pub fn start_docker_compose(
|
||||
service_name
|
||||
);
|
||||
} else {
|
||||
return Err(
|
||||
format!("Failed to start Docker Compose service '{}'.", service_name).into(),
|
||||
);
|
||||
bail!("Failed to start Docker Compose service '{}'.", service_name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,9 +62,7 @@ pub fn start_docker_compose(
|
||||
service_name
|
||||
);
|
||||
} else {
|
||||
return Err(
|
||||
format!("Failed to stop Docker Compose service '{}'.", service_name).into(),
|
||||
);
|
||||
bail!("Failed to stop Docker Compose service '{}'.", service_name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+10
-26
@@ -1,6 +1,7 @@
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use directories::BaseDirs;
|
||||
|
||||
use crate::arguments::Arguments;
|
||||
@@ -16,45 +17,28 @@ pub struct DevToolsConf {
|
||||
|
||||
impl DevToolsConf {
|
||||
pub fn project_dir_as_string(&self) -> String {
|
||||
self.project_directory
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
self.project_directory.to_string_lossy().into_owned()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_config(arguments: &Arguments) -> Result<DevToolsConf, Box<dyn std::error::Error>> {
|
||||
let base_dir: BaseDirs = match BaseDirs::new() {
|
||||
Some(dirs) => dirs,
|
||||
None => {
|
||||
return Err("No config folder found.".into());
|
||||
}
|
||||
};
|
||||
pub fn load_config(arguments: &Arguments) -> Result<DevToolsConf> {
|
||||
let base_dir: BaseDirs = BaseDirs::new().context("No config folder found.")?;
|
||||
let config_file_path: PathBuf = base_dir.config_dir().join("rcc/").join("dev");
|
||||
|
||||
match dotenv::from_path(config_file_path.as_path()) {
|
||||
Ok(env) => env,
|
||||
Err(_) => {
|
||||
return Err(format!(
|
||||
"Could not load env file {}",
|
||||
config_file_path.as_path().to_str().unwrap()
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
dotenv::from_path(config_file_path.as_path())
|
||||
.with_context(|| format!("Could not load env file {}", config_file_path.display()))?;
|
||||
|
||||
let container_service: String = env::var(DOCKER_SERVICE)
|
||||
.map_err(|e| format!("{} missing in env file. {}", DOCKER_SERVICE, e))?;
|
||||
let container_dir: String =
|
||||
env::var(DOCKER_DIR).map_err(|e| format!("{} missing in env file. {}", DOCKER_DIR, e))?;
|
||||
.with_context(|| format!("{} missing in env file.", DOCKER_SERVICE))?;
|
||||
let container_dir: String = env::var(DOCKER_DIR)
|
||||
.with_context(|| format!("{} missing in env file.", DOCKER_DIR))?;
|
||||
|
||||
let mut project_directory: PathBuf = env::current_dir()?;
|
||||
|
||||
if !arguments.project.is_empty() {
|
||||
let config_to_read = format!("{}_DIR", arguments.project.to_uppercase());
|
||||
let dir: String = env::var(&config_to_read)
|
||||
.map_err(|e| format!("{} missing in env file. {}", &config_to_read, e))?;
|
||||
.with_context(|| format!("{} missing in env file.", &config_to_read))?;
|
||||
|
||||
project_directory = PathBuf::from(dir);
|
||||
}
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
use std::process::Command;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use crate::{config::parse::GLOBAL_CONNECTION_PATH, env::config::DevToolsConf};
|
||||
|
||||
pub fn toggle_index(start: bool, config: &DevToolsConf) -> Result<(), Box<dyn std::error::Error>> {
|
||||
pub fn toggle_index(start: bool, config: &DevToolsConf) -> Result<()> {
|
||||
let change: &str = match start {
|
||||
true => "--assume-unchanged",
|
||||
false => "--no-assume-unchanged",
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
use anyhow::Result;
|
||||
use container::docker::start_docker_compose;
|
||||
use env::config::{load_config, DevToolsConf};
|
||||
|
||||
@@ -14,7 +15,7 @@ mod container;
|
||||
mod env;
|
||||
mod git;
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
fn main() -> Result<()> {
|
||||
let args: Arguments = Arguments::parse();
|
||||
args.validate()?;
|
||||
let config: DevToolsConf = load_config(&args)?;
|
||||
|
||||
Reference in New Issue
Block a user