Compare commits
2 Commits
93968b6f10
...
b1343364a3
| Author | SHA1 | Date | |
|---|---|---|---|
| b1343364a3 | |||
|
|
ec92c2bdd2 |
@@ -57,10 +57,10 @@ WORKDIR /app
|
|||||||
COPY --from=builder /build/bin/webhooker .
|
COPY --from=builder /build/bin/webhooker .
|
||||||
|
|
||||||
# Create data directory for all SQLite databases (main app DB +
|
# Create data directory for all SQLite databases (main app DB +
|
||||||
# per-webhook event DBs). DATA_DIR defaults to /var/lib/webhooker.
|
# per-webhook event DBs). DATA_DIR defaults to /data in production.
|
||||||
RUN mkdir -p /var/lib/webhooker
|
RUN mkdir -p /data
|
||||||
|
|
||||||
RUN chown -R webhooker:webhooker /app /var/lib/webhooker
|
RUN chown -R webhooker:webhooker /app /data
|
||||||
|
|
||||||
USER webhooker
|
USER webhooker
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -59,6 +59,7 @@ or `prod` (default: `dev`). The setting controls several behaviors:
|
|||||||
|
|
||||||
| Behavior | `dev` | `prod` |
|
| Behavior | `dev` | `prod` |
|
||||||
| --------------------- | -------------------------------- | ------------------------------- |
|
| --------------------- | -------------------------------- | ------------------------------- |
|
||||||
|
| Default `DATA_DIR` | `$XDG_DATA_HOME/webhooker` (or `$HOME/.local/share/webhooker`) | `/data` |
|
||||||
| CORS | Allows any origin (`*`) | Disabled (no-op) |
|
| CORS | Allows any origin (`*`) | Disabled (no-op) |
|
||||||
| Session cookie Secure | `false` (works over plain HTTP) | `true` (requires HTTPS) |
|
| Session cookie Secure | `false` (works over plain HTTP) | `true` (requires HTTPS) |
|
||||||
|
|
||||||
@@ -70,7 +71,7 @@ TTY detection, and security headers are always applied.
|
|||||||
| ----------------------- | ----------------------------------- | -------- |
|
| ----------------------- | ----------------------------------- | -------- |
|
||||||
| `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` |
|
| `WEBHOOKER_ENVIRONMENT` | `dev` or `prod` | `dev` |
|
||||||
| `PORT` | HTTP listen port | `8080` |
|
| `PORT` | HTTP listen port | `8080` |
|
||||||
| `DATA_DIR` | Directory for all SQLite databases | `/var/lib/webhooker` |
|
| `DATA_DIR` | Directory for all SQLite databases | `$XDG_DATA_HOME/webhooker` (dev) / `/data` (prod) |
|
||||||
| `DEBUG` | Enable debug logging | `false` |
|
| `DEBUG` | Enable debug logging | `false` |
|
||||||
| `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` |
|
| `METRICS_USERNAME` | Basic auth username for `/metrics` | `""` |
|
||||||
| `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` |
|
| `METRICS_PASSWORD` | Basic auth password for `/metrics` | `""` |
|
||||||
@@ -89,16 +90,16 @@ is only displayed once.
|
|||||||
```bash
|
```bash
|
||||||
docker run -d \
|
docker run -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
-v /path/to/data:/var/lib/webhooker \
|
-v /path/to/data:/data \
|
||||||
-e WEBHOOKER_ENVIRONMENT=prod \
|
-e WEBHOOKER_ENVIRONMENT=prod \
|
||||||
webhooker:latest
|
webhooker:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
The container runs as a non-root user (`webhooker`, UID 1000), exposes
|
The container runs as a non-root user (`webhooker`, UID 1000), exposes
|
||||||
port 8080, and includes a health check against
|
port 8080, and includes a health check against
|
||||||
`/.well-known/healthcheck`. The `/var/lib/webhooker` volume holds all
|
`/.well-known/healthcheck`. The `/data` volume holds all SQLite
|
||||||
SQLite databases: the main application database (`webhooker.db`) and
|
databases: the main application database (`webhooker.db`) and the
|
||||||
the per-webhook event databases (`events-{uuid}.db`). Mount this as a
|
per-webhook event databases (`events-{uuid}.db`). Mount this as a
|
||||||
persistent volume to preserve data across container restarts.
|
persistent volume to preserve data across container restarts.
|
||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
@@ -844,8 +845,8 @@ The Dockerfile uses a multi-stage build:
|
|||||||
golangci-lint, downloads dependencies, copies source, runs `make
|
golangci-lint, downloads dependencies, copies source, runs `make
|
||||||
check` (format verification, linting, tests, compilation).
|
check` (format verification, linting, tests, compilation).
|
||||||
2. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the
|
2. **Runtime stage** (`alpine:3.21`) — copies the binary, creates the
|
||||||
`/var/lib/webhooker` directory for all SQLite databases, runs as
|
`/data` directory for all SQLite databases, runs as non-root user,
|
||||||
non-root user, exposes port 8080, includes a health check.
|
exposes port 8080, includes a health check.
|
||||||
|
|
||||||
The builder uses Debian rather than Alpine because GORM's SQLite
|
The builder uses Debian rather than Alpine because GORM's SQLite
|
||||||
dialect pulls in CGO-dependent headers at compile time. The runtime
|
dialect pulls in CGO-dependent headers at compile time. The runtime
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -79,6 +80,23 @@ func envInt(key string, defaultValue int) int {
|
|||||||
return defaultValue
|
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
|
// nolint:revive // lc parameter is required by fx even if unused
|
||||||
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||||
log := params.Logger.Get()
|
log := params.Logger.Get()
|
||||||
@@ -109,11 +127,16 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
|||||||
params: ¶ms,
|
params: ¶ms,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default DataDir. All SQLite databases (main application DB
|
// Set default DataDir based on environment. All SQLite databases
|
||||||
// and per-webhook event DBs) live here. The same default is used
|
// (main application DB and per-webhook event DBs) live here.
|
||||||
// regardless of environment; override with DATA_DIR if needed.
|
// Both defaults are absolute paths to avoid dependence on the
|
||||||
|
// working directory.
|
||||||
if s.DataDir == "" {
|
if s.DataDir == "" {
|
||||||
s.DataDir = "/var/lib/webhooker"
|
if s.IsProd() {
|
||||||
|
s.DataDir = "/data"
|
||||||
|
} else {
|
||||||
|
s.DataDir = devDataDir()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.Debug {
|
if s.Debug {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -105,21 +106,37 @@ func TestEnvironmentConfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultDataDir(t *testing.T) {
|
func TestDevDataDir(t *testing.T) {
|
||||||
// Verify that when DATA_DIR is unset, the default is /var/lib/webhooker
|
t.Run("uses XDG_DATA_HOME when set", func(t *testing.T) {
|
||||||
// regardless of the environment setting.
|
os.Setenv("XDG_DATA_HOME", "/custom/data")
|
||||||
for _, env := range []string{"", "dev", "prod"} {
|
defer os.Unsetenv("XDG_DATA_HOME")
|
||||||
name := env
|
|
||||||
if name == "" {
|
got := devDataDir()
|
||||||
name = "unset"
|
assert.Equal(t, "/custom/data/webhooker", got)
|
||||||
}
|
})
|
||||||
t.Run("env="+name, func(t *testing.T) {
|
|
||||||
if env != "" {
|
t.Run("falls back to HOME/.local/share/webhooker", func(t *testing.T) {
|
||||||
os.Setenv("WEBHOOKER_ENVIRONMENT", env)
|
os.Unsetenv("XDG_DATA_HOME")
|
||||||
defer os.Unsetenv("WEBHOOKER_ENVIRONMENT")
|
|
||||||
} else {
|
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("WEBHOOKER_ENVIRONMENT")
|
||||||
}
|
|
||||||
os.Unsetenv("DATA_DIR")
|
os.Unsetenv("DATA_DIR")
|
||||||
|
|
||||||
var cfg *Config
|
var cfg *Config
|
||||||
@@ -136,7 +153,6 @@ func TestDefaultDataDir(t *testing.T) {
|
|||||||
app.RequireStart()
|
app.RequireStart()
|
||||||
defer app.RequireStop()
|
defer app.RequireStop()
|
||||||
|
|
||||||
assert.Equal(t, "/var/lib/webhooker", cfg.DataDir)
|
assert.True(t, filepath.IsAbs(cfg.DataDir),
|
||||||
})
|
"dev default DataDir should be absolute, got: %s", cfg.DataDir)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user