error-pages/internal/cli/serve/command.go
2022-01-28 20:42:08 +05:00

143 lines
3.6 KiB
Go

package serve
import (
"context"
"errors"
"os"
"time"
"github.com/spf13/cobra"
"github.com/tarampampam/error-pages/internal/breaker"
"github.com/tarampampam/error-pages/internal/config"
appHttp "github.com/tarampampam/error-pages/internal/http"
"github.com/tarampampam/error-pages/internal/pick"
"go.uber.org/zap"
)
// NewCommand creates `serve` command.
func NewCommand(ctx context.Context, log *zap.Logger, configFile *string) *cobra.Command {
var (
f flags
cfg *config.Config
)
cmd := &cobra.Command{
Use: "serve",
Aliases: []string{"s", "server"},
Short: "Start HTTP server",
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
if configFile == nil {
return errors.New("path to the config file is required for this command")
}
if err = f.overrideUsingEnv(cmd.Flags()); err != nil {
return err
}
if cfg, err = config.FromYamlFile(*configFile); err != nil {
return err
}
return f.validate()
},
RunE: func(*cobra.Command, []string) error { return run(ctx, log, f, cfg) },
}
f.init(cmd.Flags())
return cmd
}
// run current command.
func run(parentCtx context.Context, log *zap.Logger, f flags, cfg *config.Config) error { //nolint:funlen
var (
ctx, cancel = context.WithCancel(parentCtx) // serve context creation
oss = breaker.NewOSSignals(ctx) // OS signals listener
)
// subscribe for system signals
oss.Subscribe(func(sig os.Signal) {
log.Warn("Stopping by OS signal..", zap.String("signal", sig.String()))
cancel()
})
defer func() {
cancel() // call the cancellation function after all
oss.Stop() // stop system signals listening
}()
var (
templateNames = cfg.TemplateNames()
picker *pick.StringsSlice
)
switch f.template.name {
case useRandomTemplate:
log.Info("A random template will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomOnce)
case useRandomTemplateOnEachRequest:
log.Info("A random template on EACH request will be used")
picker = pick.NewStringsSlice(templateNames, pick.RandomEveryTime)
case "":
log.Info("The first template (ordered by name) will be used")
picker = pick.NewStringsSlice(templateNames, pick.First)
default:
if t, found := cfg.Template(f.template.name); found {
log.Info("We will use the requested template", zap.String("name", t.Name()))
picker = pick.NewStringsSlice([]string{t.Name()}, pick.First)
} else {
return errors.New("requested nonexistent template: " + f.template.name)
}
}
// create HTTP server
server := appHttp.NewServer(log)
// register server routes, middlewares, etc.
if err := server.Register(cfg, picker, f.defaultErrorPage, f.defaultHTTPCode, f.showDetails); err != nil {
return err
}
startedAt, startingErrCh := time.Now(), make(chan error, 1) // channel for server starting error
// start HTTP server in separate goroutine
go func(errCh chan<- error) {
defer close(errCh)
log.Info("Server starting",
zap.String("addr", f.listen.ip),
zap.Uint16("port", f.listen.port),
zap.String("default error page", f.defaultErrorPage),
zap.Uint16("default HTTP response code", f.defaultHTTPCode),
zap.Bool("show request details", f.showDetails),
)
if err := server.Start(f.listen.ip, f.listen.port); err != nil {
errCh <- err
}
}(startingErrCh)
// and wait for...
select {
case err := <-startingErrCh: // ..server starting error
return err
case <-ctx.Done(): // ..or context cancellation
log.Info("Gracefully server stopping", zap.Duration("uptime", time.Since(startedAt)))
// stop the server using created context above
if err := server.Stop(); err != nil {
return err
}
}
return nil
}