v0.35.0 tui fixes
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
Package / arch (push) Canceled after 0s
Package / user (push) Canceled after 0s
Package / deb (push) Canceled after 0s
Package / freebsd (push) Canceled after 0s
Package / image (push) Canceled after 0s
Test / test (push) Canceled after 0s
Test / e2e (push) Canceled after 0s
This commit is contained in:
1 parent
1abffcec6c
commit
f4f8cc0986
24 files changed
+965
-295
No files matched your search
@@ -209,13 +209,14 @@ func defaults() *Config {
|
||||
}
|
||||
}
|
||||
|
||||
// pipeDefault is a filter in the Unix sense: stdin to stdout, line for line
|
||||
// pipeDefault is a filter in the Unix sense: stdin to stdout, line for line.
|
||||
// Its parts take the ids lw gives their type, so a retyped part follows it.
|
||||
func pipeDefault(name string) PipelineConfig {
|
||||
return PipelineConfig{
|
||||
Name: name,
|
||||
Flow: &FlowConfig{Format: &FormatConfig{Type: "raw"}},
|
||||
PluginSources: []PluginSourceConfig{{ID: "stdin", Type: "console", Config: map[string]any{}}},
|
||||
PluginSinks: []PluginSinkConfig{{ID: "stdout", Type: "console", Config: map[string]any{}}},
|
||||
PluginSources: []PluginSourceConfig{{ID: FreeID("console", nil), Type: "console", Config: map[string]any{}}},
|
||||
PluginSinks: []PluginSinkConfig{{ID: FreeID("console", nil), Type: "console", Config: map[string]any{}}},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,16 +61,17 @@ func mirror[T any](p PresetParam, key string) PresetParam {
|
||||
}
|
||||
|
||||
// IsDir tells whether a preset's path is a directory, whose files it follows,
|
||||
// or a file pattern. Off the target host it is the user's Answers, or nil: a
|
||||
// path ending in '/' is then a directory, one with a glob a pattern, and any
|
||||
// other fails with ErrPathKind.
|
||||
// or a file pattern; a path ending in '/' is a directory without asking. Off
|
||||
// the target host it is the user's Answers, or nil: a path with a glob is then
|
||||
// a pattern, and any other fails with ErrPathKind.
|
||||
type IsDir func(path string) (bool, error)
|
||||
|
||||
var ErrPathKind error = &KeyError{"path", errors.New("a directory or a file pattern?")}
|
||||
|
||||
// HostIsDir asks this host: a path that does not exist is a pattern
|
||||
// HostIsDir asks this host, a leading ~ being the home directory: a path that
|
||||
// does not exist is a pattern
|
||||
func HostIsDir(path string) (bool, error) {
|
||||
fi, err := os.Stat(path)
|
||||
fi, err := os.Stat(ExpandHome(path))
|
||||
return err == nil && fi.IsDir(), nil
|
||||
}
|
||||
|
||||
@@ -81,8 +82,8 @@ func Answers(dirs map[string]bool) IsDir {
|
||||
if dir, ok := dirs[path]; ok {
|
||||
return dir, nil
|
||||
}
|
||||
if dir := strings.HasSuffix(path, "/"); dir || strings.ContainsAny(path, `*?[\`) {
|
||||
return dir, nil
|
||||
if strings.ContainsAny(path, `*?[\`) {
|
||||
return false, nil
|
||||
}
|
||||
return false, ErrPathKind
|
||||
}
|
||||
@@ -353,7 +354,10 @@ func pathOrStdin(v map[string]string, isDir IsDir) ([]PluginSourceConfig, error)
|
||||
if isDir == nil {
|
||||
isDir = Answers(nil)
|
||||
}
|
||||
dir, err := isDir(path)
|
||||
dir, err := strings.HasSuffix(path, "/"), error(nil)
|
||||
if !dir {
|
||||
dir, err = isDir(path)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -19,14 +21,14 @@ func TestPresetStartsAndNamesItsPipeline(t *testing.T) {
|
||||
Flow: &FlowConfig{Format: &FormatConfig{Type: "json"}},
|
||||
PluginSources: []PluginSourceConfig{{ID: "file", Type: "file", Config: map[string]any{"directory": "/var/log", "pattern": "app.log", "from": "end"}}},
|
||||
PluginSinks: []PluginSinkConfig{
|
||||
{ID: "stdout", Type: "console", Config: map[string]any{"color": "auto"}},
|
||||
{ID: "console", Type: "console", Config: map[string]any{"color": "auto"}},
|
||||
{ID: "null", Type: "null", Config: map[string]any{}},
|
||||
},
|
||||
}, {
|
||||
Name: "mine",
|
||||
Flow: &FlowConfig{Format: &FormatConfig{Type: "raw"}},
|
||||
PluginSources: []PluginSourceConfig{{ID: "stdin", Type: "console", Config: map[string]any{}}},
|
||||
PluginSinks: []PluginSinkConfig{{ID: "stdout", Type: "console", Config: map[string]any{"color": "auto"}}},
|
||||
PluginSources: []PluginSourceConfig{{ID: "console", Type: "console", Config: map[string]any{}}},
|
||||
PluginSinks: []PluginSinkConfig{{ID: "console", Type: "console", Config: map[string]any{"color": "auto"}}},
|
||||
}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("pipelines:\n got %+v\nwant %+v", got, want)
|
||||
@@ -122,11 +124,16 @@ func TestPresetUserWithoutPasswordFileAsks(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A preset path is what isDir answers, a directory named with brackets too,
|
||||
// and off the target host the user's answer, a single file too; without one
|
||||
// it is a directory when it ends in '/', a pattern when it holds a glob, and
|
||||
// otherwise ErrPathKind asks
|
||||
// A preset path ending in '/' is a directory, there yet or not; any other is
|
||||
// what isDir answers, a directory named with brackets too: this host's,
|
||||
// reading ~ as home, or off it the user's answer, a single file too; without
|
||||
// one it is a pattern when it holds a glob, and otherwise ErrPathKind asks
|
||||
func TestPresetPathKind(t *testing.T) {
|
||||
home := t.TempDir()
|
||||
t.Setenv("HOME", home)
|
||||
if err := os.Mkdir(filepath.Join(home, "logs"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
yes := func(string) (bool, error) { return true, nil }
|
||||
file := Answers(map[string]bool{"/srv/app.log": false})
|
||||
for _, c := range []struct {
|
||||
@@ -134,7 +141,8 @@ func TestPresetPathKind(t *testing.T) {
|
||||
isDir IsDir
|
||||
want string
|
||||
}{{"/srv/app/", nil, "/srv/app/ *"}, {"/srv/app/*.log", nil, "/srv/app *.log"}, {"/srv/app[1]", yes, "/srv/app[1] *"}, {"/srv/app", nil, ""},
|
||||
{"/srv/app.log", file, "/srv app.log"}, {"/srv/app", file, ""}, {"/srv/app/", file, "/srv/app/ *"}} {
|
||||
{"/srv/app.log", file, "/srv app.log"}, {"/srv/app", file, ""}, {"/srv/app/", file, "/srv/app/ *"},
|
||||
{"~/logs", HostIsDir, "~/logs *"}, {home + "/later/", HostIsDir, home + "/later/ *"}} {
|
||||
p, err := ExpandPreset("tail", map[string]string{"path": c.path}, c.isDir)
|
||||
if c.want == "" {
|
||||
if !errors.Is(err, ErrPathKind) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -234,7 +235,8 @@ func settingKeys(v reflect.Value, prefix string) []Key {
|
||||
|
||||
// Decode reads a plugin's config map into its options: unknown keys, kinds,
|
||||
// the defaults, the tag rules, the tls, auth and acl rules of its side, then the
|
||||
// options' own Check. It reads no file.
|
||||
// options' own Check; a leading ~ in a path is the home directory. It reads no
|
||||
// file.
|
||||
func Decode[T any](role, typ string, m map[string]any) (*T, error) {
|
||||
v, err := decode(role, typ, m)
|
||||
if err != nil {
|
||||
@@ -263,6 +265,7 @@ func decode(role, typ string, m map[string]any) (any, error) {
|
||||
if err := Settle(v.Interface()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
expandPaths(v.Elem())
|
||||
if n, ok := v.Interface().(networked); ok {
|
||||
if err := p.checkNetwork(n); err != nil {
|
||||
return nil, err
|
||||
@@ -287,6 +290,32 @@ func Settle(v any) error {
|
||||
// Fill gives a table its defaults and checks nothing: Settle's first step
|
||||
func Fill(v any) { settle(reflect.ValueOf(v).Elem()) }
|
||||
|
||||
// expandPaths reads ~ (ExpandHome) in the dir and file keys of a table, its
|
||||
// tables' too, where lw reads them; the forms keep ~ as typed
|
||||
func expandPaths(v reflect.Value) {
|
||||
for _, o := range options(v.Type()) {
|
||||
switch f := v.Field(o.index); {
|
||||
case f.Kind() == reflect.Pointer && !f.IsNil() && f.Elem().Kind() == reflect.Struct:
|
||||
expandPaths(f.Elem())
|
||||
case o.Hint == "dir" || o.Hint == "file":
|
||||
f.SetString(ExpandHome(f.String()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ExpandHome reads a leading ~ or ~/ as the home directory; ~user, and a
|
||||
// host without a home directory (js/wasm), keep the path as it is
|
||||
func ExpandHome(path string) string {
|
||||
rest, ok := strings.CutPrefix(path, "~")
|
||||
if !ok || rest != "" && !os.IsPathSeparator(rest[0]) {
|
||||
return path
|
||||
}
|
||||
if home, err := os.UserHomeDir(); err == nil {
|
||||
return home + rest
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// preset sets every default before decoding, so a switch that defaults on
|
||||
// stays on unless set off; it does so in the tables m sets
|
||||
func preset(v reflect.Value, m map[string]any) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"math"
|
||||
"os"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -199,3 +200,34 @@ func TestTableKeysFollowThePlugin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A leading ~ or ~/ in a dir or file key is the home directory where lw reads
|
||||
// it, in a plugin's options, its tables too, while the configuration keeps it
|
||||
// as typed, the settings' too (lw's log reads it at startup); ~user, other
|
||||
// keys and a host without a home directory keep it
|
||||
func TestTildeIsHomeInPathKeys(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
home := os.Getenv("HOME")
|
||||
src, err := Decode[FileSourceOptions]("source", "file", map[string]any{"directory": "~/logs", "pattern": "~*"})
|
||||
if err != nil || src.Directory != home+"/logs" || src.Pattern != "~*" {
|
||||
t.Fatalf("file source: %+v %v", src, err)
|
||||
}
|
||||
tls := map[string]any{"enabled": true, "ca_file": "~", "cert_file": "~/c", "key_file": "~other/k"}
|
||||
sink, err := Decode[TCPChainSinkOptions]("sink", "tcp_chain", map[string]any{"host": "h", "port": int64(1), "tls": tls})
|
||||
if err != nil || sink.TLS.CAFile != home || sink.TLS.CertFile != home+"/c" || sink.TLS.KeyFile != "~other/k" {
|
||||
t.Fatalf("tls table: %+v %v", sink.TLS, err)
|
||||
}
|
||||
m, err := Load(Args{Overrides: []string{"--logging.file.directory=~/lw"}, Specs: specs("source", "file:directory=~/logs")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer m.Close()
|
||||
cfg, err := m.Snapshot()
|
||||
if err != nil || cfg.Logging.File.Directory != "~/lw" || cfg.Pipelines[0].PluginSources[0].Config["directory"] != "~/logs" {
|
||||
t.Fatalf("as typed: %+v %v %v", cfg.Logging.File, cfg.Pipelines, err)
|
||||
}
|
||||
t.Setenv("HOME", "")
|
||||
if src, err := Decode[FileSourceOptions]("source", "file", map[string]any{"directory": "~/logs"}); err != nil || src.Directory != "~/logs" {
|
||||
t.Fatalf("without a home directory: %+v %v", src, err)
|
||||
}
|
||||
}
|
||||
@@ -172,12 +172,13 @@ func TestSpecPipelinesReplaceFilePipelines(t *testing.T) {
|
||||
}
|
||||
|
||||
// A spec pipeline without a source reads stdin and one without a sink writes
|
||||
// stdout, as a filter does; two pipelines cannot both read stdin.
|
||||
// stdout, as a filter does, each part under its type's own id; two pipelines
|
||||
// cannot both read stdin.
|
||||
func TestSpecPipelinesDefaultToStdio(t *testing.T) {
|
||||
isolateConfig(t)
|
||||
got := loadPipelines(t, "filter", "include:patterns=ERROR")
|
||||
if p := got[0]; len(p.PluginSources) != 1 || p.PluginSources[0].Type != "console" || p.PluginSources[0].ID != "stdin" ||
|
||||
len(p.PluginSinks) != 1 || p.PluginSinks[0].Type != "console" || p.PluginSinks[0].ID != "stdout" {
|
||||
if p := got[0]; len(p.PluginSources) != 1 || p.PluginSources[0].Type != "console" || p.PluginSources[0].ID != "console" ||
|
||||
len(p.PluginSinks) != 1 || p.PluginSinks[0].Type != "console" || p.PluginSinks[0].ID != "console" {
|
||||
t.Fatalf("pipeline: %+v", p)
|
||||
}
|
||||
_, err := Load(Args{Specs: specs("pipeline", "a", "sink", "null", "pipeline", "b", "sink", "null")})
|
||||
|
||||
Reference in new issue
Block a user