From 285d1464a92b729722f26cc8c8d6fb7e1ae93427 Mon Sep 17 00:00:00 2001 From: Maxim Lebedev Date: Thu, 30 Dec 2021 02:20:52 +0500 Subject: [PATCH] :building_construction: Implement CLI commands support --- cmd/indieauth/main.go | 129 --------------------------------- internal/cmd/root.go | 83 ++++++++++++++++++++++ internal/cmd/start.go | 154 ++++++++++++++++++++++++++++++++++++++++ internal/cmd/version.go | 25 +++++++ main.go | 13 ++++ 5 files changed, 275 insertions(+), 129 deletions(-) delete mode 100644 cmd/indieauth/main.go create mode 100644 internal/cmd/root.go create mode 100644 internal/cmd/start.go create mode 100644 internal/cmd/version.go create mode 100644 main.go diff --git a/cmd/indieauth/main.go b/cmd/indieauth/main.go deleted file mode 100644 index 2e1945d..0000000 --- a/cmd/indieauth/main.go +++ /dev/null @@ -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) - } -} diff --git a/internal/cmd/root.go b/internal/cmd/root.go new file mode 100644 index 0000000..1cc1689 --- /dev/null +++ b/internal/cmd/root.go @@ -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} +} diff --git a/internal/cmd/start.go b/internal/cmd/start.go new file mode 100644 index 0000000..007c33f --- /dev/null +++ b/internal/cmd/start.go @@ -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) + } +} diff --git a/internal/cmd/version.go b/internal/cmd/version.go new file mode 100644 index 0000000..7bff013 --- /dev/null +++ b/internal/cmd/version.go @@ -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 +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..55b646d --- /dev/null +++ b/main.go @@ -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() }