v0.26.4 cli enhancements
This commit is contained in:
1 parent
c00c076d9b
commit
b2f11f6104
11 files changed
+173
-81
No files matched your search
+41
-16
@@ -6,6 +6,7 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"regexp"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -72,8 +73,10 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
s := c.subcommands[i]
|
s := c.subcommands[i]
|
||||||
fs := flag.NewFlagSet("lw "+c.name+" "+s.name, flag.ContinueOnError)
|
fs := flag.NewFlagSet("lw "+c.name+" "+s.name, flag.ContinueOnError)
|
||||||
fs.SetOutput(stderr)
|
// flag names a long flag with one dash: its errors, and the usage, print below
|
||||||
fs.Usage = func() {
|
fs.SetOutput(io.Discard)
|
||||||
|
fs.Usage = func() {}
|
||||||
|
usage := func() {
|
||||||
fmt.Fprintf(stderr, "Usage: %s %s\n\n%s.\n\n", fs.Name(), s.synopsis, s.summary)
|
fmt.Fprintf(stderr, "Usage: %s %s\n\n%s.\n\n", fs.Name(), s.synopsis, s.summary)
|
||||||
printFlags(stderr, fs)
|
printFlags(stderr, fs)
|
||||||
}
|
}
|
||||||
@@ -83,13 +86,16 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int {
|
|||||||
fs.Var(f.Value, letter, "") // no usage: printFlags shows it with its long flag
|
fs.Var(f.Value, letter, "") // no usage: printFlags shows it with its long flag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := fs.Parse(args[1:]); err != nil {
|
err := fs.Parse(args[1:])
|
||||||
if errors.Is(err, flag.ErrHelp) {
|
switch {
|
||||||
|
case errors.Is(err, flag.ErrHelp):
|
||||||
|
usage()
|
||||||
return 0
|
return 0
|
||||||
|
case err != nil:
|
||||||
|
err = usageError(longFlag.ReplaceAllString(err.Error(), "$1--$2"))
|
||||||
|
default:
|
||||||
|
err = checkArgs(fs, s.required)
|
||||||
}
|
}
|
||||||
return 2 // already reported by flag, with the usage
|
|
||||||
}
|
|
||||||
err := checkArgs(fs, s.required)
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = work(stdout, stderr)
|
err = work(stdout, stderr)
|
||||||
}
|
}
|
||||||
@@ -98,12 +104,15 @@ func (c *command) run(args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
fmt.Fprintf(stderr, "%s: %v\n", fs.Name(), err)
|
fmt.Fprintf(stderr, "%s: %v\n", fs.Name(), err)
|
||||||
if _, ok := errors.AsType[usageError](err); ok {
|
if _, ok := errors.AsType[usageError](err); ok {
|
||||||
fs.Usage()
|
usage()
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// longFlag is a flag name of two or more letters in flag's errors
|
||||||
|
var longFlag = regexp.MustCompile(`(^|\s)-(\w[\w-]+)`)
|
||||||
|
|
||||||
func (c *command) usage(w io.Writer) {
|
func (c *command) usage(w io.Writer) {
|
||||||
fmt.Fprintf(w, "Usage: lw %s COMMAND [flags]\n\n", c.name)
|
fmt.Fprintf(w, "Usage: lw %s COMMAND [flags]\n\n", c.name)
|
||||||
for _, s := range c.subcommands {
|
for _, s := range c.subcommands {
|
||||||
@@ -162,9 +171,9 @@ type invocation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parseCommandLine reads lw's own flags, a short one as its long one; any
|
// parseCommandLine reads lw's own flags, a short one as its long one; any
|
||||||
// other --flag is a --path=value setting for lixenwraith/config, which reports
|
// other --flag is a setting, passed to lixenwraith/config as --path=value or a
|
||||||
// what it does not know. A pipeline flag takes the next argument unless it
|
// bare switch so it never guesses. A flag's value is the next argument unless
|
||||||
// starts with '-'. A help request wins over a malformed flag.
|
// that starts with '-'. Every error is a usage error; a help request wins.
|
||||||
func parseCommandLine(argv []string) (inv invocation, err error) {
|
func parseCommandLine(argv []string) (inv invocation, err error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if inv.help {
|
if inv.help {
|
||||||
@@ -175,9 +184,11 @@ func parseCommandLine(argv []string) (inv invocation, err error) {
|
|||||||
if i := slices.IndexFunc(commands, func(c command) bool { return c.name == argv[0] }); i >= 0 {
|
if i := slices.IndexFunc(commands, func(c command) bool { return c.name == argv[0] }); i >= 0 {
|
||||||
return invocation{command: &commands[i], args: argv[1:]}, nil
|
return invocation{command: &commands[i], args: argv[1:]}, nil
|
||||||
}
|
}
|
||||||
inv.help = argv[0] == "help"
|
|
||||||
}
|
}
|
||||||
specFlags := config.SpecFlags()
|
specFlags, settings := config.SpecFlags(), config.Settings()
|
||||||
|
unexpected := func(word string) error {
|
||||||
|
return fmt.Errorf("unexpected argument %q: lw takes options only (a configuration file is -c FILE, a switch's value follows '=')", word)
|
||||||
|
}
|
||||||
next := func(i *int) string {
|
next := func(i *int) string {
|
||||||
if *i+1 < len(argv) && !strings.HasPrefix(argv[*i+1], "-") {
|
if *i+1 < len(argv) && !strings.HasPrefix(argv[*i+1], "-") {
|
||||||
*i++
|
*i++
|
||||||
@@ -194,9 +205,11 @@ func parseCommandLine(argv []string) (inv invocation, err error) {
|
|||||||
}
|
}
|
||||||
switch {
|
switch {
|
||||||
case arg == "--":
|
case arg == "--":
|
||||||
inv.load.Overrides = append(inv.load.Overrides, argv[i:]...)
|
if i+1 < len(argv) {
|
||||||
|
err = cmp.Or(err, unexpected(argv[i+1]))
|
||||||
|
}
|
||||||
return inv, err
|
return inv, err
|
||||||
case arg == "--help":
|
case arg == "--help" || i == 0 && arg == "help":
|
||||||
inv.help = true
|
inv.help = true
|
||||||
case name == "--config":
|
case name == "--config":
|
||||||
if !inline {
|
if !inline {
|
||||||
@@ -215,9 +228,21 @@ func parseCommandLine(argv []string) (inv invocation, err error) {
|
|||||||
}
|
}
|
||||||
inv.load.Specs = append(inv.load.Specs, config.Spec{Flag: name[2:], Value: value})
|
inv.load.Specs = append(inv.load.Specs, config.Spec{Flag: name[2:], Value: value})
|
||||||
case len(name) > 1 && name[0] == '-' && unicode.IsLetter(rune(name[1])):
|
case len(name) > 1 && name[0] == '-' && unicode.IsLetter(rune(name[1])):
|
||||||
// config reads only --flags; a negative number stays a value
|
// lw's only single-dash options are the shorts
|
||||||
err = cmp.Or(err, fmt.Errorf("unknown option %s: long options take two dashes, and a value that starts with '-' follows '=' (lw --help lists them)", typed))
|
err = cmp.Or(err, fmt.Errorf("unknown option %s: long options take two dashes, and a value that starts with '-' follows '=' (lw --help lists them)", typed))
|
||||||
|
case !strings.HasPrefix(name, "--"):
|
||||||
|
err = cmp.Or(err, unexpected(arg))
|
||||||
default:
|
default:
|
||||||
|
isSwitch, known := settings[name[2:]]
|
||||||
|
if !known {
|
||||||
|
err = cmp.Or(err, fmt.Errorf("unknown option %s (lw --help lists them)", typed))
|
||||||
|
}
|
||||||
|
if !inline && !isSwitch {
|
||||||
|
if value = next(&i); value == "" && known {
|
||||||
|
err = cmp.Or(err, fmt.Errorf("%s needs a value; one that starts with '-' follows '='", typed))
|
||||||
|
}
|
||||||
|
arg += "=" + value
|
||||||
|
}
|
||||||
inv.load.Overrides = append(inv.load.Overrides, arg)
|
inv.load.Overrides = append(inv.load.Overrides, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+68
-10
@@ -7,6 +7,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -16,9 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// lw's own flags: a short is its long flag, an unknown single-dash one fails,
|
// lw's own flags: a short is its long flag, an unknown single-dash one fails,
|
||||||
// the last -c wins, a pipeline flag or --color takes the next argument unless
|
// the last -c wins, a flag takes the next argument unless it starts with '-',
|
||||||
// it starts with '-', -- ends lw's flags, and a command is only the first
|
// a switch takes none, and a command is only the first argument. A setting
|
||||||
// argument. Everything else is a setting for config.
|
// reaches config as --path=value or a bare switch.
|
||||||
func TestCommandLineGrammar(t *testing.T) {
|
func TestCommandLineGrammar(t *testing.T) {
|
||||||
for _, c := range []struct {
|
for _, c := range []struct {
|
||||||
argv []string
|
argv []string
|
||||||
@@ -30,15 +31,14 @@ func TestCommandLineGrammar(t *testing.T) {
|
|||||||
invocation{load: config.Args{Specs: []config.Spec{{Flag: "source", Value: "null"}, {Flag: "sink", Value: "http,port=1"}, {Flag: "filter"}, {Flag: "sink"}}}}},
|
invocation{load: config.Args{Specs: []config.Spec{{Flag: "source", Value: "null"}, {Flag: "sink", Value: "http,port=1"}, {Flag: "filter"}, {Flag: "sink"}}}}},
|
||||||
{[]string{"--preset", "tail,path=x", "--pipeline", "b"}, invocation{load: config.Args{Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}, {Flag: "pipeline", Value: "b"}}}}},
|
{[]string{"--preset", "tail,path=x", "--pipeline", "b"}, invocation{load: config.Args{Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}, {Flag: "pipeline", Value: "b"}}}}},
|
||||||
{[]string{"--color", "--color", "never", "--color=auto"}, invocation{load: config.Args{Overrides: []string{"--color=always", "--color=never", "--color=auto"}}}},
|
{[]string{"--color", "--color", "never", "--color=auto"}, invocation{load: config.Args{Overrides: []string{"--color=always", "--color=never", "--color=auto"}}}},
|
||||||
{[]string{"--quiet", "--", "-c", "x", "--source", "null"}, invocation{load: config.Args{Overrides: []string{"--quiet", "--", "-c", "x", "--source", "null"}}}},
|
{[]string{"--quiet", "--"}, invocation{load: config.Args{Overrides: []string{"--quiet"}}}},
|
||||||
{[]string{"--quiet", "-h"}, invocation{help: true, load: config.Args{Overrides: []string{"--quiet"}}}},
|
{[]string{"--quiet", "-h"}, invocation{help: true, load: config.Args{Overrides: []string{"--quiet"}}}},
|
||||||
{[]string{"-c", "-h"}, invocation{help: true}},
|
{[]string{"-c", "-h"}, invocation{help: true}},
|
||||||
{[]string{"help"}, invocation{help: true, load: config.Args{Overrides: []string{"help"}}}},
|
{[]string{"help"}, invocation{help: true}},
|
||||||
{[]string{"tls", "ca", "-h"}, invocation{command: &commands[1], args: []string{"ca", "-h"}}},
|
{[]string{"tls", "ca", "-h"}, invocation{command: &commands[1], args: []string{"ca", "-h"}}},
|
||||||
{[]string{"--quiet", "tls"}, invocation{load: config.Args{Overrides: []string{"--quiet", "tls"}}}},
|
{[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.level", "debug", "--logging.file.retention_hours=-1", "--dump=false"},
|
||||||
{[]string{"-q", "-t", "-V", "-p", "tail,path=x", "-c=a.toml", "--logging.file.retention_hours", "-1"},
|
|
||||||
invocation{load: config.Args{File: "a.toml", Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}},
|
invocation{load: config.Args{File: "a.toml", Specs: []config.Spec{{Flag: "preset", Value: "tail,path=x"}},
|
||||||
Overrides: []string{"--quiet", "--check", "--version", "--logging.file.retention_hours", "-1"}}}},
|
Overrides: []string{"--quiet", "--check", "--version", "--logging.level=debug", "--logging.file.retention_hours=-1", "--dump=false"}}}},
|
||||||
} {
|
} {
|
||||||
got, err := parseCommandLine(c.argv)
|
got, err := parseCommandLine(c.argv)
|
||||||
if err != nil || !reflect.DeepEqual(got, c.want) {
|
if err != nil || !reflect.DeepEqual(got, c.want) {
|
||||||
@@ -46,13 +46,56 @@ func TestCommandLineGrammar(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, argv := range [][]string{{"-c"}, {"--config"}, {"--config="}, {"-c="}, {"-c", "--quiet"},
|
for _, argv := range [][]string{{"-c"}, {"--config"}, {"--config="}, {"-c="}, {"-c", "--quiet"},
|
||||||
{"-v"}, {"-qt"}, {"-u", "x"}, {"-config", "x"}} {
|
{"-v"}, {"-qt"}, {"-u", "x"}, {"-config", "x"}, {"--logging.level"}, {"--logging.file.retention_hours", "-1"}} {
|
||||||
if _, err := parseCommandLine(argv); err == nil {
|
if _, err := parseCommandLine(argv); err == nil {
|
||||||
t.Errorf("%q accepted", argv)
|
t.Errorf("%q accepted", argv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lw takes no positional argument: a word no flag takes is named, not passed
|
||||||
|
// to config, whose bool would take it as its value
|
||||||
|
func TestAWordNoFlagTakesIsAnError(t *testing.T) {
|
||||||
|
for _, c := range []struct {
|
||||||
|
argv []string
|
||||||
|
word string
|
||||||
|
}{
|
||||||
|
{[]string{"tail"}, "tail"}, {[]string{"-t", "x.toml"}, "x.toml"}, {[]string{"--quiet", "tls"}, "tls"},
|
||||||
|
{[]string{"--status_reporter", "false"}, "false"}, {[]string{"--logging.level", "debug", "x"}, "x"},
|
||||||
|
{[]string{"--color=never", "x"}, "x"}, {[]string{"--", "-c"}, "-c"}, {[]string{"-1"}, "-1"},
|
||||||
|
} {
|
||||||
|
_, err := parseCommandLine(c.argv)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unexpected argument "+strconv.Quote(c.word)) {
|
||||||
|
t.Errorf("%q: %v", c.argv, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// An option lw does not know is an error, whatever follows it
|
||||||
|
func TestAnUnknownOptionIsAnError(t *testing.T) {
|
||||||
|
for _, argv := range [][]string{{"--nosuch"}, {"--nosuch=1"}, {"--nosuch", "x"}, {"--check", "--nosuch"}} {
|
||||||
|
_, err := parseCommandLine(argv)
|
||||||
|
if err == nil || err.Error() != "unknown option --nosuch (lw --help lists them)" {
|
||||||
|
t.Errorf("%q: %v", argv, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// flag writes a long flag with one dash; lw names it with two, then the usage
|
||||||
|
func TestFlagErrorsNameLongFlagsWithTwoDashes(t *testing.T) {
|
||||||
|
for args, want := range map[string]string{
|
||||||
|
"--user": "lw auth add-user: flag needs an argument: --user\nUsage: ",
|
||||||
|
"-u": "lw auth add-user: flag needs an argument: -u\nUsage: ",
|
||||||
|
"-nosuch": "lw auth add-user: flag provided but not defined: --nosuch\nUsage: ",
|
||||||
|
"--user=x": "lw auth add-user: --credentials is required\nUsage: ",
|
||||||
|
} {
|
||||||
|
code, _, stderr := runCommand(t, "auth", "add-user", args)
|
||||||
|
if code != 2 || !strings.HasPrefix(stderr, want) || strings.Count(stderr, "Usage: ") != 1 {
|
||||||
|
t.Errorf("lw auth add-user %s: exit %d:\n%s", args, code, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Every subcommand's -h lists each flag once, GNU style, with its short form
|
// Every subcommand's -h lists each flag once, GNU style, with its short form
|
||||||
// beside it: " -u, --user NAME" or " --credentials FILE"
|
// beside it: " -u, --user NAME" or " --credentials FILE"
|
||||||
func TestUsageListsEachFlagOnce(t *testing.T) {
|
func TestUsageListsEachFlagOnce(t *testing.T) {
|
||||||
@@ -63,6 +106,9 @@ func TestUsageListsEachFlagOnce(t *testing.T) {
|
|||||||
s.define(fs)
|
s.define(fs)
|
||||||
want := 0
|
want := 0
|
||||||
fs.VisitAll(func(f *flag.Flag) {
|
fs.VisitAll(func(f *flag.Flag) {
|
||||||
|
if f.Usage == "" {
|
||||||
|
return // an earlier name, unlisted
|
||||||
|
}
|
||||||
want++
|
want++
|
||||||
head := " --" + f.Name
|
head := " --" + f.Name
|
||||||
for letter, short := range shorts {
|
for letter, short := range shorts {
|
||||||
@@ -139,13 +185,25 @@ func TestEveryPresetBuilds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --host, the earlier name of lw tls cert --hosts, still sets the hosts
|
||||||
|
func TestTLSCertHostIsHosts(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
if code, _, stderr := runCommand(t, "tls", "ca", "--dir", dir); code != 0 {
|
||||||
|
t.Fatalf("ca: exit %d: %s", code, stderr)
|
||||||
|
}
|
||||||
|
code, _, stderr := runCommand(t, "tls", "cert", "--ca-dir", dir, "--name", "agg", "--server", "--host", "a.example")
|
||||||
|
if code != 0 || !strings.Contains(stderr, "for [a.example CN=agg]") {
|
||||||
|
t.Fatalf("exit %d: %s", code, stderr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// lw tls ca and lw tls cert write a chain a dialer verifies by the CA file,
|
// lw tls ca and lw tls cert write a chain a dialer verifies by the CA file,
|
||||||
// keys private, and never replace a file
|
// keys private, and never replace a file
|
||||||
func TestTLSCommandsIssueAVerifiableChain(t *testing.T) {
|
func TestTLSCommandsIssueAVerifiableChain(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
steps := [][]string{
|
steps := [][]string{
|
||||||
{"ca", "-dir", dir, "-name", "test CA"},
|
{"ca", "-dir", dir, "-name", "test CA"},
|
||||||
{"cert", "-ca-dir", dir, "-name", "agg", "-server", "-host", "127.0.0.1"},
|
{"cert", "-ca-dir", dir, "-name", "agg", "-server", "-hosts", "127.0.0.1"},
|
||||||
}
|
}
|
||||||
for _, args := range steps {
|
for _, args := range steps {
|
||||||
if code, _, stderr := runCommand(t, "tls", args...); code != 0 {
|
if code, _, stderr := runCommand(t, "tls", args...); code != 0 {
|
||||||
|
|||||||
+4
-5
@@ -23,8 +23,8 @@ Usage:
|
|||||||
Commands (lw COMMAND -h lists its subcommands):
|
Commands (lw COMMAND -h lists its subcommands):
|
||||||
%s
|
%s
|
||||||
Options: a short option is its long one; a value follows as the next argument
|
Options: a short option is its long one; a value follows as the next argument
|
||||||
or after '=' (-c FILE, --config=FILE), one that starts with '-' after '=';
|
or after '=' (-c FILE, --config=FILE), one that starts with '-' or a switch's
|
||||||
short options do not combine.
|
after '='; short options do not combine. lw takes options only.
|
||||||
-c, --config FILE Configuration file (default:
|
-c, --config FILE Configuration file (default:
|
||||||
~/.config/logwisp/logwisp.toml if it exists,
|
~/.config/logwisp/logwisp.toml if it exists,
|
||||||
else ./logwisp.toml)
|
else ./logwisp.toml)
|
||||||
@@ -94,9 +94,8 @@ Signals:
|
|||||||
|
|
||||||
Exit codes:
|
Exit codes:
|
||||||
0 success, including the end of input
|
0 success, including the end of input
|
||||||
1 general error, including an error in lw's own options (lw COMMAND
|
1 general error, including a configuration that does not load
|
||||||
exits 2 on a usage error)
|
2 usage error, or a named configuration file not found
|
||||||
2 configuration file not found
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func printHelp() {
|
func printHelp() {
|
||||||
|
|||||||
+1
-1
@@ -24,7 +24,7 @@ func main() {
|
|||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(2) // every command-line error is a usage error
|
||||||
case inv.command != nil:
|
case inv.command != nil:
|
||||||
os.Exit(inv.command.run(inv.args, os.Stdout, os.Stderr))
|
os.Exit(inv.command.run(inv.args, os.Stdout, os.Stderr))
|
||||||
case inv.help:
|
case inv.help:
|
||||||
|
|||||||
+3
-2
@@ -23,7 +23,7 @@ var tlsCommands = []subcommand{
|
|||||||
{"ca", "--dir DIR [--name NAME] [--days N]",
|
{"ca", "--dir DIR [--name NAME] [--days N]",
|
||||||
"Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only",
|
"Create a certificate authority, DIR/ca.crt and DIR/ca.key, that signs certificates only",
|
||||||
[]string{"dir"}, defineCA},
|
[]string{"dir"}, defineCA},
|
||||||
{"cert", "--ca-dir DIR --name NAME [--host NAME,...] [--server] [--client] [--days N] [--out DIR]",
|
{"cert", "--ca-dir DIR --name NAME [--hosts NAME,...] [--server] [--client] [--days N] [--out DIR]",
|
||||||
"Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key",
|
"Issue a certificate and key, OUT/NAME.crt and OUT/NAME.key, from DIR/ca.crt and DIR/ca.key",
|
||||||
[]string{"ca-dir", "name"}, defineCert},
|
[]string{"ca-dir", "name"}, defineCert},
|
||||||
}
|
}
|
||||||
@@ -53,7 +53,8 @@ func defineCA(fs *flag.FlagSet) func(stdout, stderr io.Writer) error {
|
|||||||
func defineCert(fs *flag.FlagSet) func(stdout, stderr io.Writer) error {
|
func defineCert(fs *flag.FlagSet) func(stdout, stderr io.Writer) error {
|
||||||
caDir := fs.String("ca-dir", "", "`dir`ectory holding ca.crt and ca.key (lw tls ca)")
|
caDir := fs.String("ca-dir", "", "`dir`ectory holding ca.crt and ca.key (lw tls ca)")
|
||||||
name := fs.String("name", "", "subject common `name`, the mtls identity, and the file names")
|
name := fs.String("name", "", "subject common `name`, the mtls identity, and the file names")
|
||||||
hosts := fs.String("host", "", "DNS `names` and IP addresses, ',' between them (default with --server: --name)")
|
hosts := fs.String("hosts", "", "DNS `names` and IP addresses, ',' between them (default with --server: --name)")
|
||||||
|
fs.Var(fs.Lookup("hosts").Value, "host", "") // its earlier name, unlisted like a short
|
||||||
server := fs.Bool("server", false, "for listeners (TLS server authentication)")
|
server := fs.Bool("server", false, "for listeners (TLS server authentication)")
|
||||||
client := fs.Bool("client", false, "for dialers presenting a certificate (TLS client authentication)")
|
client := fs.Bool("client", false, "for dialers presenting a certificate (TLS client authentication)")
|
||||||
days := fs.Int("days", int(tlsx.LeafValidity/(24*time.Hour)), "validity in `n` days, at most the CA's")
|
days := fs.Int("days", int(tlsx.LeafValidity/(24*time.Hour)), "validity in `n` days, at most the CA's")
|
||||||
|
|||||||
+17
-11
@@ -33,8 +33,12 @@ manages SCRAM credentials and logs in to `scram` listeners
|
|||||||
- A value follows as the next argument or after `=` (`-c FILE`, `-c=FILE`,
|
- A value follows as the next argument or after `=` (`-c FILE`, `-c=FILE`,
|
||||||
`--config FILE`, `--config=FILE`); a value that starts with `-` follows `=`,
|
`--config FILE`, `--config=FILE`); a value that starts with `-` follows `=`,
|
||||||
and `--` ends the options.
|
and `--` ends the options.
|
||||||
|
- A switch (`--check`, `--quiet`, any boolean key) takes no separate value:
|
||||||
|
`--quiet=false`, not `--quiet false`.
|
||||||
- Short options do not combine (`-q -t`, not `-qt`) or take an attached value
|
- Short options do not combine (`-q -t`, not `-qt`) or take an attached value
|
||||||
(`-cFILE`); an unknown single-dash option is an error.
|
(`-cFILE`).
|
||||||
|
- lw takes options only. An unknown option and a word no option takes (`lw
|
||||||
|
tail`, `lw -t FILE`) are usage errors, named on stderr.
|
||||||
- lw's multiword options join words with `-` (`--rate-limit`,
|
- lw's multiword options join words with `-` (`--rate-limit`,
|
||||||
`--password-file`); configuration keys, spec keys and preset keys keep
|
`--password-file`); configuration keys, spec keys and preset keys keep
|
||||||
TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`,
|
TOML's `_` and `.` (`--status_reporter`, `--sink http,tls.cert_file=F`,
|
||||||
@@ -48,7 +52,7 @@ pipelines have [their own flags](#pipelines):
|
|||||||
```
|
```
|
||||||
--<path>=<value> e.g. --logging.level=debug
|
--<path>=<value> e.g. --logging.level=debug
|
||||||
--<path> <value> e.g. --logging.level debug
|
--<path> <value> e.g. --logging.level debug
|
||||||
--<path> bare flag: true, for a boolean path only
|
--<path> a switch: true, for a boolean path only
|
||||||
```
|
```
|
||||||
|
|
||||||
### Common
|
### Common
|
||||||
@@ -309,14 +313,16 @@ service untouched; see [Configuration](configuration.md#hot-reload).
|
|||||||
|
|
||||||
- `0`: the end of input, a clean shutdown (`SIGINT`, `SIGTERM`), `--version` /
|
- `0`: the end of input, a clean shutdown (`SIGINT`, `SIGTERM`), `--version` /
|
||||||
`--help`, `--dump`, or a valid `--check`
|
`--help`, `--dump`, or a valid `--check`
|
||||||
- `1`: general error: a command-line error (an unknown option, a missing
|
- `1`: general error: a configuration load or validation failure (`--check`
|
||||||
`-c` path), a configuration load or validation failure (`--check`
|
included, and a bad value on the command line), a logger init failure, a
|
||||||
included), a logger init failure, a service bootstrap failure
|
service bootstrap failure
|
||||||
- `2`: an explicitly requested configuration file is not found
|
- `2`: a usage error (an unknown option, a word no option takes, a missing
|
||||||
|
value or `-c` path), or an explicitly requested configuration file that is
|
||||||
|
not found
|
||||||
- killed by `SIGPIPE` (shell status `141`): the reader of stdout went away, as
|
- killed by `SIGPIPE` (shell status `141`): the reader of stdout went away, as
|
||||||
in `lw | head`; `cat` ends the same way
|
in `lw | head`; `cat` ends the same way
|
||||||
|
|
||||||
Exit code 2 applies only when the file was named explicitly (`-c`,
|
A missing file is exit code 2 only when it was named explicitly (`-c`,
|
||||||
`--config=`, or the `LOGWISP_CONFIG_*` variables). A missing discovered default
|
`--config=`, or the `LOGWISP_CONFIG_*` variables). A missing discovered default
|
||||||
is not an error, and LogWisp starts on built-in defaults.
|
is not an error, and LogWisp starts on built-in defaults.
|
||||||
|
|
||||||
@@ -511,7 +517,7 @@ Makes ECDSA P-256 certificates for `tls` blocks. It never replaces a file
|
|||||||
|
|
||||||
```
|
```
|
||||||
lw tls ca --dir DIR [--name NAME] [--days 3650]
|
lw tls ca --dir DIR [--name NAME] [--days 3650]
|
||||||
lw tls cert --ca-dir DIR --name NAME [--host NAME,...] [--server] [--client]
|
lw tls cert --ca-dir DIR --name NAME [--hosts NAME,...] [--server] [--client]
|
||||||
[--days 397] [--out DIR]
|
[--days 397] [--out DIR]
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -520,9 +526,9 @@ lw tls cert --ca-dir DIR --name NAME [--host NAME,...] [--server] [--client]
|
|||||||
`tls.client_ca_file`; with `ca.key` it is a listener's
|
`tls.client_ca_file`; with `ca.key` it is a listener's
|
||||||
`tls.issuer_cert_file` and `tls.issuer_key_file`.
|
`tls.issuer_cert_file` and `tls.issuer_key_file`.
|
||||||
- `cert`: `NAME.crt` and `NAME.key`, in `--out` (default: `--ca-dir`).
|
- `cert`: `NAME.crt` and `NAME.key`, in `--out` (default: `--ca-dir`).
|
||||||
- `--server` for a listener: `--host` lists the DNS names and IP addresses it
|
- `--server` for a listener: `--hosts` lists the DNS names and IP addresses
|
||||||
carries, default `NAME`; `--client` for a dialer presenting a certificate;
|
it carries, default `NAME` (`--host`, its earlier name, still works);
|
||||||
both may be given.
|
`--client` for a dialer presenting a certificate; both may be given.
|
||||||
- `NAME` is the subject CN, the identity `auth.type = "mtls"` matches.
|
- `NAME` is the subject CN, the identity `auth.type = "mtls"` matches.
|
||||||
- Valid 397 days (the longest browsers accept), never past the CA. It prints
|
- Valid 397 days (the longest browsers accept), never past the CA. It prints
|
||||||
the certificate's `pin_sha256`.
|
the certificate's `pin_sha256`.
|
||||||
|
|||||||
+6
-11
@@ -234,17 +234,12 @@ lw --logging.level debug # space form also works
|
|||||||
lw --quiet # bare flag means true
|
lw --quiet # bare flag means true
|
||||||
```
|
```
|
||||||
|
|
||||||
A bare flag is accepted only for a boolean key: `--logging.file.directory
|
A bare flag is accepted only for a boolean key, which takes its value only
|
||||||
--quiet` fails with `--logging.file.directory needs a value`. Unrecognized `--`
|
after `=`: `--logging.file.directory --quiet` fails with
|
||||||
flags and positional words are reported on stderr before the logger exists and
|
`--logging.file.directory needs a value`, and `--quiet false` with `unexpected
|
||||||
are then ignored:
|
argument "false"`. An unknown key is an error too (`unknown option
|
||||||
|
--nosuch.key`); each is a usage error, exit code 2. Single-dash options are
|
||||||
```
|
lw's own ([CLI](cli.md#options)).
|
||||||
Warning: unrecognized flags ignored: [nosuch.key]
|
|
||||||
```
|
|
||||||
|
|
||||||
Single-dash options are lw's own ([CLI](cli.md#options)); an unknown one is
|
|
||||||
an error.
|
|
||||||
|
|
||||||
An empty `LOGWISP_<PATH>` variable counts as unset, as the pipeline variables
|
An empty `LOGWISP_<PATH>` variable counts as unset, as the pipeline variables
|
||||||
do.
|
do.
|
||||||
|
|||||||
@@ -112,10 +112,13 @@ that stands for it in every command that has it.
|
|||||||
A value follows as the next argument or after
|
A value follows as the next argument or after
|
||||||
.Ql = ;
|
.Ql = ;
|
||||||
a value that starts with
|
a value that starts with
|
||||||
.Ql -
|
.Ql - ,
|
||||||
follows
|
and a switch's, follows
|
||||||
.Ql = .
|
.Ql = .
|
||||||
Short options do not combine, and an unknown one is an error.
|
Short options do not combine.
|
||||||
|
.Nm
|
||||||
|
takes options only: an unknown option, or a word no option takes, is a usage
|
||||||
|
error.
|
||||||
The options of
|
The options of
|
||||||
.Nm
|
.Nm
|
||||||
join words with
|
join words with
|
||||||
@@ -375,13 +378,14 @@ Create a certificate authority,
|
|||||||
and
|
and
|
||||||
.Pa dir/ca.key ,
|
.Pa dir/ca.key ,
|
||||||
valid 3650 days.
|
valid 3650 days.
|
||||||
.It Cm cert Fl \-ca\-dir Ar dir Fl \-name Ar name Oo Fl \-host Ar names Oc Oo Fl \-server Oc Oo Fl \-client Oc Oo Fl \-days Ar n Oc Op Fl \-out Ar dir
|
.It Cm cert Fl \-ca\-dir Ar dir Fl \-name Ar name Oo Fl \-hosts Ar names Oc Oo Fl \-server Oc Oo Fl \-client Oc Oo Fl \-days Ar n Oc Op Fl \-out Ar dir
|
||||||
Issue
|
Issue
|
||||||
.Pa name.crt
|
.Pa name.crt
|
||||||
and
|
and
|
||||||
.Pa name.key
|
.Pa name.key
|
||||||
from that authority, valid 397 days and never past it.
|
from that authority, valid 397 days and never past it.
|
||||||
.Fl \-host
|
.Fl \-hosts
|
||||||
|
.Pq or Fl \-host
|
||||||
lists the DNS names and addresses a server certificate carries
|
lists the DNS names and addresses a server certificate carries
|
||||||
.Pq default: Ar name ;
|
.Pq default: Ar name ;
|
||||||
.Ar name
|
.Ar name
|
||||||
@@ -547,8 +551,8 @@ The service's working directory on Linux and on
|
|||||||
.Nm
|
.Nm
|
||||||
exits 0 at the end of input, after a clean shutdown,
|
exits 0 at the end of input, after a clean shutdown,
|
||||||
.Fl \-version
|
.Fl \-version
|
||||||
or help; 1 on a command-line, configuration, logger or startup error; and 2
|
or help; 1 on a configuration, logger or startup error; and 2 on a usage
|
||||||
when a configuration file named by
|
error or when a configuration file named by
|
||||||
.Fl c
|
.Fl c
|
||||||
or
|
or
|
||||||
.Ev LOGWISP_CONFIG_*
|
.Ev LOGWISP_CONFIG_*
|
||||||
@@ -568,7 +572,7 @@ the shell reports status 141.
|
|||||||
.Cm tls
|
.Cm tls
|
||||||
and
|
and
|
||||||
.Cm preset
|
.Cm preset
|
||||||
exit 0 on success or help, 1 on failure, and 2 on a usage error.
|
do the same: 0 on success or help, 1 on failure, and 2 on a usage error.
|
||||||
.Sh SIGNALS
|
.Sh SIGNALS
|
||||||
.Bl -tag -width "SIGINT, SIGTERM"
|
.Bl -tag -width "SIGINT, SIGTERM"
|
||||||
.It Dv SIGINT , SIGTERM
|
.It Dv SIGINT , SIGTERM
|
||||||
|
|||||||
+2
-12
@@ -65,19 +65,9 @@ identity, and both skeletons download the tagged source.
|
|||||||
|
|
||||||
Smaller items found along the way, each independent of the others.
|
Smaller items found along the way, each independent of the others.
|
||||||
|
|
||||||
1. Command line:
|
1. TLS: the generated-certificate key is one per process, so a pin taken from a
|
||||||
- a positional word at the top level (`lw tail`, `lw -t FILE`) still reaches
|
|
||||||
config with an unclear message; name it ("unexpected argument X; the file
|
|
||||||
is -c FILE");
|
|
||||||
- Go's flag errors keep single dashes ("flag needs an argument: -u");
|
|
||||||
- an unknown `--key` only warns and then runs the stdin pipe; consider an
|
|
||||||
error when stdin is a terminal;
|
|
||||||
- `lw tls cert --host` could become `--hosts` (keeping `--host`), to match
|
|
||||||
`tls.hosts` and the preset key;
|
|
||||||
- top-level usage errors exit 1, subcommand ones 2.
|
|
||||||
2. TLS: the generated-certificate key is one per process, so a pin taken from a
|
|
||||||
`self_signed` listener also matches an `issuer` listener of the same
|
`self_signed` listener also matches an `issuer` listener of the same
|
||||||
process; document it or key per listener. Pins are checked in
|
process; document it or key per listener. Pins are checked in
|
||||||
`VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer
|
`VerifyPeerCertificate`, which Go skips on resumption: guard it if a dialer
|
||||||
ever keeps a `ClientSessionCache`.
|
ever keeps a `ClientSessionCache`.
|
||||||
3. `core.ShutdownTimeout` is unused.
|
2. `core.ShutdownTimeout` is unused.
|
||||||
@@ -29,7 +29,21 @@ type Manager struct {
|
|||||||
type Args struct {
|
type Args struct {
|
||||||
File string // -c: the configuration file, "" for the environment or discovery
|
File string // -c: the configuration file, "" for the environment or discovery
|
||||||
Specs []Spec // pipeline flags, in order
|
Specs []Spec // pipeline flags, in order
|
||||||
Overrides []string // --path=value settings, for lixenwraith/config
|
Overrides []string // --path=value settings and bare switches, for lixenwraith/config
|
||||||
|
}
|
||||||
|
|
||||||
|
// Settings maps each --path setting Load takes to whether it is a switch, which
|
||||||
|
// takes no separate value; the parser must not leave config to guess.
|
||||||
|
func Settings() map[string]bool {
|
||||||
|
c := lconfig.New()
|
||||||
|
if err := c.RegisterStructWithTags("", defaults(), lconfig.FormatTOML); err != nil {
|
||||||
|
panic(err) // Config is static: every run fails or none does
|
||||||
|
}
|
||||||
|
settings := map[string]bool{}
|
||||||
|
for path, value := range c.GetRegisteredPathsWithDefaults() {
|
||||||
|
_, settings[path] = value.(bool)
|
||||||
|
}
|
||||||
|
return settings
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load reads the startup sources and validates the initial configuration.
|
// Load reads the startup sources and validates the initial configuration.
|
||||||
@@ -80,9 +94,6 @@ func Load(args Args) (*Manager, error) {
|
|||||||
// A missing discovered default still permits valid CLI/env/default values.
|
// A missing discovered default still permits valid CLI/env/default values.
|
||||||
}
|
}
|
||||||
m.read = err == nil
|
m.read = err == nil
|
||||||
if unknown := cfg.UnknownCLIKeys(); len(unknown) > 0 && !initial.Quiet {
|
|
||||||
fmt.Fprintf(os.Stderr, "Warning: unrecognized flags ignored: %v\n", unknown)
|
|
||||||
}
|
|
||||||
m.config = cfg
|
m.config = cfg
|
||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -46,10 +46,13 @@ check "lw --check builds the dumped preset" "$(lw --check -c "$RUN/dump.toml" 2>
|
|||||||
lw preset edge -to 127.0.0.1:1 >/dev/null 2>"$LOG/edge-noauth.err"
|
lw preset edge -to 127.0.0.1:1 >/dev/null 2>"$LOG/edge-noauth.err"
|
||||||
rc=$?
|
rc=$?
|
||||||
check "an edge without credentials is a usage error ($rc)" "$( ((rc == 2)) && grep -q 'never sends unauthenticated' "$LOG/edge-noauth.err" && echo 1 || echo 0)"
|
check "an edge without credentials is a usage error ($rc)" "$( ((rc == 2)) && grep -q 'never sends unauthenticated' "$LOG/edge-noauth.err" && echo 1 || echo 0)"
|
||||||
|
lw tail </dev/null 2>"$LOG/stray.err"
|
||||||
|
rc=$?
|
||||||
|
check "a word no option takes is a usage error ($rc)" "$( ((rc == 2)) && grep -q 'unexpected argument "tail"' "$LOG/stray.err" && echo 1 || echo 0)"
|
||||||
|
|
||||||
section "Certificates"
|
section "Certificates"
|
||||||
lw tls ca -dir "$RUN/pki" 2>"$LOG/tls.out" &&
|
lw tls ca -dir "$RUN/pki" 2>"$LOG/tls.out" &&
|
||||||
lw tls cert -ca-dir "$RUN/pki" -name logs.example -server -host 127.0.0.1,logs.example 2>>"$LOG/tls.out"
|
lw tls cert -ca-dir "$RUN/pki" -name logs.example -server -hosts 127.0.0.1,logs.example 2>>"$LOG/tls.out"
|
||||||
check "lw tls ca and cert issue a chain openssl verifies" \
|
check "lw tls ca and cert issue a chain openssl verifies" \
|
||||||
"$(openssl verify -CAfile "$RUN/pki/ca.crt" "$RUN/pki/logs.example.crt" 2>/dev/null | grep -q ': OK$' && echo 1 || echo 0)"
|
"$(openssl verify -CAfile "$RUN/pki/ca.crt" "$RUN/pki/logs.example.crt" 2>/dev/null | grep -q ': OK$' && echo 1 || echo 0)"
|
||||||
check "keys are private" "$(is "$(stat -c %a "$RUN/pki/ca.key" "$RUN/pki/logs.example.key" | tr '\n' ' ')" "600 600 ")"
|
check "keys are private" "$(is "$(stat -c %a "$RUN/pki/ca.key" "$RUN/pki/logs.example.key" | tr '\n' ' ')" "600 600 ")"
|
||||||
|
|||||||
Reference in new issue
Block a user