214 lines
5.4 KiB
Go
214 lines
5.4 KiB
Go
//nolint:forbidigo // This is part of the CLI and the errors will be written as fmt.Pxxx
|
|
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"os"
|
|
"os/signal"
|
|
"syscall"
|
|
|
|
devcmd "git.maronato.dev/maronato/goshort/cmd/dev"
|
|
healthcheckcmd "git.maronato.dev/maronato/goshort/cmd/healthcheck"
|
|
servecmd "git.maronato.dev/maronato/goshort/cmd/serve"
|
|
"git.maronato.dev/maronato/goshort/cmd/shared"
|
|
"git.maronato.dev/maronato/goshort/internal/config"
|
|
"git.maronato.dev/maronato/goshort/internal/util/logging"
|
|
"git.maronato.dev/maronato/goshort/internal/util/tracing"
|
|
"github.com/peterbourgon/ff/v3/ffcli"
|
|
)
|
|
|
|
var (
|
|
// ErrUnknownCommand is returned when the user tries to run an unknown command.
|
|
ErrUnknownCommand = errors.New("unknown command")
|
|
// ErrUnknownHelpTopic is returned when the user tries to run an unknown help topic.
|
|
ErrUnknownHelpTopic = errors.New("unknown help topic")
|
|
)
|
|
|
|
func NewUnknownCommand(cmd string) error {
|
|
return fmt.Errorf("%q: %w\nRun 'goshort help' for usage", cmd, ErrUnknownCommand)
|
|
}
|
|
|
|
func NewUnknownHelpTopic(topic string) error {
|
|
return fmt.Errorf("%q: %w. Run 'goshort help'", topic, ErrUnknownHelpTopic)
|
|
}
|
|
|
|
func Run(version string) error {
|
|
// Create the application-wide context, and
|
|
// implement graceful shutdown.
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
trapSignalsCrossPlatform(cancel)
|
|
|
|
// Create the root command and register subcommands.
|
|
rootCmd, cfg := newRootCmd(version)
|
|
rootCmd.Subcommands = append(
|
|
rootCmd.Subcommands,
|
|
servecmd.New(cfg),
|
|
healthcheckcmd.New(cfg),
|
|
)
|
|
|
|
// Look for the env ENV_DOCKER=true to disable the dev command
|
|
// since the docker image won't have node installed.
|
|
if os.Getenv("ENV_DOCKER") != "true" {
|
|
rootCmd.Subcommands = append(rootCmd.Subcommands, devcmd.New(cfg))
|
|
}
|
|
|
|
// Parse the command-line arguments.
|
|
if err := rootCmd.Parse(os.Args[1:]); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
|
|
// Validate config
|
|
if err := config.Validate(cfg); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
|
|
// Create system logger
|
|
l := logging.NewLogger(cfg)
|
|
ctx = logging.WithLogger(ctx, l)
|
|
|
|
// Initialize tracing
|
|
ctx, stopTracing, err := tracing.InitTracer(ctx, cfg, version)
|
|
if err != nil {
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
|
|
// Close the tracer when the application exits
|
|
defer stopTracing()
|
|
|
|
// Run the command.
|
|
if err := rootCmd.Run(ctx); err != nil {
|
|
if errors.Is(err, flag.ErrHelp) {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("%w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// newRootCmd constructs a root command from the provided options.
|
|
func newRootCmd(version string) (*ffcli.Command, *config.Config) {
|
|
var cfg config.Config
|
|
|
|
fs := flag.NewFlagSet("goshort", flag.ContinueOnError)
|
|
|
|
cmd := &ffcli.Command{
|
|
Name: "goshort",
|
|
ShortUsage: "goshort <subcommand> [flags]",
|
|
ShortHelp: "goshort is a tiny URL shortener",
|
|
FlagSet: fs,
|
|
Exec: func(_ context.Context, args []string) error {
|
|
// If the user didn't provide any subcommand, then
|
|
// we'll just show the help message.
|
|
if len(args) == 0 {
|
|
return flag.ErrHelp
|
|
}
|
|
|
|
return NewUnknownCommand(args[0])
|
|
},
|
|
}
|
|
|
|
cmd.Subcommands = []*ffcli.Command{
|
|
// Register help command
|
|
{
|
|
Name: "help",
|
|
ShortUsage: "help [command]",
|
|
ShortHelp: "Show help for a command",
|
|
Exec: func(ctx context.Context, args []string) error {
|
|
if len(args) == 0 || args[0] == "help" {
|
|
cmd.FlagSet.Usage()
|
|
|
|
fmt.Printf("\nUse 'goshort help <command>' for more information about that command.\n\n")
|
|
|
|
return nil
|
|
}
|
|
|
|
for _, c := range cmd.Subcommands {
|
|
if c.Name == args[0] {
|
|
return c.ParseAndRun(ctx, []string{"-h"}) //nolint:wrapcheck // We don't want to wrap the error here
|
|
}
|
|
}
|
|
|
|
return NewUnknownHelpTopic(args[0])
|
|
},
|
|
},
|
|
|
|
// Register version command
|
|
{
|
|
Name: "version",
|
|
ShortUsage: "version",
|
|
ShortHelp: "Show version information",
|
|
Exec: func(_ context.Context, _ []string) error {
|
|
fmt.Printf("goshort version %s\n", version)
|
|
|
|
return nil
|
|
},
|
|
},
|
|
|
|
// Register config command
|
|
NewConfigCmd(&cfg),
|
|
}
|
|
|
|
return cmd, &cfg
|
|
}
|
|
|
|
// https://github.com/caddyserver/caddy/blob/fbb0ecfa322aa7710a3448453fd3ae40f037b8d1/sigtrap.go#L37
|
|
// trapSignalsCrossPlatform captures SIGINT or interrupt (depending
|
|
// on the OS), which initiates a graceful shutdown. A second SIGINT
|
|
// or interrupt will forcefully exit the process immediately.
|
|
func trapSignalsCrossPlatform(cancel context.CancelFunc) {
|
|
go func() {
|
|
shutdown := make(chan os.Signal, 1)
|
|
signal.Notify(shutdown, os.Interrupt, syscall.SIGINT)
|
|
|
|
for i := 0; true; i++ {
|
|
<-shutdown
|
|
|
|
if i > 0 {
|
|
fmt.Printf("\nForce quit\n")
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("\nGracefully shutting down. Press Ctrl+C again to force quit\n")
|
|
cancel()
|
|
}
|
|
}()
|
|
}
|
|
|
|
func NewConfigCmd(cfg *config.Config) *ffcli.Command {
|
|
// Create the flagset and register the flags.
|
|
fs := flag.NewFlagSet("goshort serve", flag.ContinueOnError)
|
|
|
|
shared.RegisterBaseFlags(fs, cfg)
|
|
shared.RegisterServerFlags(fs, cfg)
|
|
fs.BoolVar(&cfg.Prod, "prod", config.DefaultProd, "run in production mode")
|
|
|
|
// Return the ffcli command.
|
|
return &ffcli.Command{
|
|
Name: "config",
|
|
ShortUsage: "goshort config [flags]",
|
|
ShortHelp: "Prints the current config",
|
|
FlagSet: fs,
|
|
Exec: func(_ context.Context, _ []string) error {
|
|
cfg.PrettyPrint()
|
|
|
|
return nil
|
|
},
|
|
Options: shared.NewSharedCmdOptions(),
|
|
}
|
|
}
|