🏗️ Implement CLI commands support

This commit is contained in:
Maxim Lebedev 2021-12-30 02:20:52 +05:00
parent 4e933d21e0
commit 285d1464a9
Signed by: toby3d
GPG Key ID: 1F14E25B7C119FC5
5 changed files with 275 additions and 129 deletions

View File

@ -1,129 +0,0 @@
//go:generate go install github.com/valyala/quicktemplate/qtc@latest
//go:generate qtc -dir=../../web
package main
import (
"flag"
"log"
gohttp "net/http"
_ "net/http/pprof"
"os"
"path/filepath"
"runtime"
"runtime/pprof"
"strings"
"github.com/fasthttp/router"
"github.com/spf13/viper"
http "github.com/valyala/fasthttp"
bolt "go.etcd.io/bbolt"
configrepo "source.toby3d.me/website/oauth/internal/config/repository/viper"
configucase "source.toby3d.me/website/oauth/internal/config/usecase"
healthdelivery "source.toby3d.me/website/oauth/internal/health/delivery/http"
tokendelivery "source.toby3d.me/website/oauth/internal/token/delivery/http"
tokenrepo "source.toby3d.me/website/oauth/internal/token/repository/bolt"
tokenucase "source.toby3d.me/website/oauth/internal/token/usecase"
)
//nolint: gochecknoglobals
var (
flagConfig = flag.String("config", filepath.Join(".", "config.yml"), "set specific path to config file")
flagCpuProfile = flag.String("cpuprofile", "", "write cpu profile to `file`")
flagMemProfile = flag.String("memprofile", "", "write memory profile to `file`")
)
//nolint: funlen
func main() {
flag.Parse()
if *flagCpuProfile != "" || *flagMemProfile != "" {
go log.Println(gohttp.ListenAndServe("localhost:6060", nil))
}
if *flagCpuProfile != "" {
cpuProfile, err := os.Create(*flagCpuProfile)
if err != nil {
log.Fatal("could not create CPU profile: ", err)
}
defer cpuProfile.Close()
if err := pprof.StartCPUProfile(cpuProfile); err != nil {
log.Fatal("could not start CPU profile: ", err)
}
defer pprof.StopCPUProfile()
}
v := viper.New()
v.SetDefault("url", "/")
v.SetDefault("database", map[string]interface{}{
"client": "bolt",
"connection": map[string]interface{}{
"filename": filepath.Join(".", "development.db"),
},
})
v.SetDefault("server", map[string]interface{}{
"host": "127.0.0.1",
"port": 3000, //nolint: gomnd
})
v.SetConfigName("config")
v.SetConfigType("yaml")
dir, file := filepath.Split(*flagConfig)
if file != "" {
ext := filepath.Ext(file)
v.SetConfigName(strings.TrimSuffix(file, ext))
v.SetConfigType(ext[1:])
}
v.AddConfigPath(dir)
v.AddConfigPath(filepath.Join(".", "configs"))
v.AddConfigPath(".")
r := router.New()
cfg := configucase.NewConfigUseCase(configrepo.NewViperConfigRepository(v))
db, err := bolt.Open(cfg.DBFileName(), os.ModePerm, nil)
if err != nil {
log.Fatalln(err)
}
defer db.Close()
if err = db.Update(func(tx *bolt.Tx) error {
_, err := tx.CreateBucketIfNotExists(tokenrepo.Token{}.Bucket())
return err
}); err != nil {
log.Fatalln(err)
}
tokendelivery.NewRequestHandler(tokenucase.NewTokenUseCase(tokenrepo.NewBoltTokenRepository(db))).Register(r)
healthdelivery.NewRequestHandler().Register(r)
server := &http.Server{
Handler: r.Handler,
Name: "IndieAuth/1.0.0 (" + cfg.URL() + ")",
DisableKeepalive: true,
CloseOnShutdown: true,
// TODO(toby3d): Logger
}
if err := server.ListenAndServe(cfg.Addr()); err != nil {
log.Fatalln(err)
}
if *flagMemProfile == "" {
return
}
memProfile, err := os.Create(*flagMemProfile)
if err != nil {
log.Fatal("could not create memory profile: ", err)
}
defer memProfile.Close()
runtime.GC() // NOTE(toby3d): get up-to-date statistics
if err := pprof.WriteHeapProfile(memProfile); err != nil {
log.Fatal("could not write memory profile: ", err)
}
}

83
internal/cmd/root.go Normal file
View File

@ -0,0 +1,83 @@
package cmd
import (
"log"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/spf13/viper"
http "github.com/valyala/fasthttp"
"source.toby3d.me/website/oauth/internal/domain"
)
//nolint: gochecknoglobals
var (
rootCmd = &cobra.Command{
Use: "indieauth",
Short: "",
Long: "",
}
client = new(domain.Client)
config = new(domain.Config)
)
//nolint: gochecknoglobals
var configPath string
//nolint: gochecknoinits
func init() {
cobra.OnInitialize(initConfig)
rootCmd.PersistentFlags().StringVar(&configPath, "config", filepath.Join(".", "config.yaml"), "config file")
viper.BindPFlag("port", startCmd.PersistentFlags().Lookup("port"))
}
func Execute() {
if err := rootCmd.Execute(); err != nil {
log.Fatalln(err)
}
}
func initConfig() {
viper.AddConfigPath(filepath.Join(".", "configs"))
viper.SetConfigName("config")
if configPath != "" {
viper.SetConfigFile(configPath)
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.AutomaticEnv()
var err error
if err = viper.ReadInConfig(); err != nil {
log.Fatalf("cannot load config from file %s: %v", viper.ConfigFileUsed(), err)
}
if err = viper.Unmarshal(config); err != nil {
log.Fatalln("failed to read config:", err)
}
// NOTE(toby3d): The server instance itself can be as a client.
rootURL := config.Server.GetRootURL()
client.Name = []string{config.Name}
if client.ID, err = domain.NewClientID(rootURL); err != nil {
log.Fatalln("fail to read config:", err)
}
u, logo, redirect := http.AcquireURI(), http.AcquireURI(), http.AcquireURI()
if err = u.Parse(nil, []byte(rootURL)); err != nil {
log.Fatalln("cannot parse client URL:", err)
}
u.CopyTo(logo)
u.CopyTo(redirect)
redirect.SetPath("/callback")
logo.SetPath(config.Server.StaticURLPrefix + "/icon.svg")
client.URL = []*http.URI{u}
client.Logo = []*http.URI{logo}
client.RedirectURI = []*http.URI{redirect}
}

154
internal/cmd/start.go Normal file
View File

@ -0,0 +1,154 @@
package cmd
import (
"errors"
"fmt"
"log"
"os"
"os/signal"
"path"
"runtime"
"runtime/pprof"
"syscall"
"time"
"github.com/fasthttp/router"
"github.com/spf13/cobra"
http "github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/pprofhandler"
bolt "go.etcd.io/bbolt"
"golang.org/x/text/language"
"golang.org/x/text/message"
clienthttpdelivery "source.toby3d.me/website/oauth/internal/client/delivery/http"
clientrepo "source.toby3d.me/website/oauth/internal/client/repository/http"
clientucase "source.toby3d.me/website/oauth/internal/client/usecase"
healthhttpdelivery "source.toby3d.me/website/oauth/internal/health/delivery/http"
metadatahttpdelivery "source.toby3d.me/website/oauth/internal/metadata/delivery/http"
tickethttpdelivery "source.toby3d.me/website/oauth/internal/ticket/delivery/http"
ticketucase "source.toby3d.me/website/oauth/internal/ticket/usecase"
userrepo "source.toby3d.me/website/oauth/internal/user/repository/http"
userucase "source.toby3d.me/website/oauth/internal/user/usecase"
)
const (
DefaultCacheDuration time.Duration = 8760 * time.Hour // NOTE(toby3d): year
DefaultPort int = 3000
)
//nolint: gochecknoglobals
var startCmd = &cobra.Command{
Use: "server",
Short: "start server",
Long: "",
Run: startServer,
}
//nolint: gochecknoglobals
var (
cpuProfilePath string
memProfilePath string
enablePprof bool
)
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(startCmd)
startCmd.PersistentFlags().IntP("port", "p", DefaultPort, "port to run server on")
startCmd.PersistentFlags().BoolVar(&enablePprof, "pprof", false, "enable pprof mode")
startCmd.PersistentFlags().StringVar(&cpuProfilePath, "cpuprofile", "", "write cpu profile to file")
startCmd.PersistentFlags().StringVar(&memProfilePath, "memprofile", "", "write memory profile to file")
}
func startServer(cmd *cobra.Command, args []string) {
store, err := bolt.Open(config.Database.Path, os.ModePerm, nil)
if err != nil {
log.Fatalln("failed to open database connection:", err)
}
defer store.Close()
httpClient := &http.Client{
Name: fmt.Sprintf("%s/0.1 (+%s)", config.Name, config.Server.GetAddress()),
}
r := router.New()
matcher := language.NewMatcher(message.DefaultCatalog.Languages())
r.ServeFilesCustom(path.Join(config.Server.StaticURLPrefix, "{filepath:*}"), &http.FS{
Root: config.Server.StaticRootPath,
CacheDuration: DefaultCacheDuration,
AcceptByteRange: true,
Compress: true,
CompressBrotli: true,
GenerateIndexPages: true,
})
healthhttpdelivery.NewRequestHandler().Register(r)
metadatahttpdelivery.NewRequestHandler(config).Register(r)
clienthttpdelivery.NewRequestHandler(config, client, matcher).Register(r)
tickethttpdelivery.NewRequestHandler(
ticketucase.NewTicketUseCase(httpClient),
userucase.NewUserUseCase(userrepo.NewHTTPUserRepository(httpClient)),
).Register(r)
if enablePprof {
r.GET("/debug/pprof/{filepath:*}", pprofhandler.PprofHandler)
}
server := &http.Server{
CloseOnShutdown: true,
DisableKeepalive: true,
Handler: r.Handler,
Logger: log.New(os.Stdout, config.Name+"\t", log.Lmsgprefix|log.LstdFlags|log.LUTC),
Name: fmt.Sprintf("%s/0.1 (+%s)", config.Name, config.Server.GetAddress()),
}
done := make(chan os.Signal, 1)
signal.Notify(done, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
if cpuProfilePath != "" {
cpuProfile, err := os.Create(cpuProfilePath)
if err != nil {
log.Fatalln("could not create CPU profile:", err)
}
defer cpuProfile.Close()
if err = pprof.StartCPUProfile(cpuProfile); err != nil {
log.Fatalln("could not start CPU profile:", err)
}
defer pprof.StopCPUProfile()
}
go func() {
server.Logger.Printf(
"started at %s, available at %s",
config.Server.GetAddress(),
config.Server.GetRootURL(),
)
err := server.ListenAndServe(config.Server.GetAddress())
if err != nil && !errors.Is(err, http.ErrConnectionClosed) {
log.Fatalln("cannot listen and serve:", err)
}
}()
<-done
if err = server.Shutdown(); err != nil {
log.Fatalln("failed shutdown of server:", err)
}
if memProfilePath == "" {
return
}
memProfile, err := os.Create(memProfilePath)
if err != nil {
log.Fatalln("could not create memory profile:", err)
}
defer memProfile.Close()
runtime.GC() // NOTE(toby3d): get up-to-date statistics
if err = pprof.WriteHeapProfile(memProfile); err != nil {
log.Fatalln("could not write memory profile:", err)
}
}

25
internal/cmd/version.go Normal file
View File

@ -0,0 +1,25 @@
package cmd
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
)
//nolint: gochecknoglobals
var versionCmd = &cobra.Command{
Use: "version",
Short: "print version",
Long: "prints the build information",
Run: printVersion,
}
//nolint: gochecknoinits
func init() {
rootCmd.AddCommand(versionCmd)
}
func printVersion(cmd *cobra.Command, args []string) {
fmt.Println("IndieAuth version", runtime.Version(), runtime.GOOS) //nolint: forbidigo
}

13
main.go Normal file
View File

@ -0,0 +1,13 @@
//go:generate go install github.com/valyala/quicktemplate/qtc@latest
//go:generate qtc -dir=./web
//go:generate go install golang.org/x/text/cmd/gotext@latest
//go:generate gotext -srclang=en update -out=catalog_gen.go -lang=en,ru
package main
import (
_ "embed"
"source.toby3d.me/website/oauth/internal/cmd"
)
func main() { cmd.Execute() }