diff --git a/main.go b/main.go index 18a49b1e6..49e536003 100644 --- a/main.go +++ b/main.go @@ -13,6 +13,7 @@ import ( "go.uber.org/zap" "go.uber.org/zap/zapcore" + "pathwar.pw/pkg/cli" "pathwar.pw/server" "pathwar.pw/sql" ) @@ -29,11 +30,21 @@ func main() { func newRootCommand() *cobra.Command { cmd := &cobra.Command{ - Use: "pathwar.pw", + // Use: "pathwar.pw", + Use: os.Args[0], } cmd.PersistentFlags().BoolP("help", "h", false, "print usage") //cmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "verbose mode") + // Add commands + commands := cli.Commands{} + for name, command := range sql.Commands() { + commands[name] = command + } + for name, command := range server.Commands() { + commands[name] = command + } + cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { // setup logging config := zap.NewDevelopmentConfig() @@ -56,20 +67,22 @@ func newRootCommand() *cobra.Command { } } - if err := viper.Unmarshal(sql.GetOptions()); err != nil { - return err - } - if err := viper.Unmarshal(server.GetOptions()); err != nil { - return err + for _, command := range commands { + if err := command.LoadDefaultOptions(); err != nil { + return err + } } return nil } - cmd.AddCommand( - server.NewServerCommand(), - sql.NewSQLCommand(), - ) + for name, command := range commands { + if strings.Contains(name, " ") { // do not add commands where level > 1 + continue + } + cmd.AddCommand(command.CobraCommand(commands)) + } + viper.AutomaticEnv() viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) return cmd diff --git a/pkg/cli/cobra.go b/pkg/cli/cobra.go new file mode 100644 index 000000000..1a341e982 --- /dev/null +++ b/pkg/cli/cobra.go @@ -0,0 +1,16 @@ +package cli + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +type Command interface { + CobraCommand(Commands) *cobra.Command + + LoadDefaultOptions() error + + ParseFlags(*pflag.FlagSet) +} + +type Commands map[string]Command diff --git a/server/cmd_server.go b/server/cmd_server.go new file mode 100644 index 000000000..0d5343bf1 --- /dev/null +++ b/server/cmd_server.go @@ -0,0 +1,59 @@ +package server + +import ( + "encoding/json" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + + "pathwar.pw/pkg/cli" + "pathwar.pw/sql" +) + +type serverOptions struct { + sql sql.Options + + GRPCBind string + HTTPBind string + JWTKey string + WithReflection bool +} + +func (opts serverOptions) String() string { + out, _ := json.Marshal(opts) + return string(out) +} + +func Commands() cli.Commands { + return cli.Commands{ + "server": &serverCommand{}, + } +} + +type serverCommand struct{ opts serverOptions } + +func (cmd *serverCommand) LoadDefaultOptions() error { return viper.Unmarshal(&cmd.opts) } +func (cmd *serverCommand) ParseFlags(flags *pflag.FlagSet) { + flags.StringVar(&cmd.opts.GRPCBind, "grpc-bind", ":9111", "gRPC server address") + flags.StringVar(&cmd.opts.HTTPBind, "http-bind", ":8000", "HTTP server address") + flags.StringVar(&cmd.opts.JWTKey, "jwt-key", "", "JWT secure key") + flags.BoolVarP(&cmd.opts.WithReflection, "grpc-reflection", "", false, "enable gRPC reflection") + if err := viper.BindPFlags(flags); err != nil { + zap.L().Warn("failed to bind viper flags", zap.Error(err)) + } +} +func (cmd *serverCommand) CobraCommand(commands cli.Commands) *cobra.Command { + cc := &cobra.Command{ + Use: "server", + RunE: func(_ *cobra.Command, args []string) error { + opts := cmd.opts + opts.sql = sql.GetOptions(commands) + return server(&opts) + }, + } + cmd.ParseFlags(cc.Flags()) + commands["sql"].ParseFlags(cc.Flags()) + return cc +} diff --git a/server/cobra.go b/server/cobra.go deleted file mode 100644 index 62a91820c..000000000 --- a/server/cobra.go +++ /dev/null @@ -1,35 +0,0 @@ -package server - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "go.uber.org/zap" -) - -func serverSetupFlags(flags *pflag.FlagSet, opts *Options) { - flags.StringVar(&opts.GRPCBind, "grpc-bind", ":9111", "gRPC server address") - flags.StringVar(&opts.HTTPBind, "http-bind", ":8000", "HTTP server address") - flags.StringVar(&opts.JWTKey, "jwt-key", "", "JWT secure key") - if err := viper.BindPFlags(flags); err != nil { - zap.L().Warn("failed to bind viper flags", zap.Error(err)) - } -} - -var globalOpts Options - -func GetOptions() *Options { - opts := globalOpts - return &opts -} - -func NewServerCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "server", - RunE: func(cmd *cobra.Command, args []string) error { - return server(GetOptions()) - }, - } - serverSetupFlags(cmd.Flags(), &globalOpts) - return cmd -} diff --git a/server/server.go b/server/server.go index 8f9e2105a..498041f79 100644 --- a/server/server.go +++ b/server/server.go @@ -3,7 +3,6 @@ package server import ( "context" "crypto/rand" - "encoding/json" "net" "net/http" @@ -15,7 +14,6 @@ import ( grpc_recovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" grpc_ctxtags "github.com/grpc-ecosystem/go-grpc-middleware/tags" "github.com/grpc-ecosystem/grpc-gateway/runtime" - "github.com/jinzhu/gorm" "github.com/pkg/errors" "go.uber.org/zap" "google.golang.org/grpc" @@ -26,19 +24,7 @@ import ( var _ = gogoproto.IsStdTime -type Options struct { - GRPCBind string - HTTPBind string - JWTKey string - WithReflection bool -} - -func (opts Options) String() string { - out, _ := json.Marshal(opts) - return string(out) -} - -func server(opts *Options) error { +func server(opts *serverOptions) error { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -48,7 +34,7 @@ func server(opts *Options) error { return <-errs } -func startHTTPServer(ctx context.Context, opts *Options) error { +func startHTTPServer(ctx context.Context, opts *serverOptions) error { gwmux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &gateway.JSONPb{ EmitDefaults: false, @@ -67,7 +53,7 @@ func startHTTPServer(ctx context.Context, opts *Options) error { return http.ListenAndServe(opts.HTTPBind, mux) } -func startGRPCServer(ctx context.Context, opts *Options) error { +func startGRPCServer(ctx context.Context, opts *serverOptions) error { listener, err := net.Listen("tcp", opts.GRPCBind) if err != nil { return errors.Wrap(err, "failed to listen") @@ -102,12 +88,7 @@ func startGRPCServer(ctx context.Context, opts *Options) error { grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(serverUnaryOpts...)), ) - db, err := sql.FromOpts(sql.GetOptions()) - if err != nil { - return errors.Wrap(err, "failed to initialize database") - } - - svc, err := newSvc(opts, db) + svc, err := newSvc(opts) if err != nil { return errors.Wrap(err, "failed to initialize service") } @@ -125,7 +106,12 @@ func startGRPCServer(ctx context.Context, opts *Options) error { return grpcServer.Serve(listener) } -func newSvc(opts *Options, db *gorm.DB) (*svc, error) { +func newSvc(opts *serverOptions) (*svc, error) { + db, err := sql.FromOpts(&opts.sql) + if err != nil { + return nil, errors.Wrap(err, "failed to initialize database") + } + jwtKey := []byte(opts.JWTKey) if len(jwtKey) == 0 { // generate random JWT key jwtKey = make([]byte, 128) diff --git a/sql/cmd_sql.go b/sql/cmd_sql.go new file mode 100644 index 000000000..f88e1c179 --- /dev/null +++ b/sql/cmd_sql.go @@ -0,0 +1,43 @@ +package sql + +import ( + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + "pathwar.pw/pkg/cli" +) + +type Options struct { + Path string `mapstructure:"path"` +} + +func Commands() cli.Commands { + return cli.Commands{ + "sql": &sqlCommand{}, + "sql dump": &dumpCommand{}, + "sql adduser": &adduserCommand{}, + } +} + +func GetOptions(commands cli.Commands) Options { + return commands["sql"].(*sqlCommand).opts +} + +type sqlCommand struct{ opts Options } + +func (cmd *sqlCommand) LoadDefaultOptions() error { return viper.Unmarshal(&cmd.opts) } +func (cmd *sqlCommand) ParseFlags(flags *pflag.FlagSet) { + flags.StringVarP(&cmd.opts.Path, "sql-path", "", "/tmp/pathwar.db", "SQL db path") + if err := viper.BindPFlags(flags); err != nil { + zap.L().Warn("failed to bind viper flags", zap.Error(err)) + } +} +func (cmd *sqlCommand) CobraCommand(commands cli.Commands) *cobra.Command { + command := &cobra.Command{ + Use: "sql", + } + command.AddCommand(commands["sql dump"].CobraCommand(commands)) + command.AddCommand(commands["sql adduser"].CobraCommand(commands)) + return command +} diff --git a/sql/cmd_sql_adduser.go b/sql/cmd_sql_adduser.go new file mode 100644 index 000000000..6e3692cbe --- /dev/null +++ b/sql/cmd_sql_adduser.go @@ -0,0 +1,84 @@ +package sql + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + "pathwar.pw/entity" + "pathwar.pw/pkg/cli" +) + +type adduserOptions struct { + sql Options `mapstructure:"sql"` + + email string `mapstructure:"email"` + username string `mapstructure:"username"` + password string `mapstructure:"password"` +} + +type adduserCommand struct{ opts adduserOptions } + +func (cmd *adduserCommand) CobraCommand(commands cli.Commands) *cobra.Command { + cc := &cobra.Command{ + Use: "adduser", + Args: func(_ *cobra.Command, args []string) error { + if cmd.opts.email == "" { + return errors.New("--email is mandatory") + } + return nil + }, + RunE: func(_ *cobra.Command, args []string) error { + opts := cmd.opts + opts.sql = GetOptions(commands) + return runAdduser(opts) + }, + } + cmd.ParseFlags(cc.Flags()) + commands["sql"].ParseFlags(cc.Flags()) + return cc +} +func (cmd *adduserCommand) LoadDefaultOptions() error { return viper.Unmarshal(&cmd.opts) } +func (cmd *adduserCommand) ParseFlags(flags *pflag.FlagSet) { + flags.StringVarP(&cmd.opts.email, "email", "", "", "valid email address") + flags.StringVarP(&cmd.opts.username, "username", "", "", "random value if empty") + flags.StringVarP(&cmd.opts.password, "password", "", "", "random value if empty") + if err := viper.BindPFlags(flags); err != nil { + zap.L().Warn("failed to bind viper flags", zap.Error(err)) + } +} + +func runAdduser(opts adduserOptions) error { + db, err := FromOpts(&opts.sql) + if err != nil { + return err + } + + user := entity.User{ + Email: opts.email, + Username: opts.username, + PasswordSalt: "FIXME: randomize", + } + user.PasswordHash = "FIXME: generate" + + // FIXME: randomize username, password if empty + // FIXME: verify email address validity + // FIXME: verify email address spam/blacklist + // FIXME: user.Validate() + + if err := db.Create(&user).Error; err != nil { + return err + } + + out, err := json.MarshalIndent(user, "", " ") + if err != nil { + return err + } + fmt.Println(string(out)) + + return nil +} diff --git a/sql/cmd_sql_dump.go b/sql/cmd_sql_dump.go new file mode 100644 index 000000000..515e9cbd3 --- /dev/null +++ b/sql/cmd_sql_dump.go @@ -0,0 +1,83 @@ +package sql + +import ( + "encoding/json" + "fmt" + + "github.com/jinzhu/gorm" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "go.uber.org/zap" + + "pathwar.pw/entity" + "pathwar.pw/pkg/cli" +) + +type dumpOptions struct { + sql Options `mapstructure:"sql"` + + // additional dump filters + // --anonymize +} + +type dumpCommand struct{ opts dumpOptions } + +func (cmd *dumpCommand) CobraCommand(commands cli.Commands) *cobra.Command { + cc := &cobra.Command{ + Use: "dump", + RunE: func(_ *cobra.Command, args []string) error { + opts := cmd.opts + opts.sql = GetOptions(commands) + return runDump(&opts) + }, + } + cmd.ParseFlags(cc.Flags()) + commands["sql"].ParseFlags(cc.Flags()) + return cc +} +func (cmd *dumpCommand) LoadDefaultOptions() error { return viper.Unmarshal(&cmd.opts) } +func (cmd *dumpCommand) ParseFlags(flags *pflag.FlagSet) { + if err := viper.BindPFlags(flags); err != nil { + zap.L().Warn("failed to bind viper flags", zap.Error(err)) + } +} + +func DoDump(db *gorm.DB) (*entity.Dump, error) { + dump := entity.Dump{} + if err := db.Find(&dump.Levels).Error; err != nil { + return nil, err + } + if err := db.Find(&dump.UserSessions).Error; err != nil { + return nil, err + } + if err := db.Find(&dump.Users).Error; err != nil { + return nil, err + } + if err := db.Find(&dump.Teams).Error; err != nil { + return nil, err + } + if err := db.Find(&dump.TeamMembers).Error; err != nil { + return nil, err + } + return &dump, nil +} + +func runDump(opts *dumpOptions) error { + db, err := FromOpts(&opts.sql) + if err != nil { + return err + } + + dump, err := DoDump(db) + if err != nil { + return err + } + + out, err := json.MarshalIndent(dump, "", " ") + if err != nil { + return err + } + fmt.Println(string(out)) + return nil +} diff --git a/sql/cobra.go b/sql/cobra.go deleted file mode 100644 index de0dc4af5..000000000 --- a/sql/cobra.go +++ /dev/null @@ -1,41 +0,0 @@ -package sql - -import ( - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "go.uber.org/zap" -) - -func sqlSetupFlags(flags *pflag.FlagSet, opts *Options) { - flags.StringVar(&opts.Path, "sql-path", "/tmp/pathwar.db", "SQL db path") - if err := viper.BindPFlags(flags); err != nil { - zap.L().Warn("failed to bind viper flags", zap.Error(err)) - } -} - -var globalOpts Options - -func GetOptions() *Options { - opts := globalOpts - return &opts -} - -func NewSQLCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "sql", - } - cmd.AddCommand(NewSQLDumpCommand()) - return cmd -} - -func NewSQLDumpCommand() *cobra.Command { - cmd := &cobra.Command{ - Use: "dump", - RunE: func(cmd *cobra.Command, args []string) error { - return runDump(GetOptions()) - }, - } - sqlSetupFlags(cmd.Flags(), &globalOpts) - return cmd -} diff --git a/sql/sql.go b/sql/sql.go index 0ce4d3dc7..730543385 100644 --- a/sql/sql.go +++ b/sql/sql.go @@ -1,8 +1,6 @@ package sql import ( - "encoding/json" - "fmt" "io/ioutil" "log" "os" @@ -15,10 +13,6 @@ import ( "pathwar.pw/entity" ) -type Options struct { - Path string -} - func FromOpts(opts *Options) (*gorm.DB, error) { db, err := gorm.Open("sqlite3", opts.Path) if err != nil { @@ -43,42 +37,3 @@ func FromOpts(opts *Options) (*gorm.DB, error) { return db, nil } - -func DoDump(db *gorm.DB) (*entity.Dump, error) { - dump := entity.Dump{} - if err := db.Find(&dump.Levels).Error; err != nil { - return nil, err - } - if err := db.Find(&dump.UserSessions).Error; err != nil { - return nil, err - } - if err := db.Find(&dump.Users).Error; err != nil { - return nil, err - } - if err := db.Find(&dump.Teams).Error; err != nil { - return nil, err - } - if err := db.Find(&dump.TeamMembers).Error; err != nil { - return nil, err - } - return &dump, nil -} - -func runDump(opts *Options) error { - db, err := FromOpts(opts) - if err != nil { - return err - } - - dump, err := DoDump(db) - if err != nil { - return err - } - - out, err := json.MarshalIndent(dump, "", " ") - if err != nil { - return err - } - fmt.Println(string(out)) - return nil -}