From 4513a9e62be8268f01b8578ad11b5327d66808d1eb5c3786995963bd8414bcba Mon Sep 17 00:00:00 2001 From: Lixen Wraith Date: Tue, 6 Oct 2026 00:14:05 -0400 Subject: [PATCH] v0.27.1 packaging update, install and initial config --- .github/workflows/package.yml | 65 +++++++++++---- AGENTS.md | 6 +- Makefile | 48 +++++++---- README.md | 7 +- cmd/lw/cli.go | 44 ++++++++++ cmd/lw/cli_test.go | 30 +++++++ config/sample.go | 8 ++ deploy/package/completion/_lw | 17 ++++ deploy/package/completion/lw.bash | 11 ++- deploy/package/completion/lw.fish | 59 +++++++------ doc/cli.md | 28 ++++++- doc/installation.md | 134 +++++++++++++++++++++++++----- doc/lw.1 | 47 ++++++++--- doc/todo.md | 19 +++-- internal/config/loader.go | 18 +++- 15 files changed, 431 insertions(+), 110 deletions(-) create mode 100644 config/sample.go diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index 669883d..c412543 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -7,7 +7,7 @@ on: - Dockerfile - go.mod - go.sum - - config/logwisp.toml + - config/** - doc/lw.1 - deploy/package/** - .github/workflows/package.yml @@ -17,9 +17,9 @@ on: permissions: contents: read jobs: - # The AUR package from this commit: makepkg finds the tarball the PKGBUILD - # names next to it and skips the download, then builds, tests and packages. - # checkout's safe.directory lasts only its own step; root owns .git here. + # make arch builds the AUR package from this checkout; the package installs + # over an edited configuration make install left, reinstalls keeping it, and + # goes. makepkg refuses root, so a user that owns the checkout runs it. arch: runs-on: ubuntu-latest timeout-minutes: 20 @@ -28,24 +28,52 @@ jobs: - run: pacman -Syu --noconfirm --needed git go - uses: actions/checkout@v5 - run: | - ver=$(sed -n 's/^pkgver=//p' deploy/package/arch/PKGBUILD) - dir=$(mktemp -d) - git -c safe.directory="$PWD" archive --prefix="logwisp-$ver/" -o "$dir/logwisp-$ver.tar.gz" HEAD - cp deploy/package/arch/PKGBUILD "$dir" useradd -m build - chown -R build "$dir" - su build -c "cd '$dir' && makepkg --noconfirm" - pacman -U --noconfirm "$dir"/logwisp-*.pkg.tar.zst + chown -R build . + su build -c 'make arch' - run: | + for f in bin/arch/logwisp-*.pkg.tar.zst; do case $f in *-debug-*) ;; *) pkg=$f ;; esac; done + install -D -m 0644 config/logwisp.toml /etc/logwisp/logwisp.toml + echo '# edited' >> /etc/logwisp/logwisp.toml + pacman -U --noconfirm "$pkg" + test -f /etc/logwisp/logwisp.toml.pacnew lw --version test -f /usr/share/bash-completion/completions/lw test -f /usr/share/zsh/site-functions/_lw test -f /usr/share/fish/vendor_completions.d/lw.fish + pacman -U --noconfirm "$pkg" + grep -q '^# edited' /etc/logwisp/logwisp.toml + lw --check -c /etc/logwisp/logwisp.toml pacman -R --noconfirm logwisp test ! -e /usr/bin/lw + test -f /etc/logwisp/logwisp.toml.pacsave + # make install SERVICE=no into a home, twice, then lw config init places the + # configuration lw reads without -c, and never replaces it + user: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + - run: make release + - run: | + make install SERVICE=no PREFIX="$HOME/.local" + make install SERVICE=no PREFIX="$HOME/.local" + export PATH="$HOME/.local/bin:$PATH" + test ! -e "$HOME/.local/lib/systemd/system/logwisp.service" + test ! -e /etc/logwisp/logwisp.toml + lw config init + lw --check + lw --dump | grep -q 'id = "default_source"' + if lw config init; then exit 1; fi + make uninstall PREFIX="$HOME/.local" + test ! -e "$HOME/.local/bin/lw" + test -f "$HOME/.config/logwisp/logwisp.toml" # make deb on a host with systemd: the service runs from the package's - # configuration; remove stops and disables it and leaves the conffile, purge - # removes that too + # configuration and survives a reinstall that keeps an edited one; remove + # stops and disables it and leaves the conffile, purge removes that too deb: runs-on: ubuntu-latest timeout-minutes: 10 @@ -62,6 +90,11 @@ jobs: sudo systemctl enable --now logwisp sleep 3 systemctl is-active logwisp || { sudo journalctl -u logwisp --no-pager; exit 1; } + echo '# edited' | sudo tee -a /etc/logwisp/logwisp.toml + sudo apt-get install --reinstall -y ./bin/logwisp_*.deb + grep -q '^# edited' /etc/logwisp/logwisp.toml + sleep 3 + systemctl is-active logwisp || { sudo journalctl -u logwisp --no-pager; exit 1; } sudo apt-get remove -y logwisp test ! -e /usr/bin/lw test ! -e /usr/lib/systemd/system/logwisp.service @@ -70,7 +103,8 @@ jobs: test -f /etc/logwisp/logwisp.toml sudo apt-get purge -y logwisp test ! -e /etc/logwisp/logwisp.toml - # BSD make on FreeBSD: build, install, run the rc.d service, uninstall + # BSD make on FreeBSD: build, install twice keeping an edited configuration, + # run the rc.d service, uninstall freebsd: runs-on: ubuntu-latest timeout-minutes: 20 @@ -85,6 +119,9 @@ jobs: set -e make release make install + echo '# edited' >> /usr/local/etc/logwisp/logwisp.toml + make install + grep -q '^# edited' /usr/local/etc/logwisp/logwisp.toml lw --version pw groupadd logwisp pw useradd logwisp -g logwisp -d /nonexistent -s /usr/sbin/nologin diff --git a/AGENTS.md b/AGENTS.md index 382888e..015fd8f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -77,9 +77,9 @@ change touches. keys must fail at any depth. Set defaults on the options struct before the call; `TestEveryPluginRejectsUnknownKeys` covers every registered plugin. - `cmd/lw/cli.go` owns the command line: lw's own flags, and the commands - table whose groups (`auth`, `tls`, `preset`) share one flag mechanism; - `config.Load` takes the parsed `config.Args`. Presets are rows of one table - in `internal/config/preset.go`. + table whose groups (`auth`, `tls`, `preset`, `config`) share one flag + mechanism; `config.Load` takes the parsed `config.Args`. Presets are rows of + one table in `internal/config/preset.go`. - Network security has three seams: `tlsx` (TLS configs, certificates made at startup or by `lw tls`, pins, startup warnings), `authz`, and `netacl` (address rules and per-client limits: a listener wraps the socket diff --git a/Makefile b/Makefile index e9898fb..965914a 100644 --- a/Makefile +++ b/Makefile @@ -45,6 +45,9 @@ IMAGE_RUN = $(CONTAINER_ENGINE) run --read-only --user 65532:65532 \ --network none --cap-drop ALL --security-opt no-new-privileges DESTDIR ?= +# SERVICE=no installs lw alone: no service files and no system configuration, +# which lw config init then places where the user wants it. +SERVICE ?= yes INSTALL_OS_DEFAULT != uname -s INSTALL_OS ?= $(INSTALL_OS_DEFAULT) # FreeBSD keeps everything outside the base system under /usr/local. @@ -66,7 +69,7 @@ DEB_MAINTAINER ?= Maintainer .DEFAULT_GOAL := help -.PHONY: help check-go build release dev test verify e2e image image-check install uninstall deb completion clean version +.PHONY: help check-go build release dev test verify e2e image image-check install uninstall deb arch completion clean version help: @echo "Usage: make [VARIABLE=value ...]" @@ -87,12 +90,13 @@ help: @echo " image-check Run it read-only, offline, without capabilities: --version, then" @echo " --check on $(IMAGE_CHECK_CONFIG)" @echo "Install (build or release first):" - @echo " install Binary, manual, sample config, service files, shell completion," - @echo " licence and docs" + @echo " install Binary, manual, shell completion, licence and docs; unless SERVICE=no," + @echo " the service files and $(SYSCONFDIR)/logwisp/logwisp.toml (kept if present)" @echo " uninstall Remove them, keeping $(SYSCONFDIR)/logwisp" @echo " deb Package a release build as $(BIN_DIR)/logwisp_VERSION_ARCH.deb (dpkg-deb)" + @echo " arch Build the Arch package from this working tree in $(BIN_DIR)/arch (makepkg)" @echo "" - @echo "Variables: GO=$(GO) PREFIX=$(PREFIX) SYSCONFDIR=$(SYSCONFDIR) DESTDIR=$(DESTDIR)" + @echo "Variables: GO=$(GO) PREFIX=$(PREFIX) SYSCONFDIR=$(SYSCONFDIR) DESTDIR=$(DESTDIR) SERVICE=$(SERVICE)" @echo " INSTALL_OS=$(INSTALL_OS) (the layout install uses: FreeBSD or any other)" @echo " GO_BUILDFLAGS, GO_LDFLAGS (extra go build and linker flags), E2E (scripts)," @echo " CONTAINER_ENGINE, IMAGE, IMAGE_TAG, BUILD_CA, IMAGE_BUILD_FLAGS, IMAGE_CHECK_CONFIG," @@ -131,10 +135,10 @@ verify: test $(GO) vet ./... @base='$(GOFMT_BASE)'; \ if [ -n "$$base" ] && fork=$$(git merge-base "$$base" HEAD 2>/dev/null); then \ - files=$$( { git diff --name-only --find-renames=100% --diff-filter=dr "$$fork" -- cmd internal; \ - git ls-files --others --exclude-standard -- cmd internal; } | grep '\.go$$' || true); \ + files=$$( { git diff --name-only --find-renames=100% --diff-filter=dr "$$fork" -- cmd config internal; \ + git ls-files --others --exclude-standard -- cmd config internal; } | grep '\.go$$' || true); \ else \ - files=$$(find cmd internal -name '*.go'); \ + files=$$(find cmd config internal -name '*.go'); \ fi; \ bad=$$( [ -z "$$files" ] || "$$($(GO) env GOROOT)/bin/gofmt" -l $$files ); \ if [ -n "$$bad" ]; then echo "gofmt -l reports:"; echo "$$bad"; exit 1; fi @@ -192,10 +196,11 @@ image-check: # install stages what a package ships and compiles nothing, so a packager owns # the build flags. The service files get the installed paths substituted; a -# configuration already in place is kept. +# configuration already in place is kept, on every system alike. install: @set -eu; \ [ -x $(BIN_DIR)/$(BINARY) ] || { echo "no $(BIN_DIR)/$(BINARY): run make build or make release first" >&2; exit 1; }; \ + case '$(SERVICE)' in yes|no) ;; *) echo "SERVICE is yes or no, not '$(SERVICE)'" >&2; exit 2 ;; esac; \ put() { install -d -m 0755 "$${3%/*}"; install -m "$$1" "$$2" "$$3"; echo "install $$3"; }; \ sub() { install -d -m 0755 "$${3%/*}"; \ sed -e 's|@BINDIR@|$(BINDIR)|g' -e 's|@SYSCONFDIR@|$(SYSCONFDIR)|g' -e 's|%%BINDIR%%|$(BINDIR)|g' \ @@ -209,14 +214,13 @@ install: put 0644 $(PKG_DIR)/completion/lw.fish "$$root$(FISHCOMPDIR)/lw.fish"; \ put 0644 LICENSE "$$root$(LICENSEDIR)/LICENSE"; \ for src in README.md doc/*.md; do put 0644 "$$src" "$$root$(DOCDIR)/$$src"; done; \ + if [ '$(SERVICE)' = no ]; then echo "skip the service and $$etc (SERVICE=no): lw config init writes a configuration"; exit 0; fi; \ + if [ -e "$$etc/logwisp.toml" ]; then echo "keep $$etc/logwisp.toml"; \ + else put 0644 config/logwisp.toml "$$etc/logwisp.toml"; fi; \ case '$(INSTALL_OS)' in \ FreeBSD) \ - put 0644 config/logwisp.toml "$$etc/logwisp.toml.sample"; \ - if [ -z "$$root" ] && [ ! -e "$$etc/logwisp.toml" ]; then put 0644 config/logwisp.toml "$$etc/logwisp.toml"; fi; \ sub 0755 $(PKG_DIR)/logwisp.rc "$$root$(PREFIX)/etc/rc.d/logwisp" ;; \ *) \ - if [ -e "$$etc/logwisp.toml" ]; then echo "keep $$etc/logwisp.toml"; \ - else put 0644 config/logwisp.toml "$$etc/logwisp.toml"; fi; \ sub 0644 $(PKG_DIR)/logwisp.service "$$root$(PREFIX)/lib/systemd/system/logwisp.service"; \ sub 0644 $(PKG_DIR)/logwisp.sysusers "$$root$(PREFIX)/lib/sysusers.d/logwisp.conf"; \ sub 0644 $(PKG_DIR)/logwisp.tmpfiles "$$root$(PREFIX)/lib/tmpfiles.d/logwisp.conf" ;; \ @@ -228,14 +232,14 @@ uninstall: "$$root$(BASHCOMPDIR)/lw" "$$root$(ZSHCOMPDIR)/_lw" "$$root$(FISHCOMPDIR)/lw.fish" \ "$$root$(PREFIX)/lib/systemd/system/logwisp.service" \ "$$root$(PREFIX)/lib/sysusers.d/logwisp.conf" "$$root$(PREFIX)/lib/tmpfiles.d/logwisp.conf" \ - "$$root$(PREFIX)/etc/rc.d/logwisp" "$$root$(SYSCONFDIR)/logwisp/logwisp.toml.sample"; do \ + "$$root$(PREFIX)/etc/rc.d/logwisp"; do \ if [ -e "$$f" ]; then rm -f "$$f"; echo "remove $$f"; fi; \ done; \ for d in "$$root$(DOCDIR)" "$$root$(LICENSEDIR)"; do \ case "$$d" in */logwisp) ;; *) echo "keep $$d (not a logwisp directory)"; continue ;; esac; \ if [ -d "$$d" ]; then rm -rf "$$d"; echo "remove $$d"; fi; \ done; \ - echo "keep $$root$(SYSCONFDIR)/logwisp (configuration and credentials)" + if [ -d "$$root$(SYSCONFDIR)/logwisp" ]; then echo "keep $$root$(SYSCONFDIR)/logwisp (configuration and credentials)"; fi # deb stages install in Debian's layout and takes the architecture from the # binary. An untagged or modified tree's version sorts below every release and @@ -268,7 +272,23 @@ deb: release sed -e 's/^$$/./' -e 's/^/ /' $(PKG_DIR)/freebsd/pkg-descr; } > "$$stage/DEBIAN/control"; \ dpkg-deb --root-owner-group --build "$$stage" "$(BIN_DIR)/logwisp_$${version}_$$arch.deb" +# arch builds the PKGBUILD from this working tree, tracked and new files, as +# the release tarball it names; no tag or download is needed. Untagged, the +# version is 0.rCOUNT.gHASH, below every release and rising with each commit. +# The package and build tree stay in bin/arch whatever makepkg.conf says. +arch: + @set -eu; dir=$(BIN_DIR)/arch; [ ! -d "$$dir" ] || chmod -R u+w "$$dir"; rm -rf "$$dir"; mkdir -p "$$dir"; \ + ver=$$(echo '$(VERSION)' | sed -e 's/^v//' -e 's/-\([0-9][0-9]*\)-g/.r\1.g/' -e 's/-/./g'); \ + case '$(VERSION)' in v[0-9]*) ;; *) ver="0.r$$(git rev-list --count HEAD).g$$ver" ;; esac; \ + git ls-files -z --cached --others --exclude-standard | tar --null -T - --ignore-failed-read \ + --transform "s,^,logwisp-$$ver/,S" -czf "$$dir/logwisp-$$ver.tar.gz"; \ + sed "s/^pkgver=.*/pkgver=$$ver/" $(PKG_DIR)/arch/PKGBUILD > "$$dir/PKGBUILD"; \ + cd "$$dir"; export PKGDEST="$$PWD" BUILDDIR="$$PWD"; makepkg; \ + echo "install or reinstall: sudo pacman -U $$(makepkg --packagelist | grep -v -- -debug-)" + +# A toolchain go downloaded into make arch's GOPATH is read-only. clean: + [ ! -d $(BIN_DIR) ] || chmod -R u+w $(BIN_DIR) rm -rf $(BIN_DIR) version: diff --git a/README.md b/README.md index d2f476d..4f6d6bf 100644 --- a/README.md +++ b/README.md @@ -186,8 +186,11 @@ docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \ ``` `sudo make install` installs the binary, the manual, shell completion, a -sample configuration and a systemd unit or FreeBSD rc.d script; `make deb` -packages them for Debian and Ubuntu. See [Installation](doc/installation.md). +systemd unit or FreeBSD rc.d script and its configuration; `make deb` and +`make arch` package them for Debian, Ubuntu and Arch. Without a service, +`make install SERVICE=no PREFIX="$HOME/.local"` installs `lw` alone and +`lw config init` writes the annotated configuration it reads. See +[Installation](doc/installation.md). ## System Requirements diff --git a/cmd/lw/cli.go b/cmd/lw/cli.go index 858e73f..cd6a85f 100644 --- a/cmd/lw/cli.go +++ b/cmd/lw/cli.go @@ -6,11 +6,15 @@ import ( "flag" "fmt" "io" + "io/fs" + "os" + "path/filepath" "regexp" "slices" "strings" "unicode" + shipped "github.com/lixenwraith/logwisp/config" "github.com/lixenwraith/logwisp/internal/config" "github.com/lixenwraith/toml" @@ -37,6 +41,8 @@ var commands = []command{ "Files are never overwritten; keys are written mode 0600.", tlsCommands}, {"preset", "Show the pipeline a preset makes, as TOML", "lw --preset NAME,key=value runs one; flags here are its keys.", presetCommands()}, + {"config", "Place the annotated default configuration", + "Files are never overwritten; lw --dump prints the effective configuration.", configCommands}, } // shorts are lw's one-letter flags, each its long flag in every command that @@ -287,3 +293,41 @@ func presetCommands() []subcommand { } return subs } + +var configCommands = []subcommand{ + {"init", "[--out FILE]", + "Write the annotated default configuration, to edit and run", + nil, defineConfigInit}, +} + +func defineConfigInit(flags *flag.FlagSet) func(stdout, stderr io.Writer) error { + out := flags.String("out", "", "`file` to write (default: ~/.config/logwisp/logwisp.toml)") + return func(_, stderr io.Writer) error { + user, err := config.UserFile() + path := cmp.Or(*out, user) + if path == "" { + return fmt.Errorf("no home directory for the default file (%v): name one with --out", err) + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + if err := writeNew(path, shipped.Sample, 0o644); errors.Is(err, fs.ErrExist) { + if fi, err := os.Stat(path); err == nil && fi.IsDir() { + return fmt.Errorf("%s is a directory: name the file, as in %s", path, filepath.Join(path, "logwisp.toml")) + } + return fmt.Errorf("%s exists; remove it to replace it", path) + } else if err != nil { + return err + } + // -c refuses a path that climbs out with ..; an absolute one loads anywhere + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + if found, err := filepath.Abs(config.DefaultPath()); err == nil && found == path { + fmt.Fprintf(stderr, "wrote %s; lw reads it without -c\n", path) + } else { + fmt.Fprintf(stderr, "wrote %s; run it with lw -c %s\n", path, path) + } + return nil + } +} diff --git a/cmd/lw/cli_test.go b/cmd/lw/cli_test.go index aff70b3..bab073b 100644 --- a/cmd/lw/cli_test.go +++ b/cmd/lw/cli_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + shipped "github.com/lixenwraith/logwisp/config" "github.com/lixenwraith/logwisp/internal/config" "github.com/lixenwraith/logwisp/internal/testutil" "github.com/lixenwraith/logwisp/internal/tlsx" @@ -261,6 +262,35 @@ func TestTLSCommandsIssueAVerifiableChain(t *testing.T) { } } +// lw config init writes the shipped configuration to the file lw reads without +// -c, or to --out, says how lw finds it, and never replaces a file +func TestConfigInitWritesTheShippedFileOnce(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + t.Setenv("LOGWISP_CONFIG_FILE", "") + t.Setenv("LOGWISP_CONFIG_DIR", "") + other := filepath.Join(t.TempDir(), "etc", "logwisp.toml") + for _, c := range []struct { + args []string + hint string + }{ + {[]string{"init"}, "lw reads it without -c"}, + {[]string{"init", "--out", other}, "lw -c " + other}, + } { + if code, _, stderr := runCommand(t, "config", c.args...); code != 0 || !strings.Contains(stderr, c.hint) { + t.Fatalf("lw config %v: exit %d: %s", c.args, code, stderr) + } + if code, _, stderr := runCommand(t, "config", c.args...); code != 1 || !strings.Contains(stderr, "exists") { + t.Fatalf("lw config %v again: exit %d: %s", c.args, code, stderr) + } + } + for _, path := range []string{filepath.Join(home, ".config", "logwisp", "logwisp.toml"), other} { + if data, err := os.ReadFile(path); err != nil || !bytes.Equal(data, shipped.Sample) { + t.Errorf("%s is not the shipped configuration: %v", path, err) + } + } +} + var update = flag.Bool("update", false, "rewrite the completion scripts: make completion") // The installed completion scripts are what lw's tables generate diff --git a/config/sample.go b/config/sample.go new file mode 100644 index 0000000..10b4c0c --- /dev/null +++ b/config/sample.go @@ -0,0 +1,8 @@ +// Package config ships logwisp.toml, the annotated default configuration that +// make install and the packages place and lw config init writes. +package config + +import _ "embed" + +//go:embed logwisp.toml +var Sample []byte diff --git a/deploy/package/completion/_lw b/deploy/package/completion/_lw index 0392ef9..b6962e1 100644 --- a/deploy/package/completion/_lw +++ b/deploy/package/completion/_lw @@ -150,12 +150,29 @@ preset:*) '(-h --help)'{-h,--help}'[print the flags]' ;; esac return ;; +config:2) ;; +config:3) + described=( + 'init:Write the annotated default configuration, to edit and run' + ) + _describe -t commands 'lw config command' described + return ;; +config:*) + shift 2 words + (( CURRENT -= 2 )) + case ${words[1]} in + init) _arguments -S \ + '--out=[file to write (default: ~/.config/logwisp/logwisp.toml)]:value:_default' \ + '(-h --help)'{-h,--help}'[print the flags]' ;; + esac + return ;; esac if (( CURRENT == 2 )) && [[ $PREFIX != -* ]]; then described=( 'auth:SCRAM users, bearer tokens, tcp sink viewing' 'tls:A CA and certificates for TLS and mTLS' 'preset:Show the pipeline a preset makes, as TOML' + 'config:Place the annotated default configuration' 'help:Print the help and exit' ) _describe -t commands 'lw command' described diff --git a/deploy/package/completion/lw.bash b/deploy/package/completion/lw.bash index 11339ec..fa5e60e 100644 --- a/deploy/package/completion/lw.bash +++ b/deploy/package/completion/lw.bash @@ -10,7 +10,7 @@ _lw() { elif [[ $prev == = ]]; then prev=${COMP_WORDS[i-2]} fi case ${COMP_WORDS[1]}:$i in - *:1) words='--auto_reload --check -t --color --config -c --dump --filter --format --heartbeat --help -h --logging.console.target --logging.file.directory --logging.file.max_size_mb --logging.file.max_total_size_mb --logging.file.name --logging.file.retention_hours --logging.format --logging.level --logging.output --logging.sanitization --pipeline --preset -p --quiet -q --rate-limit --sink --source --status_reporter --version -V auth tls preset help' ;; + *:1) words='--auto_reload --check -t --color --config -c --dump --filter --format --heartbeat --help -h --logging.console.target --logging.file.directory --logging.file.max_size_mb --logging.file.max_total_size_mb --logging.file.name --logging.file.retention_hours --logging.format --logging.level --logging.output --logging.sanitization --pipeline --preset -p --quiet -q --rate-limit --sink --source --status_reporter --version -V auth tls preset config help' ;; auth:2) words='add-user remove-user token stream' ;; auth:*) case ${COMP_WORDS[2]} in @@ -80,6 +80,15 @@ _lw() { esac words='--allow --cert --client_ca --deny --format --hosts --issuer_cert --issuer_key --key --listen --out --tls --transport --users --help -h' ;; esac ;; + config:2) words='init' ;; + config:*) + case ${COMP_WORDS[2]} in + init) + case $prev in + --out|-out) COMPREPLY=(); return ;; + esac + words='--out --help -h' ;; + esac ;; *) case $prev in --logging.file.directory) _lw_reply -d; return ;; diff --git a/deploy/package/completion/lw.fish b/deploy/package/completion/lw.fish index cb4ecbf..ef0150a 100644 --- a/deploy/package/completion/lw.fish +++ b/deploy/package/completion/lw.fish @@ -12,34 +12,35 @@ set -l __lw_first 'test (count (commandline -opc)) -eq 1' complete -c lw -n $__lw_first -a auth -d 'SCRAM users, bearer tokens, tcp sink viewing' complete -c lw -n $__lw_first -a tls -d 'A CA and certificates for TLS and mTLS' complete -c lw -n $__lw_first -a preset -d 'Show the pipeline a preset makes, as TOML' +complete -c lw -n $__lw_first -a config -d 'Place the annotated default configuration' complete -c lw -n $__lw_first -a help -d 'Print the help and exit' -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l auto_reload -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l check -o t -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l color -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l config -o c -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l dump -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l filter -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l format -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l heartbeat -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l help -o h -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.console.target -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.directory -x -a '(__fish_complete_directories)' -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.max_size_mb -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.max_total_size_mb -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.name -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.file.retention_hours -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.format -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.level -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.output -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l logging.sanitization -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l pipeline -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l preset -o p -x -a 'pipe tail serve edge aggregator' -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l quiet -o q -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l rate-limit -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l sink -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l source -r -F -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l status_reporter -complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset' -l version -o V +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l auto_reload +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l check -o t +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l color +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l config -o c -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l dump +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l filter -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l format -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l heartbeat -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l help -o h +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.console.target -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.file.directory -x -a '(__fish_complete_directories)' +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.file.max_size_mb -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.file.max_total_size_mb -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.file.name -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.file.retention_hours -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.format -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.level -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.output -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l logging.sanitization -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l pipeline -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l preset -o p -x -a 'pipe tail serve edge aggregator' +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l quiet -o q +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l rate-limit -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l sink -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l source -r -F +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l status_reporter +complete -c lw -n 'not __lw_in auth; and not __lw_in tls; and not __lw_in preset; and not __lw_in config' -l version -o V complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a add-user -d 'Add a user to a credentials file, or replace its password' complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a remove-user -d 'Remove a user from a credentials file' @@ -143,3 +144,7 @@ complete -c lw -n '__lw_in preset aggregator' -l tls -r -F -d 'self|issuer|files complete -c lw -n '__lw_in preset aggregator' -l transport -r -F -d 'tcp|http: tcp_chain or http_chain' complete -c lw -n '__lw_in preset aggregator' -l users -r -F -d 'credentials file (lw auth add-user): edges log in with SCRAM' complete -c lw -n '__lw_in preset aggregator' -l help -o h -d 'print the flags' + +complete -c lw -n '__lw_in config; and test (count (commandline -opc)) -eq 2' -a init -d 'Write the annotated default configuration, to edit and run' +complete -c lw -n '__lw_in config init' -l out -r -F -d 'file to write (default: ~/.config/logwisp/logwisp.toml)' +complete -c lw -n '__lw_in config init' -l help -o h -d 'print the flags' diff --git a/doc/cli.md b/doc/cli.md index 8d31bfd..aa07772 100644 --- a/doc/cli.md +++ b/doc/cli.md @@ -9,6 +9,7 @@ lw --dump [options] lw auth COMMAND [flags] lw tls COMMAND [flags] lw preset NAME [flags] +lw config init [--out FILE] lw help | -h | --help lw -V | --version ``` @@ -21,7 +22,9 @@ service. A [preset](#presets) builds a common pipeline from a few keys. `lw auth` manages SCRAM credentials and logs in to `scram` listeners ([below](#lw-auth)); `lw tls` makes a certificate authority and certificates -([below](#lw-tls)). A command is recognized only as the first argument. +([below](#lw-tls)); `lw config init` writes the annotated default +configuration ([below](#lw-config)). A command is recognized only as the +first argument. ## Options @@ -43,8 +46,7 @@ manages SCRAM credentials and logs in to `scram` listeners `--password-file`); configuration keys, spec keys and preset keys keep TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`, `lw preset edge --password_file F`). -- `lw auth`, `lw tls` and `lw preset` also take a long option after one dash - (`-user NAME`), as earlier releases documented. +- The commands also take a long option after one dash (`-user NAME`). Any scalar configuration key is settable as a flag using its TOML path; pipelines have [their own flags](#pipelines): @@ -540,3 +542,23 @@ lw tls ca --dir /etc/logwisp/pki lw tls cert --ca-dir /etc/logwisp/pki --name agg.example.org --server lw tls cert --ca-dir /etc/logwisp/pki --name edge-01 --client ``` + +## `lw config` + +``` +lw config init [--out FILE] +``` + +`init` writes the annotated configuration that ships as +`config/logwisp.toml`, every option with its default, to edit and run. It +never replaces a file (remove one to replace it). Without `--out` it writes +`~/.config/logwisp/logwisp.toml`, the file lw reads without `-c` unless a +`LOGWISP_CONFIG_*` variable names another; with that file in place, lw +without pipeline options runs its pipelines rather than filtering stdin. +Any other path is run with `-c FILE`; lw prints which applies. `lw --dump` prints the +effective configuration instead, flags and environment included. + +```bash +lw config init +sudo lw config init --out /usr/local/etc/logwisp/logwisp.toml +``` diff --git a/doc/installation.md b/doc/installation.md index 4c8bc78..5648ed8 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -42,7 +42,8 @@ The Makefile works with GNU make and BSD make alike. Targets: - Container - `make image`, `make image-check`: see [Container Image](#container-image) - Install - - `make install`, `make uninstall`, `make deb`: see [Installing](#installing) + - `make install`, `make uninstall`, `make deb`, `make arch`: see + [Installing](#installing) Variables: @@ -64,8 +65,9 @@ checkout. ## Installing `make install` copies what a package ships and compiles nothing, so build -first. It honours `DESTDIR`, `PREFIX`, `SYSCONFDIR` and `BINDIR` (default -`$PREFIX/bin`, the path the systemd unit and the rc.d script run `lw` from): +first. It honours `DESTDIR`, `PREFIX`, `SYSCONFDIR`, `BINDIR` (default +`$PREFIX/bin`, the path the systemd unit and the rc.d script run `lw` from) +and `SERVICE`: - Linux defaults: `PREFIX=/usr`, `SYSCONFDIR=/etc`. Outside a package manager, prefer `PREFIX=/usr/local`; systemd reads units, sysusers and @@ -73,6 +75,8 @@ first. It honours `DESTDIR`, `PREFIX`, `SYSCONFDIR` and `BINDIR` (default - FreeBSD defaults: `PREFIX=/usr/local`, `SYSCONFDIR=/usr/local/etc`. - `INSTALL_OS` (default: `uname -s`) picks the layout, so `INSTALL_OS=FreeBSD DESTDIR=...` stages the FreeBSD one on another system. +- `SERVICE`: `yes` (default) adds the service and its configuration; `no` + installs `lw` alone ([Without a Service](#without-a-service)). - `BASHCOMPDIR`, `ZSHCOMPDIR`, `FISHCOMPDIR`: where the shell completion goes, by default `$PREFIX/share/bash-completion/completions`, `$PREFIX/share/zsh/site-functions` (`/usr/share/zsh/vendor-completions` @@ -84,7 +88,7 @@ make release sudo make install PREFIX=/usr/local ``` -Every system gets: +Every install gets: - `$BINDIR/lw` and the manual `$PREFIX/share/man/man1/lw.1` - completion for `lw`, its commands, flags, files and preset names: @@ -92,10 +96,13 @@ Every system gets: - `$PREFIX/share/doc/logwisp/` (this documentation) and `$PREFIX/share/licenses/logwisp/LICENSE` +A service install also gets the service files below and, on Linux and +FreeBSD alike, `$SYSCONFDIR/logwisp/logwisp.toml`: the annotated sample +configuration, placed when none is there and otherwise kept. Edit it before +starting the service. + ### Linux (systemd) -- `$SYSCONFDIR/logwisp/logwisp.toml`: the annotated sample configuration, - kept when one is already there. Edit it before starting the service. - `$PREFIX/lib/systemd/system/logwisp.service`: runs `lw -c $SYSCONFDIR/logwisp/logwisp.toml` as the `logwisp` user, from `/var/lib/logwisp`; `systemctl reload` sends `SIGHUP`. @@ -133,11 +140,11 @@ sudo systemctl reload logwisp ### FreeBSD (rc.d) -- `$SYSCONFDIR/logwisp/logwisp.toml.sample`, copied to `logwisp.toml` when - none exists and `DESTDIR` is empty -- `$PREFIX/etc/rc.d/logwisp`: runs `lw` through `daemon(8)` as - `logwisp_user`, with its output in syslog under the tag `logwisp`; - `service logwisp reload` sends `SIGHUP` +`$PREFIX/etc/rc.d/logwisp` runs `lw` through `daemon(8)` as `logwisp_user`, +with its output in syslog under the tag `logwisp`; `service logwisp reload` +sends `SIGHUP`. The port in `deploy/package/freebsd/` places the +configuration the ports way, to the same effect: `@sample` installs +`logwisp.toml.sample` and copies it to `logwisp.toml` when none exists. ```bash sudo pw useradd logwisp -d /nonexistent -s /usr/sbin/nologin -c "LogWisp log transport" @@ -172,18 +179,93 @@ sudo systemctl enable --now logwisp - Installing creates the `logwisp` account and its directories where `systemd-sysusers` and `systemd-tmpfiles` exist; the service waits to be - enabled, and an upgrade restarts it if it runs. -- `/etc/logwisp/logwisp.toml` is a conffile: an upgrade keeps your edits, - and only a purge removes it. + enabled, and an upgrade or reinstall restarts it if it runs. +- `/etc/logwisp/logwisp.toml` is a conffile: an upgrade or reinstall keeps + your edits, and only a purge removes it. +- The same version again: `sudo apt install --reinstall ./bin/logwisp_*.deb`. - Removing stops and disables the service. The account, its directories and any credentials in `/etc/logwisp` stay, as with `make uninstall`. +### Arch Linux (makepkg) + +`make arch` builds the PKGBUILD in `deploy/package/arch/` from the working +tree, committed or not, into `bin/arch/`, then prints the `pacman -U` line +that installs the package. It needs `base-devel` and `go`, and runs as a +user: `makepkg` refuses root. + +```bash +make arch +sudo pacman -U bin/arch/logwisp--1-x86_64.pkg.tar.zst +sudo systemctl enable --now logwisp +``` + +- pacman's hooks create the `logwisp` account and its directories; the + service waits to be enabled. +- `/etc/logwisp/logwisp.toml` is a `backup` file: an upgrade or reinstall + keeps your edits, and a shipped version that changed lands beside it as + `logwisp.toml.pacnew`. `pacman -R` keeps an edited one as + `logwisp.toml.pacsave`. +- Reinstall: `make arch` and the same `pacman -U`. pacman does not restart + the service: `sudo systemctl restart logwisp`. +- An earlier `sudo make install` conflicts with the package + (`PREFIX=/usr`) or overrides it (`PREFIX=/usr/local`, whose unit and `lw` + come first): `sudo make uninstall` with that `PREFIX` first. The + configuration it keeps is no conflict: pacman keeps it, and when it was + edited, the shipped one lands beside it as `logwisp.toml.pacnew`. + +### Reinstalling + +Every method runs again over its own installation, for an upgrade or the +same version, and keeps an edited configuration: `make install` leaves an +existing `$SYSCONFDIR/logwisp/logwisp.toml` alone, and the packages treat it +as above. Only the `.deb` restarts a running service; otherwise restart it +(`sudo systemctl restart logwisp`, `sudo service logwisp restart`). To see +what a release changed in the shipped configuration: + +```bash +d=$(mktemp -d); lw config init --out "$d/logwisp.toml"; diff "$d/logwisp.toml" /etc/logwisp/logwisp.toml +``` + +### Without a Service + +`SERVICE=no` installs `lw`, its manual, completion and documentation, and +nothing else: no service files, account or configuration. For one user, +without root: + +```bash +make release +make install SERVICE=no PREFIX="$HOME/.local" +lw config init +``` + +- `~/.local/bin` goes on `PATH`. `man lw` then finds the manual, and bash + and fish the completion; zsh needs + `fpath=(~/.local/share/zsh/site-functions $fpath)` before `compinit`. +- `make uninstall PREFIX="$HOME/.local"` removes it. + +For every user: `sudo make install SERVICE=no PREFIX=/usr/local`. + +`lw config init` writes the shipped, annotated configuration, never over an +existing file, where you choose: + +- `lw config init`: `~/.config/logwisp/logwisp.toml`, which `lw` reads + without `-c` unless a `LOGWISP_CONFIG_*` variable names another file. With + it in place, `lw` without pipeline options runs its + pipelines instead of copying stdin to stdout; pipeline options and presets + still replace them. +- `sudo lw config init --out /usr/local/etc/logwisp/logwisp.toml`: a file + for every user, named with `-c`, or without it by + `LOGWISP_CONFIG_DIR=/usr/local/etc/logwisp` + ([File Location](configuration.md#file-location)). + +`lw --check` (with the same `-c`) then validates it. + ### Uninstall `sudo make uninstall` (with the `PREFIX` used to install) removes everything `make install` wrote except `$SYSCONFDIR/logwisp`, which holds configuration -and credentials. Remove that, the working directories and the account by -hand. +and credentials, and any file `lw config init` wrote. Remove those, the +working directories and the account by hand. ## Container Image @@ -269,15 +351,21 @@ docker run -d --name logwisp \ In place: -- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`, the manual, - shell completion and the service files in `deploy/package/` -- `make deb` +- `make install` with `DESTDIR`, `PREFIX`, `SYSCONFDIR` and `SERVICE`, the + manual, shell completion and the service files in `deploy/package/` +- `make deb`, and `make arch` building the PKGBUILD from the working tree - skeletons: `deploy/package/arch/PKGBUILD` and a `sysutils/logwisp` port in `deploy/package/freebsd/` -- `.github/workflows/package.yml`, on changes to them: `makepkg` on the - PKGBUILD in an Arch container, the `.deb` installed, started, removed and - purged on Ubuntu, BSD make building, installing and running the rc.d - service on FreeBSD, and `make image image-check` +- `.github/workflows/package.yml`, on changes to them: + - Arch container: `make arch`, installed over an edited configuration, + reinstalled keeping it, removed + - Ubuntu: the `.deb` installed, started, reinstalled keeping an edited + configuration, removed and purged + - Ubuntu, without a service: `make install SERVICE=no` into a home twice, + then `lw config init` writing the file `lw` reads, once + - FreeBSD: BSD make building, installing twice keeping an edited + configuration, and running the rc.d service + - `make image image-check` Still missing: release tags, which both skeletons download; submitting the AUR package and the port; a Debian source package for a PPA or the archive. diff --git a/doc/lw.1 b/doc/lw.1 index e3695fd..652f7b8 100644 --- a/doc/lw.1 +++ b/doc/lw.1 @@ -20,7 +20,7 @@ .Op Fl \-rate\-limit Ar spec .Op Fl \-heartbeat Ar spec .Nm -.Cm auth | tls | preset +.Cm auth | tls | preset | config .Ar command .Op Ar flags .Nm @@ -124,12 +124,7 @@ The options of join words with .Ql - ; a configuration or preset key keeps its TOML spelling. -The -.Cm auth , -.Cm tls -and -.Cm preset -commands also take a long option after a single dash. +The commands also take a long option after a single dash. .Pp The options are as follows: .Bl -tag -width Ds @@ -549,6 +544,19 @@ An IPv6 .Ar host is written in brackets, as in .Li [2001:db8::1]:8443 . +.Ss Configuration command +.Bl -tag -width Ds +.It Cm config init Op Fl \-out Ar file +Write the annotated configuration +.Nm +ships, every option with its default, to +.Ar file , +by default +.Pa ~/.config/logwisp/logwisp.toml , +the first file in +.Sx FILES . +An existing file is never replaced. +.El .Sh ENVIRONMENT .Bl -tag -width Ds .It Ev LOGWISP_ Ns Ar KEY @@ -611,7 +619,11 @@ reads the first of these that exists: .Pa ./logwisp.toml .El .Pp -Packages install a sample configuration and service files that pass it with +.Nm +.Cm config init +writes the first. +A service installation places the annotated configuration, keeping one +already there, and service files that pass it with .Fl c : .Bl -tag -width Ds .It Pa /etc/logwisp/logwisp.toml @@ -620,9 +632,7 @@ The configuration on Linux, used by .It Pa /usr/local/etc/logwisp/logwisp.toml The configuration on .Fx , -installed as -.Pa logwisp.toml.sample -and used by +used by .Pa /usr/local/etc/rc.d/logwisp . .It Pa /var/lib/logwisp , /var/db/logwisp The service's working directory on Linux and on @@ -668,9 +678,10 @@ the shell reports status 141. .Pp .Nm .Cm auth , -.Cm tls -and +.Cm tls , .Cm preset +and +.Cm config do the same: 0 on success or help, 1 on failure, and 2 on a usage error. .Sh SIGNALS .Bl -tag -width "SIGINT, SIGTERM" @@ -749,6 +760,16 @@ edge# lw \-\-preset edge,path=/var/log/app,to=agg.example.org:9000,\e ca=/etc/logwisp/pki/ca.crt,user=edge-01,password_file=/root/edge-01.pass .Ed .Pp +Write the annotated configuration to the file +.Nm +reads without +.Fl c , +edit it, then check it: +.Bd -literal -offset indent +$ lw config init +$ lw \-\-check +.Ed +.Pp Check a configuration without binding a port or touching a log file: .Bd -literal -offset indent $ lw \-\-check \-c /etc/logwisp/logwisp.toml diff --git a/doc/todo.md b/doc/todo.md index e6fc9b3..76892b3 100644 --- a/doc/todo.md +++ b/doc/todo.md @@ -11,15 +11,18 @@ The foundation exists: 24.04 and 26.04, and the FreeBSD 15.1 ports tree. - The canonical module path `github.com/lixenwraith/logwisp`, which `go install`, FreeBSD's `USES=go:modules` and Debian's dh-golang expect. -- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`: the binary, the - `doc/lw.1` manual, bash, zsh and fish completion generated from `cmd/lw`'s - `shorts` and `commands` tables, and the service files in `deploy/package/` - (systemd unit, sysusers, tmpfiles, FreeBSD rc.d script). -- `make deb`, a binary package built with `dpkg-deb`. +- `make install` with `DESTDIR`, `PREFIX`, `SYSCONFDIR` and `SERVICE`: the + binary, the `doc/lw.1` manual, bash, zsh and fish completion generated from + `cmd/lw`'s `shorts` and `commands` tables, and the service files in + `deploy/package/` (systemd unit, sysusers, tmpfiles, FreeBSD rc.d script) + with the configuration, kept on reinstall. +- `make deb`, a binary package built with `dpkg-deb`, and `make arch`, the + PKGBUILD built from the working tree. - Skeletons in `deploy/package/arch/` and `deploy/package/freebsd/`. -- `.github/workflows/package.yml`: `makepkg` on the PKGBUILD, the `.deb` - installed, started and purged, BSD make and the rc.d service on FreeBSD - 15.1, and `make image image-check`. +- `.github/workflows/package.yml`: `make arch`, the `.deb` and BSD make on + FreeBSD 15.1, each reinstalled over an edited configuration; the systemd + and rc.d services run; a home install with `SERVICE=no` and + `lw config init`; and `make image image-check`. What remains is the maintainer's: it needs push rights, an identity or a release tag, and both skeletons download the tagged source. diff --git a/internal/config/loader.go b/internal/config/loader.go index 248f400..fb92ca1 100644 --- a/internal/config/loader.go +++ b/internal/config/loader.go @@ -219,11 +219,25 @@ func resolveConfigPath(file string) (path string, isExplicit bool) { if configDir := os.Getenv("LOGWISP_CONFIG_DIR"); configDir != "" { return filepath.Join(configDir, "logwisp.toml"), true } - if homeDir, err := os.UserHomeDir(); err == nil { - path = filepath.Join(homeDir, ".config", "logwisp", "logwisp.toml") + if path, err := UserFile(); err == nil { if _, err := os.Stat(path); err == nil { return path, false } } return "logwisp.toml", false } + +// DefaultPath is the file lw reads without -c +func DefaultPath() string { + path, _ := resolveConfigPath("") + return path +} + +// UserFile is ~/.config/logwisp/logwisp.toml, the file lw reads first without -c +func UserFile() (string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, ".config", "logwisp", "logwisp.toml"), nil +}