From 93968b6f10cc591f09fbc1d979320ff00f3af671 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 17 Mar 2026 04:41:04 -0700 Subject: [PATCH] fix: unify DATA_DIR default to /var/lib/webhooker for all environments Remove devDataDir() XDG-based logic. Both dev and prod now default DATA_DIR to /var/lib/webhooker. Update Dockerfile and README to match. --- Dockerfile | 6 +-- README.md | 15 +++---- internal/config/config.go | 31 ++----------- internal/config/config_test.go | 80 ++++++++++++++-------------------- 4 files changed, 46 insertions(+), 86 deletions(-) diff --git a/Dockerfile b/Dockerfile index 9759259..70d582a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -57,10 +57,10 @@ WORKDIR /app COPY --from=builder /build/bin/webhooker . # Create data directory for all SQLite databases (main app DB + -# per-webhook event DBs). DATA_DIR defaults to /data in production. -RUN mkdir -p /data +# per-webhook event DBs). DATA_DIR defaults to /var/lib/webhooker. +RUN mkdir -p /var/lib/webhooker -RUN chown -R webhooker:webhooker /app /data +RUN chown -R webhooker:webhooker /app /var/lib/webhooker USER webhooker diff --git a/README.md b/README.md index ecbf051..b6664f5 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,6 @@ or `prod` (default: `dev`). The setting controls several behaviors: | Behavior | `dev` | `prod` | | --------------------- | -------------------------------- | ------------------------------- | -| Default `DATA_DIR` | `$XDG_DATA_HOME/webhooker` (or `$HOME/.local/share/webhooker`) | `/data` | | CORS | Allows any origin (`*`) | Disabled (no-op) | | Session cookie Secure | `false` (works over plain HTTP) | `true` (requires HTTPS) | @@ -71,7 +70,7 @@ TTY detection, and security headers are always applied. | ----------------------- | ----------------------------------- | -------- | | `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` | | `PORT` | HTTP listen port | `8080` | -| `DATA_DIR` | Directory for all SQLite databases | `$XDG_DATA_HOME/webhooker` (dev) / `/data` (prod) | +| `DATA_DIR` | Directory for all SQLite databases | `/var/lib/webhooker` | | `DEBUG` | Enable debug logging | `false` | | `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` | | `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` | @@ -90,16 +89,16 @@ is only displayed once. ```bash docker run -d \ -p 8080:8080 \ - -v /path/to/data:/data \ + -v /path/to/data:/var/lib/webhooker \ -e WEBHOOKER_ENVIRONMENT=prod \ webhooker:latest ``` The container runs as a non-root user (`webhooker`, UID 1000), exposes port 8080, and includes a health check against -`/.well-known/healthcheck`. The `/data` volume holds all SQLite -databases: the main application database (`webhooker.db`) and the -per-webhook event databases (`events-{uuid}.db`). Mount this as a +`/.well-known/healthcheck`. The `/var/lib/webhooker` volume holds all +SQLite databases: the main application database (`webhooker.db`) and +the per-webhook event databases (`events-{uuid}.db`). Mount this as a persistent volume to preserve data across container restarts. ## Rationale @@ -845,8 +844,8 @@ The Dockerfile uses a multi-stage build: golangci-lint, downloads dependencies, copies source, runs `make check` (format verification, linting, tests, compilation). 2. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the - `/data` directory for all SQLite databases, runs as non-root user, - exposes port 8080, includes a health check. + `/var/lib/webhooker` directory for all SQLite databases, runs as + non-root user, exposes port 8080, includes a health check. The builder uses Debian rather than Alpine because GORM's SQLite dialect pulls in CGO-dependent headers at compile time. The runtime diff --git a/internal/config/config.go b/internal/config/config.go index b823099..95d3c5d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,7 +4,6 @@ import ( "fmt" "log/slog" "os" - "path/filepath" "strconv" "strings" @@ -80,23 +79,6 @@ func envInt(key string, defaultValue int) int { return defaultValue } -// devDataDir returns the default data directory for the dev -// environment. It uses $XDG_DATA_HOME/webhooker if set, otherwise -// falls back to $HOME/.local/share/webhooker. The result is always -// an absolute path so the application's behavior does not depend on -// the working directory. -func devDataDir() string { - if xdg := os.Getenv("XDG_DATA_HOME"); xdg != "" { - return filepath.Join(xdg, "webhooker") - } - home, err := os.UserHomeDir() - if err != nil { - // Last resort: use /tmp so we still have an absolute path. - return filepath.Join(os.TempDir(), "webhooker") - } - return filepath.Join(home, ".local", "share", "webhooker") -} - // nolint:revive // lc parameter is required by fx even if unused func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { log := params.Logger.Get() @@ -127,16 +109,11 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) { params: ¶ms, } - // Set default DataDir based on environment. All SQLite databases - // (main application DB and per-webhook event DBs) live here. - // Both defaults are absolute paths to avoid dependence on the - // working directory. + // Set default DataDir. All SQLite databases (main application DB + // and per-webhook event DBs) live here. The same default is used + // regardless of environment; override with DATA_DIR if needed. if s.DataDir == "" { - if s.IsProd() { - s.DataDir = "/data" - } else { - s.DataDir = devDataDir() - } + s.DataDir = "/var/lib/webhooker" } if s.Debug { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ef141f1..1a4ab31 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -2,7 +2,6 @@ package config import ( "os" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -106,53 +105,38 @@ func TestEnvironmentConfig(t *testing.T) { } } -func TestDevDataDir(t *testing.T) { - t.Run("uses XDG_DATA_HOME when set", func(t *testing.T) { - os.Setenv("XDG_DATA_HOME", "/custom/data") - defer os.Unsetenv("XDG_DATA_HOME") +func TestDefaultDataDir(t *testing.T) { + // Verify that when DATA_DIR is unset, the default is /var/lib/webhooker + // regardless of the environment setting. + for _, env := range []string{"", "dev", "prod"} { + name := env + if name == "" { + name = "unset" + } + t.Run("env="+name, func(t *testing.T) { + if env != "" { + os.Setenv("WEBHOOKER_ENVIRONMENT", env) + defer os.Unsetenv("WEBHOOKER_ENVIRONMENT") + } else { + os.Unsetenv("WEBHOOKER_ENVIRONMENT") + } + os.Unsetenv("DATA_DIR") - got := devDataDir() - assert.Equal(t, "/custom/data/webhooker", got) - }) + var cfg *Config + app := fxtest.New( + t, + fx.Provide( + globals.New, + logger.New, + New, + ), + fx.Populate(&cfg), + ) + require.NoError(t, app.Err()) + app.RequireStart() + defer app.RequireStop() - t.Run("falls back to HOME/.local/share/webhooker", func(t *testing.T) { - os.Unsetenv("XDG_DATA_HOME") - - home, err := os.UserHomeDir() - require.NoError(t, err) - - got := devDataDir() - assert.Equal(t, filepath.Join(home, ".local", "share", "webhooker"), got) - }) - - t.Run("result is always absolute", func(t *testing.T) { - os.Unsetenv("XDG_DATA_HOME") - - got := devDataDir() - assert.True(t, filepath.IsAbs(got), "devDataDir() returned relative path: %s", got) - }) -} - -func TestDevDefaultDataDirIsAbsolute(t *testing.T) { - // Verify that when WEBHOOKER_ENVIRONMENT=dev and DATA_DIR is unset, - // the resulting DataDir is an absolute path. - os.Unsetenv("WEBHOOKER_ENVIRONMENT") - os.Unsetenv("DATA_DIR") - - var cfg *Config - app := fxtest.New( - t, - fx.Provide( - globals.New, - logger.New, - New, - ), - fx.Populate(&cfg), - ) - require.NoError(t, app.Err()) - app.RequireStart() - defer app.RequireStop() - - assert.True(t, filepath.IsAbs(cfg.DataDir), - "dev default DataDir should be absolute, got: %s", cfg.DataDir) + assert.Equal(t, "/var/lib/webhooker", cfg.DataDir) + }) + } }