Skip to content

Commit

Permalink
feat: add 'sql adduser --email [...]' cli command + refactor cobra
Browse files Browse the repository at this point in the history
  • Loading branch information
moul committed Jan 10, 2019
1 parent 5a6fcb6 commit f5be441
Show file tree
Hide file tree
Showing 10 changed files with 318 additions and 155 deletions.
33 changes: 23 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"

"pathwar.pw/pkg/cli"
"pathwar.pw/server"
"pathwar.pw/sql"
)
Expand All @@ -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()
Expand All @@ -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
Expand Down
16 changes: 16 additions & 0 deletions pkg/cli/cobra.go
Original file line number Diff line number Diff line change
@@ -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
59 changes: 59 additions & 0 deletions server/cmd_server.go
Original file line number Diff line number Diff line change
@@ -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
}
35 changes: 0 additions & 35 deletions server/cobra.go

This file was deleted.

34 changes: 10 additions & 24 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package server
import (
"context"
"crypto/rand"
"encoding/json"
"net"
"net/http"

Expand All @@ -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"
Expand All @@ -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()

Expand All @@ -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,
Expand All @@ -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")
Expand Down Expand Up @@ -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")
}
Expand All @@ -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)
Expand Down
43 changes: 43 additions & 0 deletions sql/cmd_sql.go
Original file line number Diff line number Diff line change
@@ -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
}
84 changes: 84 additions & 0 deletions sql/cmd_sql_adduser.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit f5be441

Please sign in to comment.