v0.27.0 packaging update
This commit is contained in:
1 parent
b5ac9084c7
commit
60abc97eb0
17 files changed
+1194
-43
No files matched your search
@@ -0,0 +1,102 @@
|
||||
name: Package
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: &paths
|
||||
- Makefile
|
||||
- Dockerfile
|
||||
- go.mod
|
||||
- go.sum
|
||||
- config/logwisp.toml
|
||||
- doc/lw.1
|
||||
- deploy/package/**
|
||||
- .github/workflows/package.yml
|
||||
pull_request:
|
||||
paths: *paths
|
||||
workflow_dispatch:
|
||||
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.
|
||||
arch:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
container: archlinux:base-devel
|
||||
steps:
|
||||
- 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
|
||||
- run: |
|
||||
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 -R --noconfirm logwisp
|
||||
test ! -e /usr/bin/lw
|
||||
# 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
|
||||
deb:
|
||||
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 deb
|
||||
- run: |
|
||||
sudo apt-get install -y ./bin/logwisp_*.deb
|
||||
lw --version
|
||||
test -f /usr/share/zsh/vendor-completions/_lw
|
||||
sudo systemctl enable --now logwisp
|
||||
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
|
||||
test ! -e /etc/systemd/system/multi-user.target.wants/logwisp.service
|
||||
if systemctl is-active --quiet logwisp; then exit 1; fi
|
||||
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
|
||||
freebsd:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: vmactions/freebsd-vm@c46abacb49f09938ca4e1702d15d836285d694cc # v1.5.9
|
||||
with:
|
||||
release: "15.1"
|
||||
copyback: false
|
||||
prepare: pkg install -y go127
|
||||
run: |
|
||||
set -e
|
||||
make release
|
||||
make install
|
||||
lw --version
|
||||
pw groupadd logwisp
|
||||
pw useradd logwisp -g logwisp -d /nonexistent -s /usr/sbin/nologin
|
||||
service logwisp onestart
|
||||
sleep 3
|
||||
service logwisp onestatus
|
||||
service logwisp onestop
|
||||
make uninstall
|
||||
test ! -e /usr/local/bin/lw
|
||||
image:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- run: make image image-check
|
||||
@@ -56,10 +56,17 @@ BINDIR ?= $(PREFIX)/bin
|
||||
MANDIR ?= $(PREFIX)/share/man
|
||||
DOCDIR ?= $(PREFIX)/share/doc/logwisp
|
||||
LICENSEDIR ?= $(PREFIX)/share/licenses/logwisp
|
||||
BASHCOMPDIR ?= $(PREFIX)/share/bash-completion/completions
|
||||
# Debian's zsh reads /usr/share/zsh/vendor-completions, not site-functions
|
||||
ZSHCOMPDIR_DEFAULT != [ '$(PREFIX)' = /usr ] && [ -f /etc/debian_version ] && \
|
||||
echo /usr/share/zsh/vendor-completions || echo '$(PREFIX)/share/zsh/site-functions'
|
||||
ZSHCOMPDIR ?= $(ZSHCOMPDIR_DEFAULT)
|
||||
FISHCOMPDIR ?= $(PREFIX)/share/fish/vendor_completions.d
|
||||
DEB_MAINTAINER ?= Maintainer <maintainer@example.org>
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
.PHONY: help check-go build release dev test verify e2e image image-check install uninstall clean version
|
||||
.PHONY: help check-go build release dev test verify e2e image image-check install uninstall deb completion clean version
|
||||
|
||||
help:
|
||||
@echo "Usage: make <target> [VARIABLE=value ...]"
|
||||
@@ -69,6 +76,7 @@ help:
|
||||
@echo " release Build it static, stripped and trimmed (CGO_ENABLED=0)"
|
||||
@echo " dev Build it with the race detector"
|
||||
@echo " version Print the version metadata a build embeds"
|
||||
@echo " completion Regenerate $(PKG_DIR)/completion from lw's command tables"
|
||||
@echo " clean Remove $(BIN_DIR)/"
|
||||
@echo "Check:"
|
||||
@echo " test Run the Go tests"
|
||||
@@ -79,13 +87,16 @@ 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, licence and docs"
|
||||
@echo " install Binary, manual, sample config, service files, shell completion,"
|
||||
@echo " licence and docs"
|
||||
@echo " uninstall Remove them, keeping $(SYSCONFDIR)/logwisp"
|
||||
@echo " deb Package a release build as $(BIN_DIR)/logwisp_VERSION_ARCH.deb (dpkg-deb)"
|
||||
@echo ""
|
||||
@echo "Variables: GO=$(GO) PREFIX=$(PREFIX) SYSCONFDIR=$(SYSCONFDIR) DESTDIR=$(DESTDIR)"
|
||||
@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"
|
||||
@echo " CONTAINER_ENGINE, IMAGE, IMAGE_TAG, BUILD_CA, IMAGE_BUILD_FLAGS, IMAGE_CHECK_CONFIG,"
|
||||
@echo " BASHCOMPDIR, ZSHCOMPDIR, FISHCOMPDIR, DEB_MAINTAINER"
|
||||
|
||||
check-go:
|
||||
@command -v $(GO) >/dev/null 2>&1 || { \
|
||||
@@ -108,6 +119,10 @@ dev: check-go
|
||||
test: check-go
|
||||
$(GO) test ./...
|
||||
|
||||
# A test fails when the committed scripts differ from what the tables generate
|
||||
completion: check-go
|
||||
$(GO) test ./cmd/lw -run '^TestCompletionScriptsAreCurrent$$' -args -update
|
||||
|
||||
# gofmt checks the Go files changed since the branch forked from GOFMT_BASE,
|
||||
# exact renames aside, or the whole tree without git or that ref: older files
|
||||
# predate the gate, and AGENTS.md formats what a change touches.
|
||||
@@ -189,6 +204,9 @@ install:
|
||||
root='$(DESTDIR)'; etc="$$root$(SYSCONFDIR)/logwisp"; \
|
||||
put 0755 $(BIN_DIR)/$(BINARY) "$$root$(BINDIR)/$(BINARY)"; \
|
||||
put 0644 doc/lw.1 "$$root$(MANDIR)/man1/lw.1"; \
|
||||
put 0644 $(PKG_DIR)/completion/lw.bash "$$root$(BASHCOMPDIR)/lw"; \
|
||||
put 0644 $(PKG_DIR)/completion/_lw "$$root$(ZSHCOMPDIR)/_lw"; \
|
||||
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; \
|
||||
case '$(INSTALL_OS)' in \
|
||||
@@ -207,6 +225,7 @@ install:
|
||||
uninstall:
|
||||
@set -eu; root='$(DESTDIR)'; \
|
||||
for f in "$$root$(BINDIR)/$(BINARY)" "$$root$(MANDIR)/man1/lw.1" \
|
||||
"$$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 \
|
||||
@@ -218,6 +237,37 @@ uninstall:
|
||||
done; \
|
||||
echo "keep $$root$(SYSCONFDIR)/logwisp (configuration and credentials)"
|
||||
|
||||
# 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
|
||||
# by build time, so each build upgrades the last.
|
||||
deb: release
|
||||
@set -eu; stage=$(BIN_DIR)/deb; doc="$$stage/usr/share/doc/logwisp"; rm -rf "$$stage"; \
|
||||
$(MAKE) install DESTDIR="$$stage" PREFIX=/usr SYSCONFDIR=/etc INSTALL_OS=Linux \
|
||||
LICENSEDIR=/usr/share/doc/logwisp ZSHCOMPDIR=/usr/share/zsh/vendor-completions >/dev/null; \
|
||||
meta=$$($(GO) version -m $(BIN_DIR)/$(BINARY)); \
|
||||
case $$meta in *GOOS=linux*) ;; *) echo "$(BIN_DIR)/$(BINARY) is not a Linux build" >&2; exit 1 ;; esac; \
|
||||
arch=$$(echo "$$meta" | sed -n 's/^[[:space:]]*build[[:space:]]*GOARCH=//p'); \
|
||||
case $$arch in 386) arch=i386 ;; arm) arch=armhf ;; ppc64le) arch=ppc64el ;; \
|
||||
mipsle) arch=mipsel ;; mips64le) arch=mips64el ;; esac; \
|
||||
stamp=$$(echo '$(BUILD_TIME)' | tr -dc 0-9); \
|
||||
version=$$(echo '$(VERSION)' | sed -e 's/^v//' -e 's/-dirty$$/+dirty/' \
|
||||
-e 's/-\([0-9][0-9]*\)-g/+\1.g/' -e 's/-/~/g'); \
|
||||
case $$version in \
|
||||
[0-9]*.*+dirty) version="$$version.$$stamp" ;; \
|
||||
[0-9]*.*) ;; \
|
||||
*) version="0~$$stamp.$$version" ;; \
|
||||
esac; \
|
||||
mv "$$doc/LICENSE" "$$doc/copyright"; gzip -9n "$$stage/usr/share/man/man1/lw.1"; \
|
||||
install -d -m 0755 "$$stage/DEBIAN"; \
|
||||
for f in postinst prerm postrm; do install -m 0755 $(PKG_DIR)/deb/$$f "$$stage/DEBIAN/$$f"; done; \
|
||||
echo /etc/logwisp/logwisp.toml > "$$stage/DEBIAN/conffiles"; \
|
||||
(cd "$$stage" && find usr -type f -exec md5sum {} + | LC_ALL=C sort -k 2) > "$$stage/DEBIAN/md5sums"; \
|
||||
size=$$(du -sk --apparent-size "$$stage/usr" "$$stage/etc" | awk '{ s += $$1 } END { print s }'); \
|
||||
{ printf 'Package: logwisp\nVersion: %s\nArchitecture: %s\nMaintainer: %s\nInstalled-Size: %s\n' \
|
||||
"$$version" "$$arch" "$(DEB_MAINTAINER)" "$$size"; cat $(PKG_DIR)/deb/control; \
|
||||
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"
|
||||
|
||||
clean:
|
||||
rm -rf $(BIN_DIR)
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ authentication designs for the rationale and what is deliberately left out.
|
||||
exit codes, `lw auth`; also the `lw(1)` manual, [`doc/lw.1`](doc/lw.1)
|
||||
- [Operations](doc/operations.md): running, monitoring, tuning,
|
||||
troubleshooting
|
||||
- [To Do](doc/todo.md): planned work: packaging
|
||||
- [To Do](doc/todo.md): planned work: packaging, `lw --tui` and its shared configuration engine
|
||||
|
||||
A fully annotated configuration covering every option ships as
|
||||
[`config/logwisp.toml`](config/logwisp.toml).
|
||||
@@ -185,9 +185,9 @@ docker run --rm --read-only --cap-drop ALL --security-opt no-new-privileges \
|
||||
-v /etc/logwisp:/etc/logwisp:ro logwisp:dev -c /etc/logwisp/logwisp.toml
|
||||
```
|
||||
|
||||
`sudo make install` installs the binary, the manual, a sample configuration
|
||||
and a systemd unit or FreeBSD rc.d script; see
|
||||
[Installation](doc/installation.md).
|
||||
`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).
|
||||
|
||||
## System Requirements
|
||||
|
||||
|
||||
+24
-12
@@ -136,18 +136,7 @@ func TestHelpAndManualListEveryOption(t *testing.T) {
|
||||
}
|
||||
var help bytes.Buffer
|
||||
printHelp(&help)
|
||||
options := map[string]string{} // long -> its short, if lw takes one
|
||||
for _, long := range config.SpecFlags() {
|
||||
options[long] = ""
|
||||
}
|
||||
for long := range config.Settings() {
|
||||
options[long] = ""
|
||||
}
|
||||
for letter, short := range shorts {
|
||||
if short.top {
|
||||
options[short.long] = letter
|
||||
}
|
||||
}
|
||||
options := options()
|
||||
// mdoc's Fl writes the first dash, and the others as \-
|
||||
fl := func(flag string) string { return regexp.QuoteMeta("Fl " + strings.ReplaceAll(flag[1:], "-", `\-`)) }
|
||||
for long, letter := range options {
|
||||
@@ -271,3 +260,26 @@ func TestTLSCommandsIssueAVerifiableChain(t *testing.T) {
|
||||
t.Fatalf("second ca: exit %d: %s", code, stderr)
|
||||
}
|
||||
}
|
||||
|
||||
var update = flag.Bool("update", false, "rewrite the completion scripts: make completion")
|
||||
|
||||
// The installed completion scripts are what lw's tables generate
|
||||
func TestCompletionScriptsAreCurrent(t *testing.T) {
|
||||
for _, sh := range shells {
|
||||
script := sh.script()
|
||||
path := filepath.Join("../../deploy/package/completion", sh.file)
|
||||
if *update {
|
||||
if err := os.WriteFile(path, script, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
have, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(have, script) {
|
||||
t.Errorf("%s is not what lw's tables generate: run make completion", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/lixenwraith/logwisp/internal/config"
|
||||
)
|
||||
|
||||
// compFlag is a flag as a shell completes it: value is its row in a shell's
|
||||
// values, "" for a switch; repeat lets a shell offer it again.
|
||||
type compFlag struct {
|
||||
long, short, value, help string
|
||||
repeat bool
|
||||
}
|
||||
|
||||
type compCommand struct {
|
||||
name, summary string
|
||||
subs []compSub
|
||||
}
|
||||
|
||||
type compSub struct {
|
||||
name, summary string
|
||||
flags []compFlag
|
||||
}
|
||||
|
||||
// shell is a completion script make completion writes under
|
||||
// deploy/package/completion: values completes a flag's value by its kind,
|
||||
// "text" as the shell would without lw's script, PRESETS the preset names.
|
||||
type shell struct {
|
||||
file string
|
||||
values map[string]string
|
||||
write func(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string)
|
||||
}
|
||||
|
||||
var shells = []shell{
|
||||
{"lw.bash", map[string]string{
|
||||
"text": "COMPREPLY=()",
|
||||
"dir": "_lw_reply -d",
|
||||
"preset": "mapfile -t COMPREPLY < <(compgen -W 'PRESETS' -- \"$cur\")",
|
||||
}, writeBash},
|
||||
{"_lw", map[string]string{
|
||||
"text": ":value:_default",
|
||||
"dir": ":directory:_files -/",
|
||||
"preset": ":preset:(PRESETS)",
|
||||
}, writeZsh},
|
||||
{"lw.fish", map[string]string{
|
||||
"text": "-r -F",
|
||||
"dir": "-x -a '(__fish_complete_directories)'",
|
||||
"preset": "-x -a 'PRESETS'",
|
||||
}, writeFish},
|
||||
}
|
||||
|
||||
// valueOf is the kind of value an option of lw's own takes when it is not
|
||||
// text: "" for none, as a bare --color means always.
|
||||
var valueOf = map[string]string{"preset": "preset", "logging.file.directory": "dir", "color": "", "help": ""}
|
||||
|
||||
const generated = "Generated by make completion from lw's own tables: edit cmd/lw, not this file."
|
||||
|
||||
// script is the shell's completion for lw as its tables stand
|
||||
func (s shell) script() []byte {
|
||||
top, cmds, presets := completionModel()
|
||||
values := map[string]string{}
|
||||
for kind, action := range s.values {
|
||||
values[kind] = strings.ReplaceAll(action, "PRESETS", presets)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
s.write(&b, top, cmds, values)
|
||||
return b.Bytes()
|
||||
}
|
||||
|
||||
// options maps each long option lw itself takes to its short letter, if any
|
||||
func options() map[string]string {
|
||||
options := map[string]string{}
|
||||
for _, long := range config.SpecFlags() {
|
||||
options[long] = ""
|
||||
}
|
||||
for long := range config.Settings() {
|
||||
options[long] = ""
|
||||
}
|
||||
for letter, short := range shorts {
|
||||
if short.top {
|
||||
options[short.long] = letter
|
||||
}
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
// completionModel reads the parser's tables: lw's options, then each
|
||||
// command's subcommands with the flags their define registers.
|
||||
func completionModel() (top []compFlag, cmds []compCommand, presets string) {
|
||||
settings, specFlags, shortOf := config.Settings(), config.SpecFlags(), options()
|
||||
for _, long := range slices.Sorted(maps.Keys(shortOf)) {
|
||||
f := compFlag{long: long, short: shortOf[long], value: "text", repeat: slices.Contains(specFlags, long)}
|
||||
if v, ok := valueOf[long]; ok {
|
||||
f.value = v
|
||||
} else if settings[long] {
|
||||
f.value = ""
|
||||
}
|
||||
top = append(top, f)
|
||||
}
|
||||
for _, c := range commands {
|
||||
cmd := compCommand{name: c.name, summary: c.summary}
|
||||
for _, s := range c.subcommands {
|
||||
fs := flag.NewFlagSet(s.name, flag.ContinueOnError)
|
||||
s.define(fs)
|
||||
sub := compSub{name: s.name, summary: s.summary}
|
||||
fs.VisitAll(func(f *flag.Flag) {
|
||||
if f.Usage == "" {
|
||||
return // an alias, unlisted like a short
|
||||
}
|
||||
arg, help := flag.UnquoteUsage(f)
|
||||
value := arg // "" for a switch; a backquoted `dir` names its row
|
||||
if _, known := shells[0].values[arg]; arg != "" && !known {
|
||||
value = "text"
|
||||
}
|
||||
sub.flags = append(sub.flags, compFlag{long: f.Name, short: shortFor(f.Name), value: value, help: help})
|
||||
})
|
||||
// flag answers -h and --help itself
|
||||
sub.flags = append(sub.flags, compFlag{long: "help", short: shortFor("help"), help: "print the flags"})
|
||||
cmd.subs = append(cmd.subs, sub)
|
||||
}
|
||||
cmds = append(cmds, cmd)
|
||||
}
|
||||
var names []string
|
||||
for _, p := range config.Presets() {
|
||||
names = append(names, p.Name)
|
||||
}
|
||||
return top, cmds, strings.Join(names, " ")
|
||||
}
|
||||
|
||||
func shortFor(long string) string {
|
||||
for letter, short := range shorts {
|
||||
if short.long == long {
|
||||
return letter
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// words lists the flags as typed: --long, then -s for a short
|
||||
func words(flags []compFlag, dashes ...string) string {
|
||||
var w []string
|
||||
for _, f := range flags {
|
||||
for _, d := range dashes {
|
||||
w = append(w, d+f.long)
|
||||
}
|
||||
if f.short != "" {
|
||||
w = append(w, "-"+f.short)
|
||||
}
|
||||
}
|
||||
return strings.Join(w, " ")
|
||||
}
|
||||
|
||||
// bashValues is a case statement completing each valued flag's argument
|
||||
func bashValues(w io.Writer, indent string, flags []compFlag, values map[string]string, dashes ...string) {
|
||||
byValue := map[string][]string{}
|
||||
for _, f := range flags {
|
||||
if f.value != "" {
|
||||
byValue[f.value] = append(byValue[f.value], strings.Fields(words([]compFlag{f}, dashes...))...)
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(w, "%scase $prev in\n", indent)
|
||||
for _, v := range slices.Sorted(maps.Keys(byValue)) {
|
||||
fmt.Fprintf(w, "%s%s) %s; return ;;\n", indent, strings.Join(byValue[v], "|"), values[v])
|
||||
}
|
||||
fmt.Fprintf(w, "%sesac\n", indent)
|
||||
}
|
||||
|
||||
func writeBash(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
|
||||
var names []string
|
||||
for _, c := range cmds {
|
||||
names = append(names, c.name)
|
||||
}
|
||||
fmt.Fprintf(w, "# bash completion for lw. %s\n\n", generated)
|
||||
fmt.Fprintf(w, "_lw_reply() { compopt -o filenames 2>/dev/null; mapfile -t COMPREPLY < <(compgen \"$@\" -- \"$cur\"); }\n\n")
|
||||
fmt.Fprintf(w, "_lw() {\n")
|
||||
fmt.Fprintf(w, "\tlocal cur=${COMP_WORDS[COMP_CWORD]} prev='' i=$COMP_CWORD words\n")
|
||||
fmt.Fprintf(w, "\t((i > 0)) && prev=${COMP_WORDS[i-1]}\n")
|
||||
fmt.Fprintf(w, "\t# bash splits --key=value at '=': complete the value of the key before it\n")
|
||||
fmt.Fprintf(w, "\tif [[ $cur == = ]]; then cur=''\n\telif [[ $prev == = ]]; then prev=${COMP_WORDS[i-2]}\n\tfi\n")
|
||||
fmt.Fprintf(w, "\tcase ${COMP_WORDS[1]}:$i in\n")
|
||||
fmt.Fprintf(w, "\t*:1) words='%s %s help' ;;\n", words(top, "--"), strings.Join(names, " "))
|
||||
for _, c := range cmds {
|
||||
var subs []string
|
||||
for _, s := range c.subs {
|
||||
subs = append(subs, s.name)
|
||||
}
|
||||
fmt.Fprintf(w, "\t%s:2) words='%s' ;;\n", c.name, strings.Join(subs, " "))
|
||||
fmt.Fprintf(w, "\t%s:*)\n\t\tcase ${COMP_WORDS[2]} in\n", c.name)
|
||||
for _, s := range c.subs {
|
||||
fmt.Fprintf(w, "\t\t%s)\n", s.name)
|
||||
bashValues(w, "\t\t\t", s.flags, values, "--", "-")
|
||||
fmt.Fprintf(w, "\t\t\twords='%s' ;;\n", words(s.flags, "--"))
|
||||
}
|
||||
fmt.Fprintf(w, "\t\tesac ;;\n")
|
||||
}
|
||||
fmt.Fprintf(w, "\t*)\n")
|
||||
bashValues(w, "\t\t", top, values, "--")
|
||||
fmt.Fprintf(w, "\t\twords='%s' ;;\n", words(top, "--"))
|
||||
fmt.Fprintf(w, "\tesac\n")
|
||||
fmt.Fprintf(w, "\tmapfile -t COMPREPLY < <(compgen -W \"$words\" -- \"$cur\")\n}\n\n")
|
||||
fmt.Fprintf(w, "# An empty reply falls back to file names, as without this script\n")
|
||||
fmt.Fprintf(w, "complete -o bashdefault -o default -F _lw lw\n")
|
||||
}
|
||||
|
||||
// zshQuote makes help text safe inside a single-quoted _arguments spec
|
||||
func zshQuote(s string) string {
|
||||
return strings.NewReplacer("'", `'\''`, "]", `\]`).Replace(s)
|
||||
}
|
||||
|
||||
// zshSpecs are _arguments specs for flags; a value follows '=' or as the next
|
||||
// word, never attached to a short, the forms lw parses.
|
||||
func zshSpecs(flags []compFlag, values map[string]string) []string {
|
||||
var specs []string
|
||||
for _, f := range flags {
|
||||
help, action, eq := "", "", ""
|
||||
if f.help != "" {
|
||||
help = "[" + zshQuote(f.help) + "]"
|
||||
}
|
||||
if f.value != "" {
|
||||
action, eq = values[f.value], "="
|
||||
}
|
||||
names, repeat := fmt.Sprintf("'(-%s --%s)'{-%s%s,--%s%s}", f.short, f.long, f.short, eq, f.long, eq), ""
|
||||
if f.repeat {
|
||||
names, repeat = fmt.Sprintf("'*'{-%s%s,--%s%s}", f.short, eq, f.long, eq), "*"
|
||||
}
|
||||
switch tail := help + action; {
|
||||
case f.short == "":
|
||||
specs = append(specs, fmt.Sprintf("'%s--%s%s%s'", repeat, f.long, eq, tail))
|
||||
case tail == "":
|
||||
specs = append(specs, names)
|
||||
default:
|
||||
specs = append(specs, names+"'"+tail+"'")
|
||||
}
|
||||
}
|
||||
return specs
|
||||
}
|
||||
|
||||
func writeZsh(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
|
||||
fmt.Fprintf(w, "#compdef lw\n# zsh completion for lw. %s\n\n", generated)
|
||||
fmt.Fprintf(w, "local -a described\n")
|
||||
fmt.Fprintf(w, "case ${words[2]}:$CURRENT in\n")
|
||||
for _, c := range cmds {
|
||||
fmt.Fprintf(w, "%s:2) ;;\n%s:3)\n\tdescribed=(\n", c.name, c.name)
|
||||
for _, s := range c.subs {
|
||||
fmt.Fprintf(w, "\t\t'%s:%s'\n", s.name, zshQuote(s.summary))
|
||||
}
|
||||
fmt.Fprintf(w, "\t)\n\t_describe -t commands 'lw %s command' described\n\treturn ;;\n", c.name)
|
||||
// _arguments reads words from the second: make that the subcommand's first flag
|
||||
fmt.Fprintf(w, "%s:*)\n\tshift 2 words\n\t(( CURRENT -= 2 ))\n\tcase ${words[1]} in\n", c.name)
|
||||
for _, s := range c.subs {
|
||||
fmt.Fprintf(w, "\t%s) _arguments -S \\\n\t\t%s ;;\n", s.name, strings.Join(zshSpecs(s.flags, values), " \\\n\t\t"))
|
||||
}
|
||||
fmt.Fprintf(w, "\tesac\n\treturn ;;\n")
|
||||
}
|
||||
fmt.Fprintf(w, "esac\n")
|
||||
fmt.Fprintf(w, "if (( CURRENT == 2 )) && [[ $PREFIX != -* ]]; then\n\tdescribed=(\n")
|
||||
for _, c := range cmds {
|
||||
fmt.Fprintf(w, "\t\t'%s:%s'\n", c.name, zshQuote(c.summary))
|
||||
}
|
||||
fmt.Fprintf(w, "\t\t'help:Print the help and exit'\n\t)\n")
|
||||
fmt.Fprintf(w, "\t_describe -t commands 'lw command' described\n\treturn\nfi\n")
|
||||
fmt.Fprintf(w, "_arguments -S \\\n\t%s\n", strings.Join(zshSpecs(top, values), " \\\n\t"))
|
||||
}
|
||||
|
||||
// fishQuote makes text safe inside a single-quoted fish string
|
||||
func fishQuote(s string) string {
|
||||
return strings.NewReplacer(`\`, `\\`, "'", `\'`).Replace(s)
|
||||
}
|
||||
|
||||
// fishFlags completes a short as an old-style option (-o): lw takes no
|
||||
// bundled shorts and no value attached to one.
|
||||
func fishFlags(w io.Writer, cond string, flags []compFlag, values map[string]string) {
|
||||
for _, f := range flags {
|
||||
line := fmt.Sprintf("complete -c lw -n '%s' -l %s", cond, f.long)
|
||||
if f.short != "" {
|
||||
line += " -o " + f.short
|
||||
}
|
||||
if f.value != "" {
|
||||
line += " " + values[f.value]
|
||||
}
|
||||
if f.help != "" {
|
||||
line += " -d '" + fishQuote(f.help) + "'"
|
||||
}
|
||||
fmt.Fprintln(w, line)
|
||||
}
|
||||
}
|
||||
|
||||
func writeFish(w io.Writer, top []compFlag, cmds []compCommand, values map[string]string) {
|
||||
fmt.Fprintf(w, "# fish completion for lw. %s\n\n", generated)
|
||||
fmt.Fprintf(w, "# __lw_in WORD...: the command line so far is lw WORD...\n")
|
||||
fmt.Fprintf(w, "function __lw_in\n\tset -l w (commandline -opc)\n\tset -l n (math (count $argv) + 1)\n")
|
||||
fmt.Fprintf(w, "\ttest (count $w) -ge $n; and test \"$w[2..$n]\" = \"$argv\"\nend\n\n")
|
||||
fmt.Fprintf(w, "complete -c lw -f\n")
|
||||
fmt.Fprintf(w, "set -l __lw_first 'test (count (commandline -opc)) -eq 1'\n")
|
||||
for _, c := range cmds {
|
||||
fmt.Fprintf(w, "complete -c lw -n $__lw_first -a %s -d '%s'\n", c.name, fishQuote(c.summary))
|
||||
}
|
||||
fmt.Fprintf(w, "complete -c lw -n $__lw_first -a help -d 'Print the help and exit'\n")
|
||||
var cond []string
|
||||
for _, c := range cmds {
|
||||
cond = append(cond, "__lw_in "+c.name)
|
||||
}
|
||||
fishFlags(w, "not "+strings.Join(cond, "; and not "), top, values)
|
||||
for _, c := range cmds {
|
||||
fmt.Fprintf(w, "\n")
|
||||
for _, s := range c.subs {
|
||||
fmt.Fprintf(w, "complete -c lw -n '__lw_in %s; and test (count (commandline -opc)) -eq 2' -a %s -d '%s'\n",
|
||||
c.name, s.name, fishQuote(s.summary))
|
||||
}
|
||||
for _, s := range c.subs {
|
||||
fishFlags(w, "__lw_in "+c.name+" "+s.name, s.flags, values)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
#compdef lw
|
||||
# zsh completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
|
||||
|
||||
local -a described
|
||||
case ${words[2]}:$CURRENT in
|
||||
auth:2) ;;
|
||||
auth:3)
|
||||
described=(
|
||||
'add-user:Add a user to a credentials file, or replace its password'
|
||||
'remove-user:Remove a user from a credentials file'
|
||||
'token:Log in to an http sink or http_chain source and print a bearer token'
|
||||
'stream:Log in to a tcp sink and copy its stream to stdout until interrupted'
|
||||
)
|
||||
_describe -t commands 'lw auth command' described
|
||||
return ;;
|
||||
auth:*)
|
||||
shift 2 words
|
||||
(( CURRENT -= 2 ))
|
||||
case ${words[1]} in
|
||||
add-user) _arguments -S \
|
||||
'--credentials=[credentials file, created when missing]:value:_default' \
|
||||
'--generate[generate a new password, replacing any --password-file; needed to rotate without one]' \
|
||||
'--password-file=[read the password from file when it exists, else write a generated one there]:value:_default' \
|
||||
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
remove-user) _arguments -S \
|
||||
'--credentials=[credentials file]:value:_default' \
|
||||
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
token) _arguments -S \
|
||||
'--ca-file=[CA file that verifies the server (default: system roots)]:value:_default' \
|
||||
'--cert-file=[client certificate file, for listeners with tls.client_auth]:value:_default' \
|
||||
'--key-file=[client key file]:value:_default' \
|
||||
'--password-file=[file holding the password]:value:_default' \
|
||||
'--pin-sha256=[sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)]:value:_default' \
|
||||
'--server-name=[name the server certificate must carry (default: the host)]:value:_default' \
|
||||
'--unbound[log in without channel binding, to an http sink behind a TLS-terminating proxy (auth.trusted_proxies)]' \
|
||||
'--url=[listener URL, https://HOST:PORT or https://[IPV6\]:PORT (with --unbound, a proxy'\''s mount URL may carry a path)]:value:_default' \
|
||||
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
stream) _arguments -S \
|
||||
'--addr=[tcp sink address, HOST:PORT or [IPV6\]:PORT]:value:_default' \
|
||||
'--ca-file=[CA file that verifies the server (default: system roots)]:value:_default' \
|
||||
'--cert-file=[client certificate file, for listeners with tls.client_auth]:value:_default' \
|
||||
'--key-file=[client key file]:value:_default' \
|
||||
'--password-file=[file holding the password]:value:_default' \
|
||||
'--pin-sha256=[sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)]:value:_default' \
|
||||
'--server-name=[name the server certificate must carry (default: the host)]:value:_default' \
|
||||
'(-u --user)'{-u=,--user=}'[user name]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
esac
|
||||
return ;;
|
||||
tls:2) ;;
|
||||
tls:3)
|
||||
described=(
|
||||
'ca:Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only'
|
||||
'cert:Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key'
|
||||
)
|
||||
_describe -t commands 'lw tls command' described
|
||||
return ;;
|
||||
tls:*)
|
||||
shift 2 words
|
||||
(( CURRENT -= 2 ))
|
||||
case ${words[1]} in
|
||||
ca) _arguments -S \
|
||||
'--days=[validity in n days]:value:_default' \
|
||||
'--dir=[directory to write ca.crt and ca.key in, created when missing]:directory:_files -/' \
|
||||
'--name=[subject common name]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
cert) _arguments -S \
|
||||
'--ca-dir=[directory holding ca.crt and ca.key (lw tls ca)]:directory:_files -/' \
|
||||
'--client[for dialers presenting a certificate (TLS client authentication)]' \
|
||||
'--days=[validity in n days, at most the CA'\''s]:value:_default' \
|
||||
'--hosts=[DNS names and IP addresses, '\'','\'' between them (default with --server: --name)]:value:_default' \
|
||||
'--name=[subject common name, the mtls identity, and the file names]:value:_default' \
|
||||
'--out=[directory to write in (default: --ca-dir)]:directory:_files -/' \
|
||||
'--server[for listeners (TLS server authentication)]' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
esac
|
||||
return ;;
|
||||
preset:2) ;;
|
||||
preset:3)
|
||||
described=(
|
||||
'pipe:Standard input to standard output, line for line: lw without a file'
|
||||
'tail:Follow files to standard output, like tail -F'
|
||||
'serve:Serve files or standard input live: a browser viewer at /, SSE at /stream'
|
||||
'edge:Forward files or standard input to an aggregator over TLS, authenticated'
|
||||
'aggregator:Receive from edges over TLS, authenticated, to files or standard output'
|
||||
)
|
||||
_describe -t commands 'lw preset command' described
|
||||
return ;;
|
||||
preset:*)
|
||||
shift 2 words
|
||||
(( CURRENT -= 2 ))
|
||||
case ${words[1]} in
|
||||
pipe) _arguments -S \
|
||||
'--format=[raw|txt|json]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
tail) _arguments -S \
|
||||
'--format=[raw|txt|json]:value:_default' \
|
||||
'--from=[start|end: where reading a file starts]:value:_default' \
|
||||
'--path=[file, directory (its files) or glob to follow]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
serve) _arguments -S \
|
||||
'--allow=[addresses or CIDRs that may connect, of listen'\''s family (either behind proxy=); default: all; '\'','\'' between them]:value:_default' \
|
||||
'--cert=[certificate file, for tls=files]:value:_default' \
|
||||
'--deny=[addresses or CIDRs refused, allow or not; '\'','\'' between them]:value:_default' \
|
||||
'--format=[raw|txt|json]:value:_default' \
|
||||
'--from=[start|end: where reading a file starts]:value:_default' \
|
||||
'--hosts=[names and addresses, beyond this host'\''s, of a self or issuer certificate; '\'','\'' between them]:value:_default' \
|
||||
'--issuer_cert=[CA certificate file (lw tls ca), for tls=issuer]:value:_default' \
|
||||
'--issuer_key=[CA key file, for tls=issuer]:value:_default' \
|
||||
'--key=[key file, for tls=files]:value:_default' \
|
||||
'--listen=[HOST:PORT; [IPV6\]:PORT]:value:_default' \
|
||||
'--path=[file, directory (its files) or glob to follow; default: standard input]:value:_default' \
|
||||
'--proxy=[addresses or CIDRs of the TLS-terminating proxies browsers come through; '\'','\'' between them]:value:_default' \
|
||||
'--tls=[off|self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files]:value:_default' \
|
||||
'--users=[credentials file (lw auth add-user): readers log in with SCRAM]:value:_default' \
|
||||
'--viewer=[true: the login page and viewer for users, behind proxy; without users the viewer is always on]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
edge) _arguments -S \
|
||||
'--ca=[CA file that verifies the aggregator; default: system roots]:value:_default' \
|
||||
'--cert=[client certificate file, for an aggregator with client_ca]:value:_default' \
|
||||
'--from=[start|end: where reading a file starts]:value:_default' \
|
||||
'--key=[client key file]:value:_default' \
|
||||
'--node=[origin label; default: this host'\''s name]:value:_default' \
|
||||
'--password_file=[file holding the SCRAM password]:value:_default' \
|
||||
'--path=[file, directory (its files) or glob to follow; default: standard input]:value:_default' \
|
||||
'--pin=[sha256//BASE64 of the aggregator'\''s key, which it logs with tls=self]:value:_default' \
|
||||
'--server_name=[name the aggregator'\''s certificate carries; default: to'\''s host]:value:_default' \
|
||||
'--to=[the aggregator'\''s HOST:PORT; [IPV6\]:PORT]:value:_default' \
|
||||
'--transport=[tcp|http: tcp_chain or http_chain]:value:_default' \
|
||||
'(-u --user)'{-u=,--user=}'[SCRAM user (lw auth add-user on the aggregator)]:value:_default' \
|
||||
'(-h --help)'{-h,--help}'[print the flags]' ;;
|
||||
aggregator) _arguments -S \
|
||||
'--allow=[addresses or CIDRs that may connect, of listen'\''s family (either behind proxy=); default: all; '\'','\'' between them]:value:_default' \
|
||||
'--cert=[certificate file, for tls=files]:value:_default' \
|
||||
'--client_ca=[CA file that verifies edge certificates (mTLS)]:value:_default' \
|
||||
'--deny=[addresses or CIDRs refused, allow or not; '\'','\'' between them]:value:_default' \
|
||||
'--format=[raw|txt|json]:value:_default' \
|
||||
'--hosts=[names and addresses, beyond this host'\''s, of a self or issuer certificate; '\'','\'' between them]:value:_default' \
|
||||
'--issuer_cert=[CA certificate file (lw tls ca), for tls=issuer]:value:_default' \
|
||||
'--issuer_key=[CA key file, for tls=issuer]:value:_default' \
|
||||
'--key=[key file, for tls=files]:value:_default' \
|
||||
'--listen=[HOST:PORT; [IPV6\]:PORT; [::\] for IPv6 only]:value:_default' \
|
||||
'--out=[directory for the received entries; default: standard output]:value:_default' \
|
||||
'--tls=[self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files]:value:_default' \
|
||||
'--transport=[tcp|http: tcp_chain or http_chain]:value:_default' \
|
||||
'--users=[credentials file (lw auth add-user): edges log in with SCRAM]: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'
|
||||
'help:Print the help and exit'
|
||||
)
|
||||
_describe -t commands 'lw command' described
|
||||
return
|
||||
fi
|
||||
_arguments -S \
|
||||
'--auto_reload' \
|
||||
'(-t --check)'{-t,--check} \
|
||||
'--color' \
|
||||
'(-c --config)'{-c=,--config=}':value:_default' \
|
||||
'--dump' \
|
||||
'*--filter=:value:_default' \
|
||||
'*--format=:value:_default' \
|
||||
'*--heartbeat=:value:_default' \
|
||||
'(-h --help)'{-h,--help} \
|
||||
'--logging.console.target=:value:_default' \
|
||||
'--logging.file.directory=:directory:_files -/' \
|
||||
'--logging.file.max_size_mb=:value:_default' \
|
||||
'--logging.file.max_total_size_mb=:value:_default' \
|
||||
'--logging.file.name=:value:_default' \
|
||||
'--logging.file.retention_hours=:value:_default' \
|
||||
'--logging.format=:value:_default' \
|
||||
'--logging.level=:value:_default' \
|
||||
'--logging.output=:value:_default' \
|
||||
'--logging.sanitization=:value:_default' \
|
||||
'*--pipeline=:value:_default' \
|
||||
'*'{-p=,--preset=}':preset:(pipe tail serve edge aggregator)' \
|
||||
'(-q --quiet)'{-q,--quiet} \
|
||||
'*--rate-limit=:value:_default' \
|
||||
'*--sink=:value:_default' \
|
||||
'*--source=:value:_default' \
|
||||
'--status_reporter' \
|
||||
'(-V --version)'{-V,--version}
|
||||
@@ -0,0 +1,95 @@
|
||||
# bash completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
|
||||
|
||||
_lw_reply() { compopt -o filenames 2>/dev/null; mapfile -t COMPREPLY < <(compgen "$@" -- "$cur"); }
|
||||
|
||||
_lw() {
|
||||
local cur=${COMP_WORDS[COMP_CWORD]} prev='' i=$COMP_CWORD words
|
||||
((i > 0)) && prev=${COMP_WORDS[i-1]}
|
||||
# bash splits --key=value at '=': complete the value of the key before it
|
||||
if [[ $cur == = ]]; then cur=''
|
||||
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' ;;
|
||||
auth:2) words='add-user remove-user token stream' ;;
|
||||
auth:*)
|
||||
case ${COMP_WORDS[2]} in
|
||||
add-user)
|
||||
case $prev in
|
||||
--credentials|-credentials|--password-file|-password-file|--user|-user|-u) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--credentials --generate --password-file --user -u --help -h' ;;
|
||||
remove-user)
|
||||
case $prev in
|
||||
--credentials|-credentials|--user|-user|-u) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--credentials --user -u --help -h' ;;
|
||||
token)
|
||||
case $prev in
|
||||
--ca-file|-ca-file|--cert-file|-cert-file|--key-file|-key-file|--password-file|-password-file|--pin-sha256|-pin-sha256|--server-name|-server-name|--url|-url|--user|-user|-u) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--ca-file --cert-file --key-file --password-file --pin-sha256 --server-name --unbound --url --user -u --help -h' ;;
|
||||
stream)
|
||||
case $prev in
|
||||
--addr|-addr|--ca-file|-ca-file|--cert-file|-cert-file|--key-file|-key-file|--password-file|-password-file|--pin-sha256|-pin-sha256|--server-name|-server-name|--user|-user|-u) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--addr --ca-file --cert-file --key-file --password-file --pin-sha256 --server-name --user -u --help -h' ;;
|
||||
esac ;;
|
||||
tls:2) words='ca cert' ;;
|
||||
tls:*)
|
||||
case ${COMP_WORDS[2]} in
|
||||
ca)
|
||||
case $prev in
|
||||
--dir|-dir) _lw_reply -d; return ;;
|
||||
--days|-days|--name|-name) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--days --dir --name --help -h' ;;
|
||||
cert)
|
||||
case $prev in
|
||||
--ca-dir|-ca-dir|--out|-out) _lw_reply -d; return ;;
|
||||
--days|-days|--hosts|-hosts|--name|-name) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--ca-dir --client --days --hosts --name --out --server --help -h' ;;
|
||||
esac ;;
|
||||
preset:2) words='pipe tail serve edge aggregator' ;;
|
||||
preset:*)
|
||||
case ${COMP_WORDS[2]} in
|
||||
pipe)
|
||||
case $prev in
|
||||
--format|-format) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--format --help -h' ;;
|
||||
tail)
|
||||
case $prev in
|
||||
--format|-format|--from|-from|--path|-path) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--format --from --path --help -h' ;;
|
||||
serve)
|
||||
case $prev in
|
||||
--allow|-allow|--cert|-cert|--deny|-deny|--format|-format|--from|-from|--hosts|-hosts|--issuer_cert|-issuer_cert|--issuer_key|-issuer_key|--key|-key|--listen|-listen|--path|-path|--proxy|-proxy|--tls|-tls|--users|-users|--viewer|-viewer) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--allow --cert --deny --format --from --hosts --issuer_cert --issuer_key --key --listen --path --proxy --tls --users --viewer --help -h' ;;
|
||||
edge)
|
||||
case $prev in
|
||||
--ca|-ca|--cert|-cert|--from|-from|--key|-key|--node|-node|--password_file|-password_file|--path|-path|--pin|-pin|--server_name|-server_name|--to|-to|--transport|-transport|--user|-user|-u) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--ca --cert --from --key --node --password_file --path --pin --server_name --to --transport --user -u --help -h' ;;
|
||||
aggregator)
|
||||
case $prev in
|
||||
--allow|-allow|--cert|-cert|--client_ca|-client_ca|--deny|-deny|--format|-format|--hosts|-hosts|--issuer_cert|-issuer_cert|--issuer_key|-issuer_key|--key|-key|--listen|-listen|--out|-out|--tls|-tls|--transport|-transport|--users|-users) COMPREPLY=(); return ;;
|
||||
esac
|
||||
words='--allow --cert --client_ca --deny --format --hosts --issuer_cert --issuer_key --key --listen --out --tls --transport --users --help -h' ;;
|
||||
esac ;;
|
||||
*)
|
||||
case $prev in
|
||||
--logging.file.directory) _lw_reply -d; return ;;
|
||||
--preset|-p) mapfile -t COMPREPLY < <(compgen -W 'pipe tail serve edge aggregator' -- "$cur"); return ;;
|
||||
--config|-c|--filter|--format|--heartbeat|--logging.console.target|--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|--rate-limit|--sink|--source) COMPREPLY=(); return ;;
|
||||
esac
|
||||
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' ;;
|
||||
esac
|
||||
mapfile -t COMPREPLY < <(compgen -W "$words" -- "$cur")
|
||||
}
|
||||
|
||||
# An empty reply falls back to file names, as without this script
|
||||
complete -o bashdefault -o default -F _lw lw
|
||||
@@ -0,0 +1,145 @@
|
||||
# fish completion for lw. Generated by make completion from lw's own tables: edit cmd/lw, not this file.
|
||||
|
||||
# __lw_in WORD...: the command line so far is lw WORD...
|
||||
function __lw_in
|
||||
set -l w (commandline -opc)
|
||||
set -l n (math (count $argv) + 1)
|
||||
test (count $w) -ge $n; and test "$w[2..$n]" = "$argv"
|
||||
end
|
||||
|
||||
complete -c lw -f
|
||||
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 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 '__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'
|
||||
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a token -d 'Log in to an http sink or http_chain source and print a bearer token'
|
||||
complete -c lw -n '__lw_in auth; and test (count (commandline -opc)) -eq 2' -a stream -d 'Log in to a tcp sink and copy its stream to stdout until interrupted'
|
||||
complete -c lw -n '__lw_in auth add-user' -l credentials -r -F -d 'credentials file, created when missing'
|
||||
complete -c lw -n '__lw_in auth add-user' -l generate -d 'generate a new password, replacing any --password-file; needed to rotate without one'
|
||||
complete -c lw -n '__lw_in auth add-user' -l password-file -r -F -d 'read the password from file when it exists, else write a generated one there'
|
||||
complete -c lw -n '__lw_in auth add-user' -l user -o u -r -F -d 'user name'
|
||||
complete -c lw -n '__lw_in auth add-user' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in auth remove-user' -l credentials -r -F -d 'credentials file'
|
||||
complete -c lw -n '__lw_in auth remove-user' -l user -o u -r -F -d 'user name'
|
||||
complete -c lw -n '__lw_in auth remove-user' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in auth token' -l ca-file -r -F -d 'CA file that verifies the server (default: system roots)'
|
||||
complete -c lw -n '__lw_in auth token' -l cert-file -r -F -d 'client certificate file, for listeners with tls.client_auth'
|
||||
complete -c lw -n '__lw_in auth token' -l key-file -r -F -d 'client key file'
|
||||
complete -c lw -n '__lw_in auth token' -l password-file -r -F -d 'file holding the password'
|
||||
complete -c lw -n '__lw_in auth token' -l pin-sha256 -r -F -d 'sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)'
|
||||
complete -c lw -n '__lw_in auth token' -l server-name -r -F -d 'name the server certificate must carry (default: the host)'
|
||||
complete -c lw -n '__lw_in auth token' -l unbound -d 'log in without channel binding, to an http sink behind a TLS-terminating proxy (auth.trusted_proxies)'
|
||||
complete -c lw -n '__lw_in auth token' -l url -r -F -d 'listener URL, https://HOST:PORT or https://[IPV6]:PORT (with --unbound, a proxy\'s mount URL may carry a path)'
|
||||
complete -c lw -n '__lw_in auth token' -l user -o u -r -F -d 'user name'
|
||||
complete -c lw -n '__lw_in auth token' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in auth stream' -l addr -r -F -d 'tcp sink address, HOST:PORT or [IPV6]:PORT'
|
||||
complete -c lw -n '__lw_in auth stream' -l ca-file -r -F -d 'CA file that verifies the server (default: system roots)'
|
||||
complete -c lw -n '__lw_in auth stream' -l cert-file -r -F -d 'client certificate file, for listeners with tls.client_auth'
|
||||
complete -c lw -n '__lw_in auth stream' -l key-file -r -F -d 'client key file'
|
||||
complete -c lw -n '__lw_in auth stream' -l password-file -r -F -d 'file holding the password'
|
||||
complete -c lw -n '__lw_in auth stream' -l pin-sha256 -r -F -d 'sha256//BASE64 pin of the server key, in place of --ca-file (a self_signed listener logs it)'
|
||||
complete -c lw -n '__lw_in auth stream' -l server-name -r -F -d 'name the server certificate must carry (default: the host)'
|
||||
complete -c lw -n '__lw_in auth stream' -l user -o u -r -F -d 'user name'
|
||||
complete -c lw -n '__lw_in auth stream' -l help -o h -d 'print the flags'
|
||||
|
||||
complete -c lw -n '__lw_in tls; and test (count (commandline -opc)) -eq 2' -a ca -d 'Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only'
|
||||
complete -c lw -n '__lw_in tls; and test (count (commandline -opc)) -eq 2' -a cert -d 'Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key'
|
||||
complete -c lw -n '__lw_in tls ca' -l days -r -F -d 'validity in n days'
|
||||
complete -c lw -n '__lw_in tls ca' -l dir -x -a '(__fish_complete_directories)' -d 'directory to write ca.crt and ca.key in, created when missing'
|
||||
complete -c lw -n '__lw_in tls ca' -l name -r -F -d 'subject common name'
|
||||
complete -c lw -n '__lw_in tls ca' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in tls cert' -l ca-dir -x -a '(__fish_complete_directories)' -d 'directory holding ca.crt and ca.key (lw tls ca)'
|
||||
complete -c lw -n '__lw_in tls cert' -l client -d 'for dialers presenting a certificate (TLS client authentication)'
|
||||
complete -c lw -n '__lw_in tls cert' -l days -r -F -d 'validity in n days, at most the CA\'s'
|
||||
complete -c lw -n '__lw_in tls cert' -l hosts -r -F -d 'DNS names and IP addresses, \',\' between them (default with --server: --name)'
|
||||
complete -c lw -n '__lw_in tls cert' -l name -r -F -d 'subject common name, the mtls identity, and the file names'
|
||||
complete -c lw -n '__lw_in tls cert' -l out -x -a '(__fish_complete_directories)' -d 'directory to write in (default: --ca-dir)'
|
||||
complete -c lw -n '__lw_in tls cert' -l server -d 'for listeners (TLS server authentication)'
|
||||
complete -c lw -n '__lw_in tls cert' -l help -o h -d 'print the flags'
|
||||
|
||||
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a pipe -d 'Standard input to standard output, line for line: lw without a file'
|
||||
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a tail -d 'Follow files to standard output, like tail -F'
|
||||
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a serve -d 'Serve files or standard input live: a browser viewer at /, SSE at /stream'
|
||||
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a edge -d 'Forward files or standard input to an aggregator over TLS, authenticated'
|
||||
complete -c lw -n '__lw_in preset; and test (count (commandline -opc)) -eq 2' -a aggregator -d 'Receive from edges over TLS, authenticated, to files or standard output'
|
||||
complete -c lw -n '__lw_in preset pipe' -l format -r -F -d 'raw|txt|json'
|
||||
complete -c lw -n '__lw_in preset pipe' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in preset tail' -l format -r -F -d 'raw|txt|json'
|
||||
complete -c lw -n '__lw_in preset tail' -l from -r -F -d 'start|end: where reading a file starts'
|
||||
complete -c lw -n '__lw_in preset tail' -l path -r -F -d 'file, directory (its files) or glob to follow'
|
||||
complete -c lw -n '__lw_in preset tail' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in preset serve' -l allow -r -F -d 'addresses or CIDRs that may connect, of listen\'s family (either behind proxy=); default: all; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset serve' -l cert -r -F -d 'certificate file, for tls=files'
|
||||
complete -c lw -n '__lw_in preset serve' -l deny -r -F -d 'addresses or CIDRs refused, allow or not; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset serve' -l format -r -F -d 'raw|txt|json'
|
||||
complete -c lw -n '__lw_in preset serve' -l from -r -F -d 'start|end: where reading a file starts'
|
||||
complete -c lw -n '__lw_in preset serve' -l hosts -r -F -d 'names and addresses, beyond this host\'s, of a self or issuer certificate; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset serve' -l issuer_cert -r -F -d 'CA certificate file (lw tls ca), for tls=issuer'
|
||||
complete -c lw -n '__lw_in preset serve' -l issuer_key -r -F -d 'CA key file, for tls=issuer'
|
||||
complete -c lw -n '__lw_in preset serve' -l key -r -F -d 'key file, for tls=files'
|
||||
complete -c lw -n '__lw_in preset serve' -l listen -r -F -d 'HOST:PORT; [IPV6]:PORT'
|
||||
complete -c lw -n '__lw_in preset serve' -l path -r -F -d 'file, directory (its files) or glob to follow; default: standard input'
|
||||
complete -c lw -n '__lw_in preset serve' -l proxy -r -F -d 'addresses or CIDRs of the TLS-terminating proxies browsers come through; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset serve' -l tls -r -F -d 'off|self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or files'
|
||||
complete -c lw -n '__lw_in preset serve' -l users -r -F -d 'credentials file (lw auth add-user): readers log in with SCRAM'
|
||||
complete -c lw -n '__lw_in preset serve' -l viewer -r -F -d 'true: the login page and viewer for users, behind proxy; without users the viewer is always on'
|
||||
complete -c lw -n '__lw_in preset serve' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in preset edge' -l ca -r -F -d 'CA file that verifies the aggregator; default: system roots'
|
||||
complete -c lw -n '__lw_in preset edge' -l cert -r -F -d 'client certificate file, for an aggregator with client_ca'
|
||||
complete -c lw -n '__lw_in preset edge' -l from -r -F -d 'start|end: where reading a file starts'
|
||||
complete -c lw -n '__lw_in preset edge' -l key -r -F -d 'client key file'
|
||||
complete -c lw -n '__lw_in preset edge' -l node -r -F -d 'origin label; default: this host\'s name'
|
||||
complete -c lw -n '__lw_in preset edge' -l password_file -r -F -d 'file holding the SCRAM password'
|
||||
complete -c lw -n '__lw_in preset edge' -l path -r -F -d 'file, directory (its files) or glob to follow; default: standard input'
|
||||
complete -c lw -n '__lw_in preset edge' -l pin -r -F -d 'sha256//BASE64 of the aggregator\'s key, which it logs with tls=self'
|
||||
complete -c lw -n '__lw_in preset edge' -l server_name -r -F -d 'name the aggregator\'s certificate carries; default: to\'s host'
|
||||
complete -c lw -n '__lw_in preset edge' -l to -r -F -d 'the aggregator\'s HOST:PORT; [IPV6]:PORT'
|
||||
complete -c lw -n '__lw_in preset edge' -l transport -r -F -d 'tcp|http: tcp_chain or http_chain'
|
||||
complete -c lw -n '__lw_in preset edge' -l user -o u -r -F -d 'SCRAM user (lw auth add-user on the aggregator)'
|
||||
complete -c lw -n '__lw_in preset edge' -l help -o h -d 'print the flags'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l allow -r -F -d 'addresses or CIDRs that may connect, of listen\'s family (either behind proxy=); default: all; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l cert -r -F -d 'certificate file, for tls=files'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l client_ca -r -F -d 'CA file that verifies edge certificates (mTLS)'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l deny -r -F -d 'addresses or CIDRs refused, allow or not; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l format -r -F -d 'raw|txt|json'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l hosts -r -F -d 'names and addresses, beyond this host\'s, of a self or issuer certificate; \',\' between them'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l issuer_cert -r -F -d 'CA certificate file (lw tls ca), for tls=issuer'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l issuer_key -r -F -d 'CA key file, for tls=issuer'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l key -r -F -d 'key file, for tls=files'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l listen -r -F -d 'HOST:PORT; [IPV6]:PORT; [::] for IPv6 only'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l out -r -F -d 'directory for the received entries; default: standard output'
|
||||
complete -c lw -n '__lw_in preset aggregator' -l tls -r -F -d 'self|issuer|files: a certificate made at startup (self-signed or from issuer_*), or 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'
|
||||
@@ -0,0 +1,4 @@
|
||||
Section: admin
|
||||
Priority: optional
|
||||
Homepage: https://github.com/lixenwraith/logwisp
|
||||
Description: Pipeline-based log transport with TLS and SCRAM authentication
|
||||
Executable
+13
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# sysusers and tmpfiles need no running systemd (a chroot); a first install
|
||||
# leaves the service to systemctl enable --now logwisp, an upgrade restarts it
|
||||
# if it runs. As in debhelper's scripts, only the account is fatal.
|
||||
set -e
|
||||
if [ "$1" = configure ]; then
|
||||
if command -v systemd-sysusers >/dev/null; then systemd-sysusers logwisp.conf; fi
|
||||
if command -v systemd-tmpfiles >/dev/null; then systemd-tmpfiles --create logwisp.conf || true; fi
|
||||
if [ -d /run/systemd/system ]; then
|
||||
systemctl daemon-reload || true
|
||||
if [ -n "$2" ]; then systemctl try-restart logwisp.service || true; fi
|
||||
fi
|
||||
fi
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
# A purge removes logwisp.toml; credentials in /etc/logwisp, /var/lib/logwisp,
|
||||
# /var/log/logwisp and the logwisp user stay, as make uninstall keeps them.
|
||||
set -e
|
||||
if [ -d /run/systemd/system ]; then systemctl daemon-reload || true; fi
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ "$1" = remove ]; then
|
||||
if [ -d /run/systemd/system ]; then systemctl stop logwisp.service || true; fi
|
||||
systemctl disable logwisp.service >/dev/null 2>&1 || true
|
||||
fi
|
||||
@@ -33,6 +33,15 @@ PORTDOCS= *
|
||||
|
||||
post-install:
|
||||
${INSTALL_MAN} ${WRKSRC}/doc/lw.1 ${STAGEDIR}${PREFIX}/share/man/man1
|
||||
${MKDIR} ${STAGEDIR}${PREFIX}/share/bash-completion/completions \
|
||||
${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d \
|
||||
${STAGEDIR}${PREFIX}/share/zsh/site-functions
|
||||
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/lw.bash \
|
||||
${STAGEDIR}${PREFIX}/share/bash-completion/completions/lw
|
||||
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/lw.fish \
|
||||
${STAGEDIR}${PREFIX}/share/fish/vendor_completions.d
|
||||
${INSTALL_DATA} ${WRKSRC}/deploy/package/completion/_lw \
|
||||
${STAGEDIR}${PREFIX}/share/zsh/site-functions
|
||||
${MKDIR} ${STAGEDIR}${ETCDIR}
|
||||
${INSTALL_DATA} ${WRKSRC}/config/logwisp.toml ${STAGEDIR}${ETCDIR}/logwisp.toml.sample
|
||||
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
bin/lw
|
||||
@sample %%ETCDIR%%/logwisp.toml.sample
|
||||
share/bash-completion/completions/lw
|
||||
share/fish/vendor_completions.d/lw.fish
|
||||
share/man/man1/lw.1.gz
|
||||
share/zsh/site-functions/_lw
|
||||
+1
-1
@@ -30,7 +30,7 @@ streams, or downstream LogWisp nodes.
|
||||
- [CLI](cli.md): flags, signals, exit codes, `lw auth`; also the `lw(1)`
|
||||
manual, [`lw.1`](lw.1)
|
||||
- [Operations](operations.md): running, monitoring, tuning, troubleshooting
|
||||
- [To Do](todo.md): planned work: packaging
|
||||
- [To Do](todo.md): planned work: packaging, `lw --tui` and its shared configuration engine
|
||||
|
||||
A fully annotated configuration covering every option lives at
|
||||
[`config/logwisp.toml`](../config/logwisp.toml).
|
||||
|
||||
+43
-9
@@ -27,6 +27,9 @@ The Makefile works with GNU make and BSD make alike. Targets:
|
||||
- `make release`: the same, static (`CGO_ENABLED=0`), `-trimpath`, stripped
|
||||
- `make dev`: built with the race detector
|
||||
- `make version`: the metadata a build would embed
|
||||
- `make completion`: regenerate the bash, zsh and fish completion in
|
||||
`deploy/package/completion/` from `cmd/lw`'s `shorts` and `commands`
|
||||
tables; `go test ./cmd/lw` fails while they differ
|
||||
- `make clean`: remove `bin/`
|
||||
- Check
|
||||
- `make test`: the Go tests
|
||||
@@ -39,7 +42,7 @@ 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`: see [Installing](#installing)
|
||||
- `make install`, `make uninstall`, `make deb`: see [Installing](#installing)
|
||||
|
||||
Variables:
|
||||
|
||||
@@ -70,6 +73,11 @@ 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.
|
||||
- `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`
|
||||
for `PREFIX=/usr` on Debian and Ubuntu, whose zsh reads only that) and
|
||||
`$PREFIX/share/fish/vendor_completions.d`.
|
||||
|
||||
```bash
|
||||
make release
|
||||
@@ -79,6 +87,8 @@ sudo make install PREFIX=/usr/local
|
||||
Every system gets:
|
||||
|
||||
- `$BINDIR/lw` and the manual `$PREFIX/share/man/man1/lw.1`
|
||||
- completion for `lw`, its commands, flags, files and preset names:
|
||||
`lw`, `_lw` and `lw.fish` in the three directories above
|
||||
- `$PREFIX/share/doc/logwisp/` (this documentation) and
|
||||
`$PREFIX/share/licenses/logwisp/LICENSE`
|
||||
|
||||
@@ -146,6 +156,28 @@ rc.conf variables:
|
||||
- `logwisp_args`: extra `lw` options, e.g. `--logging.level=debug` (default
|
||||
empty); `logwisp_flags` goes to `daemon(8)`, not to `lw`
|
||||
|
||||
### Debian and Ubuntu (.deb)
|
||||
|
||||
`make deb` builds a release and packages what `make install` stages, in
|
||||
Debian's layout, as `bin/logwisp_VERSION_ARCH.deb`, the architecture taken
|
||||
from the binary. A release tag gives the version; an untagged or modified
|
||||
tree's sorts below every release and by build time, so each build upgrades
|
||||
the last. `DEB_MAINTAINER` names the maintainer.
|
||||
|
||||
```bash
|
||||
make deb
|
||||
sudo apt install ./bin/logwisp_*.deb
|
||||
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.
|
||||
- Removing stops and disables the service. The account, its directories
|
||||
and any credentials in `/etc/logwisp` stay, as with `make uninstall`.
|
||||
|
||||
### Uninstall
|
||||
|
||||
`sudo make uninstall` (with the `PREFIX` used to install) removes everything
|
||||
@@ -235,18 +267,20 @@ docker run -d --name logwisp \
|
||||
|
||||
## Packaging Status
|
||||
|
||||
The foundation for distribution packages is in place:
|
||||
In place:
|
||||
|
||||
- the `lw` binary name, and `make install` with `DESTDIR`, `PREFIX` and
|
||||
`SYSCONFDIR`
|
||||
- the `doc/lw.1` manual
|
||||
- service files in `deploy/package/`: `logwisp.service`, `logwisp.sysusers`,
|
||||
`logwisp.tmpfiles` and the FreeBSD `logwisp.rc`
|
||||
- `make install` with `DESTDIR`, `PREFIX` and `SYSCONFDIR`, the manual,
|
||||
shell completion and the service files in `deploy/package/`
|
||||
- `make deb`
|
||||
- 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`
|
||||
|
||||
Still missing: release tags, which both skeletons download; finishing and
|
||||
submitting the AUR package and the port; Debian packaging.
|
||||
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.
|
||||
[To Do](todo.md) has the steps.
|
||||
|
||||
## Verification
|
||||
|
||||
+175
-14
@@ -11,14 +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 `doc/lw.1` manual.
|
||||
- Service files in `deploy/package/`: systemd unit, sysusers, tmpfiles and the
|
||||
FreeBSD rc.d script.
|
||||
- `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`.
|
||||
- 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`.
|
||||
|
||||
What remains. Steps 1 to 3 are the maintainer's: they need push rights and an
|
||||
identity, and both skeletons download the tagged source.
|
||||
What remains is the maintainer's: it needs push rights, an identity or a
|
||||
release tag, and both skeletons download the tagged source.
|
||||
|
||||
1. Tag the release on the merged main commit, `vX.Y.Z`:
|
||||
```
|
||||
@@ -52,11 +56,168 @@ identity, and both skeletons download the tagged source.
|
||||
- `portlint -AC`, `poudriere testport` on 14.x and 15.x jails, then a
|
||||
Bugzilla report with the port directory and the UIDs/GIDs diff.
|
||||
4. Debian:
|
||||
- Debian policy wants every Go dependency packaged; the four `lixenwraith`
|
||||
libraries are not.
|
||||
- Either package them too (dh-golang), or start with an `.deb` built by the
|
||||
Makefile `install` target (nfpm or `dpkg-deb`) and a PPA, and move to the
|
||||
archive later.
|
||||
5. Shell completion for bash, zsh and fish, installed by `make install`.
|
||||
6. A packaging CI job: build the AUR package in an Arch container and the port
|
||||
in a FreeBSD VM, run `lw --version` and `make image-check`.
|
||||
- `make deb` serves direct installs. A PPA builds from a signed source
|
||||
package without network access: a `debian/` directory (changelog,
|
||||
control, rules) and the Go modules vendored into the source tarball.
|
||||
- The archive wants every Go dependency packaged (dh-golang); the four
|
||||
`lixenwraith` libraries are not.
|
||||
- `lintian` on the `make deb` package reports `no-changelog` (a source
|
||||
package's `debian/changelog` provides it), `statically-linked-binary`
|
||||
(lw is static by design) and `maintainer-script-calls-systemctl` (dh's
|
||||
scripts use `deb-systemd-helper` instead).
|
||||
5. With a tag, the packaging workflow's FreeBSD job can build the port itself
|
||||
from a ports tree (`make stage check-plist` in a copy of
|
||||
`deploy/package/freebsd/`), rather than BSD make on the checkout.
|
||||
|
||||
## 2. `lw --tui` and the configuration engine it shares with lixen.com
|
||||
|
||||
`lw --tui` composes pipelines in a full-screen terminal UI, then either runs
|
||||
them at once or prints one form of them, chosen on exit: a command line,
|
||||
environment variables, or a configuration file. Its logic is a pure engine that
|
||||
lixen.com's planned Configure tab (lixencom `doc/todo.md`) runs as WebAssembly,
|
||||
so no part of LogWisp is rewritten in JavaScript.
|
||||
|
||||
What exists:
|
||||
- Option structs with toml tags for every plugin and flow stage
|
||||
(`internal/config/config.go`), presets as rows with typed parameters
|
||||
(`preset.go`), the command-line and environment grammar (`spec.go`),
|
||||
`ValidateConfig`, `--check` and `--dump`.
|
||||
- `internal/config` already compiles for `GOOS=js`; it links no net/http,
|
||||
crypto/tls or terminal code.
|
||||
- `github.com/lixenwraith/terminal` (pinned) and its `tui` package: regions,
|
||||
layout, boxes, List, Tree, TabBar, StatusBar, TextField, Form, Modal,
|
||||
ConfirmDialog, scrolling, mouse. `github.com/lixenwraith/color`: RGB,
|
||||
blending, RGBTo256, RGBTo16.
|
||||
|
||||
What is missing:
|
||||
- A machine-readable option schema. Defaults, enums and required checks live
|
||||
inside the plugin constructors (about 50 `Default*` constants, 24 inline
|
||||
validators); `doc/sources.md`, `doc/sinks.md`, `config/logwisp.toml` and the
|
||||
site's catalogue copy them by hand, and the site's copy already lags.
|
||||
- Offline plugin validation: the constructors mix their checks with reading
|
||||
certificates, credentials and files.
|
||||
- Typed command-line values: `lw --sink http,port=8080 --dump` prints
|
||||
`port = "8080"`, a file `port = 8080`.
|
||||
- A host-independent preset: `fileSource` (`preset.go`) decides directory or
|
||||
pattern with `os.Stat`, so off the target host `path=/var/log` expands to
|
||||
directory `/var`, pattern `log`.
|
||||
- Emitters: only parsers exist for the three forms.
|
||||
|
||||
Steps:
|
||||
1. Schema, in `internal/config`, as the one source of truth.
|
||||
- Tags on the option structs: `default:`, `help:`, and `lw:` for required,
|
||||
enum, min and max, what zero means (default, none, unlimited), hints
|
||||
(path, regex, file) and roles (listener, dialer) on the shared tls, auth
|
||||
and acl tables.
|
||||
- One catalogue table, one row per plugin type: role, network role, single
|
||||
instance, summary, option struct. `RegisterSource`/`RegisterSink` panic on
|
||||
a type without a row.
|
||||
- `Decode[T](role, type, map)` applies defaults, unknown-key and tag checks,
|
||||
then the struct's pure `Check()`. Every constructor decodes through it and
|
||||
keeps only I/O; the `Default*` constants and inline validators go.
|
||||
- Pure cross-field checks become methods that tlsx, authz and netacl call
|
||||
first: TLS role rules, auth bindings without file reads, ACL parsing, the
|
||||
http sink's paths, filter regexes.
|
||||
- `Coerce` converts command-line and environment strings, and JSON floats,
|
||||
to the declared kinds; `spec.go` uses it, so a command line dumps typed.
|
||||
- `ValidateConfig` adds unique plugin ids per pipeline and decodes every
|
||||
plugin, naming the path (`pipelines[0].plugin_sinks[view].config.port`).
|
||||
It opens nothing; `lw --check` stays the authority on files.
|
||||
- Keep under test what moves: file sink `min_disk_free_mb` (negative means
|
||||
100, 0 means none), a backoff maximum below the minimum, format `text`.
|
||||
2. Engine, `internal/compose`.
|
||||
- Imports internal/config, internal/version and lixenwraith/toml only; a
|
||||
test fails when `go list -deps` shows net, crypto/tls, os/exec, os/signal,
|
||||
x/term or the auth, log and terminal modules.
|
||||
- Starts: empty; a preset, with a `stat` function that is nil off the host
|
||||
(the UI then asks "directory or file pattern"); a loaded configuration; a
|
||||
pasted command line (wrapping `cliSpecs` and `buildPipelines`).
|
||||
- Operations: add or remove a source, sink or filter; set or unset an
|
||||
option; move a filter; validate.
|
||||
- Emitters, one form per call:
|
||||
- Command line: `--pipeline NAME`, `id=`, the rate limit policy and
|
||||
heartbeat `enabled` always explicit (their command-line defaults differ
|
||||
from the file's); `--key=value` when a value starts with `-`; `,` `=`
|
||||
`\` escaped; POSIX-quoted.
|
||||
- Environment: `LOGWISP_SOURCE_1` and the rest, single-quoted
|
||||
`KEY='value'` lines; an error for more than one pipeline, which the
|
||||
environment form cannot hold.
|
||||
- File: the TOML `lw --dump` prints for those pipelines.
|
||||
3. `lw --schema`, a parser switch like `--help` (not a Config field, so no
|
||||
file or environment can trigger it). It prints one JSON document: version;
|
||||
settings with kind, default, enum, flag and variable; the pipeline flags;
|
||||
flow stage order; the tls, auth and acl tables; sources, sinks, presets.
|
||||
The site generates its catalogue from it.
|
||||
4. In `lixenwraith/terminal`, released and pinned before the TUI:
|
||||
- Full-screen 8- and 16-color output (SGR 30-37, 90-97, 40-47) and the
|
||||
default colors 39/49; it emits only 256-color and truecolor today.
|
||||
- Detection as `inline` does it: 16 colors on text consoles, none under
|
||||
`NO_COLOR`, refusal on `TERM=dumb`, and an override for LogWisp's `color`
|
||||
key.
|
||||
- Drawing on `/dev/tty`, so `lw --tui > logwisp.toml` and `cmd | lw --tui`
|
||||
keep stdin and stdout as data.
|
||||
- Bracketed paste into text fields.
|
||||
- `tui` widgets the site's controls have: segmented choice, filterable
|
||||
option list, toggle, typed fields (default as placeholder, required mark,
|
||||
error and help line, folded groups), a focus ring; every widget styled
|
||||
from one `tui.Theme`.
|
||||
- In `color`, optional: export the 16 xterm reference shades. RGBTo16 maps
|
||||
the site's hues badly (green to cyan, red to bright black), so the
|
||||
8-color tier is chosen by hand.
|
||||
5. `lw --tui`.
|
||||
- A parser switch, no short form. With `-c FILE`, pipeline flags or
|
||||
`LOGWISP_SOURCE` it starts from that composition, and never rewrites FILE
|
||||
(TOML encoding drops its comments); bare, it opens the preset menu (pipe,
|
||||
tail, serve, edge, aggregator, empty).
|
||||
- The pipeline as the site draws it: SOURCES, FLOW, SINKS; a node is a
|
||||
tinted bar, its type, listens or dials, and a summary line; the four flow
|
||||
stages in order inside a dashed box, the ones off dimmed; wires drawn from
|
||||
per-cell connectivity so every junction is right. The inspector sits
|
||||
below at 80 to 109 columns, beside at 110 and over; under 64 columns the
|
||||
stages stack, as on the site.
|
||||
```
|
||||
● SOURCES ● FLOW ● SINKS
|
||||
▌ file ─┐ ╭╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╮ ┌─▌ http listens
|
||||
▌ /var/log *.log │ ╎ ▌ rate_limit 400/s ╎ │ ▌ :8080 /stream
|
||||
█ tcp_chain listens─┴─►╎ ▌ filters -TRACE ╎─►┤
|
||||
█ :9000 tls scram ╎ ▌ format txt ╎ └─▌ console
|
||||
╎ ○ heartbeat off ╎ ▌ stdout
|
||||
╰╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╯
|
||||
── tcp_chain · source · id src-2 ─────────────────── ⏎ edit tab next ──
|
||||
host 0.0.0.0 default format raw [txt] json
|
||||
port * 9000▌ tls ▸ on (auth, acl folded)
|
||||
✓ valid a add d delete ␣ on/off J/K move p preset o output ? help
|
||||
```
|
||||
- Keys: arrows or hjkl move, Enter edits, `a` adds from the catalogue, `d`
|
||||
deletes, Space switches a flow stage, `J`/`K` reorder filters, `p`
|
||||
presets, `[` `]` switch pipelines, `c` lists errors, `o` output, `?`
|
||||
help, `q` quits.
|
||||
- Theme: the site's dark tokens as one role table in four tiers. Truecolor
|
||||
and 256 colors paint the site's background; 8 colors and mono keep the
|
||||
terminal's. Sources cyan, flow violet, sinks green, accent blue, muted
|
||||
grey, errors red. A selection always changes a glyph (bar, `▸`,
|
||||
underline), never only a shade. Glyphs: Unicode, a CP437 set on text
|
||||
consoles, ASCII outside UTF-8.
|
||||
- Output, `o`: Run hands `config.Args` built from the composition to the
|
||||
usual `config.Load`, after the terminal is released and before anything
|
||||
reads stdin; reloads keep the pipelines, and `--tui --check` and `--tui
|
||||
--dump` follow. Or exactly one of command line, environment and file,
|
||||
printed to stdout once the screen closes.
|
||||
6. WebAssembly: `cmd/lwconf` (`js && wasm`) exposes schema, preset, parse,
|
||||
validate and emit to the page as JSON strings. `make wasm` builds
|
||||
`bin/lwconf.wasm` with the version stamped, copies the toolchain's
|
||||
`wasm_exec.js`, and prints both checksums. A prototype measured about 6 MB,
|
||||
1.7 MB gzipped, ready in about 60 ms in Chromium; `wasm_exec.js` uses no
|
||||
eval, so the page needs only `'wasm-unsafe-eval'` in its `script-src`.
|
||||
|
||||
Verify:
|
||||
- A Go test in `cmd/lw`: for every preset, an edge and aggregator pair,
|
||||
escaped values and two pipelines, the pipelines loaded from the emitted
|
||||
command line, file and environment (one pipeline) are equal. It runs in an
|
||||
empty directory with the `LOGWISP_` environment cleared, since a discovered
|
||||
file would add settings.
|
||||
- A schema test: every option has a kind, every enum values, every catalogue
|
||||
row a factory; `TestEveryPluginRejectsUnknownKeys` still passes.
|
||||
- CI builds `cmd/lwconf` for `GOOS=js` and runs a node test of its calls.
|
||||
- The diagram rendered into a cell buffer at 80x24 and 64x24, per tier; a
|
||||
scripted session on a pseudo-terminal for Run and each output.
|
||||
Reference in new issue
Block a user