211 lines
5.2 KiB
Go
211 lines
5.2 KiB
Go
// Package cli implements the command line interface.
|
|
//
|
|
// `serve` is the default when no subcommand is given, so running the binary
|
|
// with no arguments starts the server, which is the overwhelmingly common case.
|
|
package cli
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/owen/vibedns/internal/config"
|
|
"github.com/owen/vibedns/internal/version"
|
|
)
|
|
|
|
// Command is one subcommand.
|
|
type Command struct {
|
|
Name string
|
|
Summary string
|
|
Usage string
|
|
Run func(ctx context.Context, env *Env, args []string) error
|
|
}
|
|
|
|
// Env carries what every command needs.
|
|
type Env struct {
|
|
Boot config.Bootstrap
|
|
Stdout io.Writer
|
|
Stderr io.Writer
|
|
Log *slog.Logger
|
|
}
|
|
|
|
// ExitError carries a specific process exit status.
|
|
type ExitError struct {
|
|
Code int
|
|
Err error
|
|
}
|
|
|
|
func (e *ExitError) Error() string { return e.Err.Error() }
|
|
func (e *ExitError) Unwrap() error { return e.Err }
|
|
|
|
// Exit wraps an error with an exit code.
|
|
func Exit(code int, format string, args ...any) error {
|
|
return &ExitError{Code: code, Err: fmt.Errorf(format, args...)}
|
|
}
|
|
|
|
// commands is the full command table.
|
|
func commands() []*Command {
|
|
return []*Command{
|
|
serveCommand(),
|
|
versionCommand(),
|
|
configCommand(),
|
|
adminCommand(),
|
|
databaseCommand(),
|
|
}
|
|
}
|
|
|
|
// Main parses arguments and runs the selected command.
|
|
func Main(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|
env := &Env{Boot: config.DefaultBootstrap(), Stdout: stdout, Stderr: stderr}
|
|
|
|
// Find the subcommand: the first argument that is not a flag. This lets
|
|
// both `vibedns --db x serve` and `vibedns serve --db x` work.
|
|
name := ""
|
|
rest := args
|
|
for i, a := range args {
|
|
if !strings.HasPrefix(a, "-") {
|
|
name = a
|
|
rest = append(append([]string{}, args[:i]...), args[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
|
|
switch name {
|
|
case "help", "-h", "--help", "":
|
|
if name == "" && !wantsHelp(args) {
|
|
// No subcommand: serve.
|
|
return run(ctx, env, serveCommand(), args)
|
|
}
|
|
usage(stdout)
|
|
return 0
|
|
}
|
|
|
|
for _, c := range commands() {
|
|
if c.Name == name {
|
|
return run(ctx, env, c, rest)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(stderr, "unknown command %q\n\n", name)
|
|
usage(stderr)
|
|
return 2
|
|
}
|
|
|
|
func wantsHelp(args []string) bool {
|
|
for _, a := range args {
|
|
if a == "-h" || a == "--help" || a == "help" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func run(ctx context.Context, env *Env, c *Command, args []string) int {
|
|
err := c.Run(ctx, env, args)
|
|
if err == nil {
|
|
return 0
|
|
}
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
return 0
|
|
}
|
|
var ee *ExitError
|
|
if errors.As(err, &ee) {
|
|
fmt.Fprintf(env.Stderr, "%s: %v\n", c.Name, ee.Err)
|
|
return ee.Code
|
|
}
|
|
fmt.Fprintf(env.Stderr, "%s: %v\n", c.Name, err)
|
|
return 1
|
|
}
|
|
|
|
func usage(w io.Writer) {
|
|
fmt.Fprintf(w, `%s %s — authoritative DNS server, recursive resolver and filtering appliance
|
|
|
|
Usage:
|
|
vibedns [command] [flags]
|
|
|
|
Commands:
|
|
`, version.Name, version.Version)
|
|
|
|
for _, c := range commands() {
|
|
fmt.Fprintf(w, " %-20s %s\n", c.Name, c.Summary)
|
|
}
|
|
|
|
fmt.Fprintf(w, `
|
|
Running with no command starts the server.
|
|
|
|
Common flags:
|
|
--db PATH SQLite database file (default %s)
|
|
--http ADDR management interface address (default %s)
|
|
--dns ADDR DNS listen address for UDP and TCP (default %s)
|
|
--log-level LEVEL debug, info, warn or error
|
|
--log-format FORMAT text or json
|
|
|
|
Environment variables:
|
|
%-22s database file
|
|
%-22s management interface address
|
|
%-22s DNS listen address
|
|
%-22s initial administrator username
|
|
%-22s initial administrator password
|
|
%-22s log level
|
|
%-22s log format
|
|
|
|
Examples:
|
|
vibedns
|
|
vibedns serve --db /var/lib/vibedns/dns.db --http 127.0.0.1:8080
|
|
vibedns config check
|
|
vibedns admin reset-password
|
|
vibedns database backup --output /var/backups
|
|
vibedns version
|
|
|
|
`,
|
|
config.DefaultDBPath, config.DefaultHTTPAddr, config.DefaultDNSAddr,
|
|
config.EnvDBPath, config.EnvHTTPAddr, config.EnvDNSAddr,
|
|
config.EnvAdminUsername, config.EnvAdminPassword,
|
|
config.EnvLogLevel, config.EnvLogFormat)
|
|
}
|
|
|
|
// newFlagSet builds a flag set that prints its own usage on error.
|
|
func newFlagSet(env *Env, c *Command) *flag.FlagSet {
|
|
fs := flag.NewFlagSet(c.Name, flag.ContinueOnError)
|
|
fs.SetOutput(env.Stderr)
|
|
fs.Usage = func() {
|
|
fmt.Fprintf(env.Stderr, "%s\n\n", c.Usage)
|
|
fs.PrintDefaults()
|
|
}
|
|
return fs
|
|
}
|
|
|
|
// versionCommand prints build information.
|
|
func versionCommand() *Command {
|
|
return &Command{
|
|
Name: "version",
|
|
Summary: "print version and build information",
|
|
Usage: "Usage: vibedns version",
|
|
Run: func(ctx context.Context, env *Env, args []string) error {
|
|
fmt.Fprint(env.Stdout, version.Long())
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
// confirm asks for an interactive yes/no answer. It returns false when stdin
|
|
// is not a terminal, so a piped invocation can never be silently destructive.
|
|
func confirm(env *Env, prompt string) bool {
|
|
fi, err := os.Stdin.Stat()
|
|
if err != nil || (fi.Mode()&os.ModeCharDevice) == 0 {
|
|
fmt.Fprintf(env.Stderr,
|
|
"%s\nRefusing to continue without an interactive confirmation; pass --yes to proceed.\n", prompt)
|
|
return false
|
|
}
|
|
fmt.Fprintf(env.Stdout, "%s [y/N]: ", prompt)
|
|
var answer string
|
|
_, _ = fmt.Fscanln(os.Stdin, &answer)
|
|
answer = strings.ToLower(strings.TrimSpace(answer))
|
|
return answer == "y" || answer == "yes"
|
|
}
|