Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# Stackdog configuration.
# Copy to .env in the working directory, or to ~/.config/stackdog/.env
# (`stackdog init` writes this file there). Real environment variables and
# the nearer file win; everything here is optional and defaults are safe.

#APP_HOST=localhost
# in container
APP_HOST=0.0.0.0
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- Configuration is also read from `~/.config/stackdog/.env`
(`$XDG_CONFIG_HOME/stackdog/.env` when set), behind the working-directory
`.env`: the nearest file wins and real environment variables win over both.
A missing file is no longer a warning — every setting has a default.
- `stackdog init` writes the starter config to that directory and never
overwrites an existing file (it holds credentials).
- Discord webhook notifications (`STACKDOG_DISCORD_WEBHOOK_URL`), routed exactly
like Slack: Critical, High and Medium alerts arrive as plain messages under
the webhook's own name and avatar, truncated to Discord's 2000 character
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -862,7 +862,7 @@ stackdog/
# 1. Clone and setup
git clone https://github.com/trydirect/stackdog
cd stackdog
cp .env.sample .env
cp .env.sample .env # or run `stackdog init` once built

# 2. Build
cargo build
Expand Down Expand Up @@ -891,6 +891,14 @@ bpftool version
uname -r # Should be 4.19+
```

### Where the config lives

`stackdog` reads settings from three places, nearest first: real environment
variables, then `.env` in the working directory, then `~/.config/stackdog/.env`
(`$XDG_CONFIG_HOME/stackdog/.env` when set). Every setting has a default, so
the app runs with no config at all. `stackdog init` writes a starter file to
the config directory and never overwrites an existing one.

---

## 🤝 Contributing
Expand Down
3 changes: 3 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ pub enum Command {

/// Inspect and repair the Stackdog firewall rules
Firewall(FirewallCommand),

/// Write a starter config to ~/.config/stackdog/.env
Init,
}

#[derive(Args, Debug, Clone)]
Expand Down
63 changes: 56 additions & 7 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,11 @@ use tracing_subscriber::FmtSubscriber;

#[actix_rt::main]
async fn main() -> io::Result<()> {
// Load environment
if let Err(err) = dotenvy::dotenv() {
eprintln!(
"Warning: could not load .env file ({}). Continuing with existing environment.",
err
);
}
// Parse CLI first, then load the environment: real variables always
// win (dotenvy never overrides them), then ./.env for project-local
// setups, then the service-style config in ~/.config/stackdog/.
let cli = Cli::parse();
load_environment(!matches!(cli.command, Some(Command::Init)));

// Parse CLI arguments
let cli = Cli::parse();
Expand Down Expand Up @@ -103,11 +101,62 @@ async fn main() -> io::Result<()> {
cli::FirewallSubcommand::List => run_firewall_list().await,
cli::FirewallSubcommand::Reconcile => run_firewall_reconcile().await,
},
Some(Command::Init) => run_init(),
// Default: serve (backward compatible)
Some(Command::Serve) | None => run_serve().await,
}
}

/// Where the service-style config lives: `$XDG_CONFIG_HOME/stackdog`,
/// falling back to `~/.config/stackdog`.
fn config_dir() -> Option<std::path::PathBuf> {
if let Some(xdg) = env::var_os("XDG_CONFIG_HOME") {
if !xdg.is_empty() {
return Some(std::path::PathBuf::from(xdg).join("stackdog"));
}
}
env::var_os("HOME").map(|home| std::path::PathBuf::from(home).join(".config").join("stackdog"))
}

/// Load `.env` from the working directory and from [`config_dir`]. The
/// nearest file wins: dotenvy never overrides a variable that is already
/// set, and the working directory is loaded first. Real environment
/// variables win over both. With `quiet`, a missing file is silent — used
/// by `stackdog init`, which exists to create one.
fn load_environment(quiet: bool) {
let local = dotenvy::dotenv();
let home = config_dir().map(|dir| dotenvy::from_path(dir.join(".env")));
if local.is_err() && home.as_ref().is_none_or(Result::is_err) && !quiet {
eprintln!(
"Note: no .env in the working directory or {:?}. Using the existing environment; every setting has a default. Run `stackdog init` to write a starter config.",
config_dir()
);
}
}

/// Write a starter config to [`config_dir`]. An existing file is never
/// overwritten: it holds credentials.
fn run_init() -> io::Result<()> {
let dir = config_dir().ok_or_else(|| {
io::Error::new(
io::ErrorKind::NotFound,
"cannot determine the config directory (no HOME or XDG_CONFIG_HOME)",
)
})?;
std::fs::create_dir_all(&dir)?;
let path = dir.join(".env");
if path.exists() {
println!("{} already exists; not overwriting it.", path.display());
return Ok(());
}
std::fs::write(&path, include_str!("../.env.sample"))?;
println!("Wrote {}.", path.display());
println!(
"Edit it to set notification channels and log paths, then start stackdog again."
);
Ok(())
}

async fn run_serve() -> io::Result<()> {
let app_host = env::var("APP_HOST").unwrap_or_else(|_| "0.0.0.0".to_string());
let app_port = env::var("APP_PORT").unwrap_or_else(|_| "5000".to_string());
Expand Down
Loading