diff --git a/auth/flags.go b/auth/flags.go index 6bf67ba36..18dce24c0 100644 --- a/auth/flags.go +++ b/auth/flags.go @@ -23,7 +23,7 @@ func (c configuration) validate() error { } // Flags should be called from main to register flags -func Flags() { +func Flags(_ ...string) { flag.StringVar(&config.jwtSecret, "auth-jwt-secret", "", "JWT Secret") flag.Int64Var(&config.jwtExpiry, "auth-jwt-expiry", 3600, "JWT Expiration in minutes") } diff --git a/cmd/crm/flags.go b/cmd/crm/flags.go index 042fd9b8c..b705f48c2 100644 --- a/cmd/crm/flags.go +++ b/cmd/crm/flags.go @@ -6,24 +6,16 @@ import ( ) type configuration struct { - httpAddr string - dbDSN string monitorInterval int } -func flags(prefix string, mountFlags ...func()) configuration { +func flags(prefix string, mountFlags ...func(...string)) configuration { var config configuration - p := func(s string) string { - return prefix + "-" + s - } - - flag.StringVar(&config.httpAddr, p("http-addr"), ":3000", "Listen address for HTTP server") - flag.StringVar(&config.dbDSN, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection") flag.IntVar(&config.monitorInterval, "monitor-interval", 300, "Monitor interval (seconds, 0 = disable)") for _, mount := range mountFlags { - mount() + mount(prefix) } flag.Parse() diff --git a/cmd/crm/main.go b/cmd/crm/main.go index 10c4b0b76..0cb219580 100644 --- a/cmd/crm/main.go +++ b/cmd/crm/main.go @@ -2,58 +2,25 @@ package main import ( "log" - "net" "os" - "net/http" - - "github.com/go-chi/chi" - + "github.com/crusttech/crust/crm" "github.com/crusttech/crust/auth" - "github.com/crusttech/crust/crm/rest" "github.com/crusttech/crust/rbac" - "github.com/titpetric/factory" ) -func handleError(err error, message string) { - if message == "" { - message = "Error making API call" - } - if err != nil { - log.Fatalf(message+": %v", err.Error()) - } -} - func main() { - config := flags("crm", rbac.Flags, auth.Flags) + config := flags("crm", crm.Flags, rbac.Flags, auth.Flags) // log to stdout not stderr log.SetOutput(os.Stdout) + log.SetFlags(log.LstdFlags | log.Lshortfile) go NewMonitor(config.monitorInterval) - // set up database connection - factory.Database.Add("default", config.dbDSN) - db, err := factory.Database.Get() - handleError(err, "Can't connect to database") - db.Profiler = &factory.Database.ProfilerStdout - - // listen socket for http server - log.Println("Starting http server on address " + config.httpAddr) - listener, err := net.Listen("tcp", config.httpAddr) - handleError(err, "Can't listen on addr "+config.httpAddr) - - // route options - routeOptions, err := RouteOptions{}.New() - handleError(err, "Error creating RouteOptions object") - - r := chi.NewRouter() - - // JWT Auth - jwtAuth, err := auth.JWT() - handleError(err, "Error creating JWT Auth object") - r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator()) - - // mount routes - MountRoutes(r, routeOptions, rest.MountRoutes(jwtAuth)) - http.Serve(listener, r) + if err := crm.Init(); err != nil { + log.Fatalf("Error initializing crm: %+v", err) + } + if err := crm.Start(); err != nil { + log.Fatalf("Error starting/running crm: %+v", err) + } } diff --git a/cmd/crm/options.go b/cmd/crm/options.go deleted file mode 100644 index a41316e83..000000000 --- a/cmd/crm/options.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -type RouteOptions struct { - enableLogging bool -} - -func (RouteOptions) New() (*RouteOptions, error) { - opts := &RouteOptions{} - opts.enableLogging = true - return opts, nil -} - -func (o *RouteOptions) EnableLogging(enable bool) *RouteOptions { - o.enableLogging = enable - return o -} diff --git a/cmd/sam/flags.go b/cmd/sam/flags.go index 042fd9b8c..b705f48c2 100644 --- a/cmd/sam/flags.go +++ b/cmd/sam/flags.go @@ -6,24 +6,16 @@ import ( ) type configuration struct { - httpAddr string - dbDSN string monitorInterval int } -func flags(prefix string, mountFlags ...func()) configuration { +func flags(prefix string, mountFlags ...func(...string)) configuration { var config configuration - p := func(s string) string { - return prefix + "-" + s - } - - flag.StringVar(&config.httpAddr, p("http-addr"), ":3000", "Listen address for HTTP server") - flag.StringVar(&config.dbDSN, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection") flag.IntVar(&config.monitorInterval, "monitor-interval", 300, "Monitor interval (seconds, 0 = disable)") for _, mount := range mountFlags { - mount() + mount(prefix) } flag.Parse() diff --git a/cmd/sam/main.go b/cmd/sam/main.go index 5aac8cb76..aeba4d3d4 100644 --- a/cmd/sam/main.go +++ b/cmd/sam/main.go @@ -1,17 +1,12 @@ package main import ( - "github.com/SentimensRG/sigctx" "github.com/crusttech/crust/auth" "github.com/crusttech/crust/rbac" - "github.com/crusttech/crust/sam/rest" - "github.com/crusttech/crust/sam/websocket" - "github.com/go-chi/chi" - "github.com/titpetric/factory" + + "github.com/crusttech/crust/sam" "log" - "net" - "net/http" "os" ) @@ -25,39 +20,17 @@ func handleError(err error, message string) { } func main() { - var ctx = sigctx.New() - - config := flags("sam", auth.Flags, rbac.Flags, websocket.Flags) + config := flags("sam", sam.Flags, auth.Flags, rbac.Flags) // log to stdout not stderr log.SetOutput(os.Stdout) + log.SetFlags(log.LstdFlags | log.Lshortfile) go NewMonitor(config.monitorInterval) - // set up database connection - factory.Database.Add("default", config.dbDSN) - db, err := factory.Database.Get() - handleError(err, "Can't connect to database") - db.Profiler = &factory.Database.ProfilerStdout - - // listen socket for http server - log.Println("Starting http server on address " + config.httpAddr) - listener, err := net.Listen("tcp", config.httpAddr) - handleError(err, "Can't listen on addr "+config.httpAddr) - - // route options - routeOptions, err := RouteOptions{}.New() - handleError(err, "Error creating RouteOptions object") - - r := chi.NewRouter() - - // JWT Auth - jwtAuth, err := auth.JWT() - handleError(err, "Error creating JWT Auth object") - r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator()) - - // mount REST & WS routes - MountRoutes(r, routeOptions, rest.MountRoutes(jwtAuth), websocket.MountRoutes(ctx)) - - go http.Serve(listener, r) - <-ctx.Done() + if err := sam.Init(); err != nil { + log.Fatalf("Error initializing sam: %+v", err) + } + if err := sam.Start(); err != nil { + log.Fatalf("Error starting/running sam: %+v", err) + } } diff --git a/cmd/sam/options.go b/cmd/sam/options.go deleted file mode 100644 index a41316e83..000000000 --- a/cmd/sam/options.go +++ /dev/null @@ -1,16 +0,0 @@ -package main - -type RouteOptions struct { - enableLogging bool -} - -func (RouteOptions) New() (*RouteOptions, error) { - opts := &RouteOptions{} - opts.enableLogging = true - return opts, nil -} - -func (o *RouteOptions) EnableLogging(enable bool) *RouteOptions { - o.enableLogging = enable - return o -} diff --git a/crm/flags.go b/crm/flags.go new file mode 100644 index 000000000..0e1a656ed --- /dev/null +++ b/crm/flags.go @@ -0,0 +1,51 @@ +package crm + +import ( + "github.com/namsral/flag" + "github.com/pkg/errors" +) + +type ( + configuration struct { + http struct { + addr string + logging bool + } + db struct { + dsn string + } + } +) + +var config *configuration + +func (c *configuration) Validate() error { + if c == nil { + return errors.New("CRM config is not initialized, need to call Flags()") + } + if c.http.addr == "" { + return errors.New("No HTTP Addr is set, can't listen for HTTP") + } + if c.db.dsn == "" { + return errors.New("No DB DSN is set, can't connect to database") + } + return nil +} + +func Flags(prefix ...string) { + if config != nil { + return + } + if len(prefix) == 0 { + panic("crm.Flags() needs prefix on first call") + } + config := new(configuration) + + p := func(s string) string { + return prefix[0] + "-" + s + } + + flag.StringVar(&config.http.addr, p("http-addr"), ":3000", "Listen address for HTTP server") + flag.BoolVar(&config.http.logging, p("http-log"), true, "Enable/disable HTTP request log") + flag.StringVar(&config.db.dsn, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection") +} diff --git a/cmd/crm/routes.go b/crm/routes.go similarity index 91% rename from cmd/crm/routes.go rename to crm/routes.go index eac9588b3..1ebc20967 100644 --- a/cmd/crm/routes.go +++ b/crm/routes.go @@ -1,4 +1,4 @@ -package main +package crm import ( "fmt" @@ -11,7 +11,7 @@ import ( ) // MountRoutes will register API routes -func MountRoutes(r chi.Router, opts *RouteOptions, mountRoutes ...func(r chi.Router)) { +func MountRoutes(r chi.Router, opts *configuration, mountRoutes ...func(r chi.Router)) { // CORS for local development... cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -22,7 +22,7 @@ func MountRoutes(r chi.Router, opts *RouteOptions, mountRoutes ...func(r chi.Rou }) r.Use(cors.Handler) - if opts.enableLogging { + if opts.http.logging { r.Use(middleware.Logger) } diff --git a/crm/start.go b/crm/start.go new file mode 100644 index 000000000..3761058a5 --- /dev/null +++ b/crm/start.go @@ -0,0 +1,60 @@ +package crm + +import ( + "fmt" + "log" + "net" + "net/http" + + "github.com/SentimensRG/sigctx" + "github.com/go-chi/chi" + "github.com/pkg/errors" + + "github.com/crusttech/crust/auth" + "github.com/crusttech/crust/crm/rest" + + "github.com/titpetric/factory" +) + +func Init() error { + // validate configuration + if err := config.Validate(); err != nil { + return err + } + + // start/configure database connection + factory.Database.Add("default", config.db.dsn) + db, err := factory.Database.Get() + if err != nil { + return err + } + db.Profiler = &factory.Database.ProfilerStdout + return nil +} + +func Start() error { + var ctx = sigctx.New() + + log.Println("Starting http server on address " + config.http.addr) + listener, err := net.Listen("tcp", config.http.addr) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", config.http.addr)) + } + + // JWT Auth + jwtAuth, err := auth.JWT() + if err != nil { + return errors.Wrap(err, "Error creating JWT Auth object") + } + + r := chi.NewRouter() + r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator()) + + // mount routes + MountRoutes(r, config, rest.MountRoutes(jwtAuth)) + + go http.Serve(listener, r) + <-ctx.Done() + + return nil +} diff --git a/rbac/flags.go b/rbac/flags.go index 408fd722a..f7c2ab82c 100644 --- a/rbac/flags.go +++ b/rbac/flags.go @@ -30,7 +30,7 @@ func (c configuration) validate() error { } // Flags should be called from main to register flags -func Flags() { +func Flags(_ ...string) { flag.StringVar(&config.auth, "rbac-auth", "username:password", "Credentials to use for RBAC queries") flag.StringVar(&config.tenant, "rbac-tenant", "", "Tenant ID") flag.StringVar(&config.baseURL, "rbac-base-url", "", "RBAC Base URL") diff --git a/sam/flags.go b/sam/flags.go new file mode 100644 index 000000000..a56cfd8bd --- /dev/null +++ b/sam/flags.go @@ -0,0 +1,58 @@ +package sam + +import ( + "github.com/namsral/flag" + "github.com/pkg/errors" + + "github.com/crusttech/crust/sam/websocket" +) + +type ( + configuration struct { + http struct { + addr string + logging bool + } + websocket websocket.Configuration + db struct { + dsn string + } + } +) + +var config *configuration + +func (c *configuration) Validate() error { + if c == nil { + return errors.New("SAM config is not initialized, need to call Flags()") + } + if c.http.addr == "" { + return errors.New("No HTTP Addr is set, can't listen for HTTP") + } + if c.db.dsn == "" { + return errors.New("No DB DSN is set, can't connect to database") + } + if err := c.websocket.Validate(); err != nil { + return err + } + return nil +} + +func Flags(prefix ...string) { + if config != nil { + return + } + if len(prefix) == 0 { + panic("sam.Flags() needs prefix on first call") + } + config := new(configuration) + (&config.websocket).Init() + + p := func(s string) string { + return prefix[0] + "-" + s + } + + flag.StringVar(&config.http.addr, p("http-addr"), ":3000", "Listen address for HTTP server") + flag.BoolVar(&config.http.logging, p("http-log"), true, "Enable/disable HTTP request log") + flag.StringVar(&config.db.dsn, p("db-dsn"), "crust:crust@tcp(db1:3306)/crust?collation=utf8mb4_general_ci", "DSN for database connection") +} diff --git a/cmd/sam/routes.go b/sam/routes.go similarity index 91% rename from cmd/sam/routes.go rename to sam/routes.go index eac9588b3..61ca54695 100644 --- a/cmd/sam/routes.go +++ b/sam/routes.go @@ -1,4 +1,4 @@ -package main +package sam import ( "fmt" @@ -11,7 +11,7 @@ import ( ) // MountRoutes will register API routes -func MountRoutes(r chi.Router, opts *RouteOptions, mountRoutes ...func(r chi.Router)) { +func MountRoutes(r chi.Router, opts *configuration, mountRoutes ...func(r chi.Router)) { // CORS for local development... cors := cors.New(cors.Options{ AllowedOrigins: []string{"*"}, @@ -22,7 +22,7 @@ func MountRoutes(r chi.Router, opts *RouteOptions, mountRoutes ...func(r chi.Rou }) r.Use(cors.Handler) - if opts.enableLogging { + if opts.http.logging { r.Use(middleware.Logger) } diff --git a/sam/start.go b/sam/start.go new file mode 100644 index 000000000..4179a3d0b --- /dev/null +++ b/sam/start.go @@ -0,0 +1,61 @@ +package sam + +import ( + "fmt" + "log" + "net" + "net/http" + + "github.com/SentimensRG/sigctx" + "github.com/go-chi/chi" + "github.com/pkg/errors" + + "github.com/crusttech/crust/auth" + "github.com/crusttech/crust/sam/rest" + "github.com/crusttech/crust/sam/websocket" + + "github.com/titpetric/factory" +) + +func Init() error { + // validate configuration + if err := config.Validate(); err != nil { + return err + } + + // start/configure database connection + factory.Database.Add("default", config.db.dsn) + db, err := factory.Database.Get() + if err != nil { + return err + } + db.Profiler = &factory.Database.ProfilerStdout + return nil +} + +func Start() error { + var ctx = sigctx.New() + + log.Println("Starting http server on address " + config.http.addr) + listener, err := net.Listen("tcp", config.http.addr) + if err != nil { + return errors.Wrap(err, fmt.Sprintf("Can't listen on addr %s", config.http.addr)) + } + + // JWT Auth + jwtAuth, err := auth.JWT() + if err != nil { + return errors.Wrap(err, "Error creating JWT Auth object") + } + + r := chi.NewRouter() + r.Use(jwtAuth.Verifier(), jwtAuth.Authenticator()) + + // mount routes + MountRoutes(r, config, rest.MountRoutes(jwtAuth), websocket.MountRoutes(ctx, config.websocket)) + + go http.Serve(listener, r) + <-ctx.Done() + + return nil +} diff --git a/sam/websocket/configuration.go b/sam/websocket/configuration.go new file mode 100644 index 000000000..4ffb76f50 --- /dev/null +++ b/sam/websocket/configuration.go @@ -0,0 +1,43 @@ +package websocket + +import ( + "github.com/namsral/flag" + "github.com/pkg/errors" + "time" +) + +type ( + Configuration struct { + writeTimeout time.Duration + pingTimeout time.Duration + pingPeriod time.Duration + + pubSubMode string + pubSubRedis string + pubSubInterval time.Duration + } +) + +// Validate returns error if there is an issue with the config +func (c *Configuration) Validate() error { + switch c.pubSubMode { + case "redis", "poll": + default: + return errors.Errorf("Unknown pubSubMode: %s", c.pubSubMode) + } + if c.pubSubMode == "redis" && c.pubSubRedis == "" { + return errors.New("No host defined for mode=redis, pubSubRedis is empty") + } + return nil +} + +// Init binds flags to websocket configuration structure +func (c *Configuration) Init() { + c.writeTimeout = 15 * time.Second + c.pingTimeout = 120 * time.Second + c.pingPeriod = (c.pingTimeout * 10) / 9 + + flag.StringVar(&c.pubSubMode, "pubsub", "poll", "Pubsub mode (poll, redis)") + flag.StringVar(&c.pubSubRedis, "pubsub-redis", "", "Redis Pub/Sub hostname") + flag.DurationVar(&c.pubSubInterval, "pubsub-poll-interval", 3*time.Second, "Pub/Sub polling interval (3s, 12m, 3h...)") +} diff --git a/sam/websocket/event_queue.go b/sam/websocket/event_queue.go index 023bec4e0..7071d26f8 100644 --- a/sam/websocket/event_queue.go +++ b/sam/websocket/event_queue.go @@ -57,7 +57,7 @@ func (eq *eventQueue) store(ctx context.Context, qp eventQueuePusher) { }() } -func (eq *eventQueue) feedSessions(ctx context.Context, qp eventQueuePuller, store eventQueueWalker) error { +func (eq *eventQueue) feedSessions(ctx context.Context, config Configuration, qp eventQueuePuller, store eventQueueWalker) error { newMessageEvent := make(chan struct{}, eventQueueBacklog) done := make(chan error, 1) diff --git a/sam/websocket/flags.go b/sam/websocket/flags.go deleted file mode 100644 index e2dc90cf0..000000000 --- a/sam/websocket/flags.go +++ /dev/null @@ -1,44 +0,0 @@ -package websocket - -import ( - "github.com/namsral/flag" - "github.com/pkg/errors" - "time" -) - -type ( - configuration struct { - writeTimeout time.Duration - pingTimeout time.Duration - pingPeriod time.Duration - - pubSubMode string - pubSubRedis string - pubSubInterval time.Duration - } -) - -var config configuration - -func (c configuration) validate() error { - switch c.pubSubMode { - case "redis", "poll": - default: - return errors.Errorf("Unknown pubSubMode: %s", c.pubSubMode) - } - if c.pubSubMode == "redis" && c.pubSubRedis == "" { - return errors.New("No host defined for mode=redis, pubSubRedis is empty") - } - return nil -} - -// Flags should be called from main to register flags -func Flags() { - config.writeTimeout = 15 * time.Second - config.pingTimeout = 120 * time.Second - config.pingPeriod = (config.pingTimeout * 10) / 9 - - flag.StringVar(&config.pubSubMode, "pubsub", "poll", "Pubsub mode (poll, redis)") - flag.StringVar(&config.pubSubRedis, "pubsub-redis", "", "Redis Pub/Sub hostname") - flag.DurationVar(&config.pubSubInterval, "pubsub-poll-interval", 3*time.Second, "Pub/Sub polling interval (3s, 12m, 3h...)") -} diff --git a/sam/websocket/router.go b/sam/websocket/router.go index 4c956c400..2bc220d84 100644 --- a/sam/websocket/router.go +++ b/sam/websocket/router.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/chi" ) -func MountRoutes(ctx context.Context) func(chi.Router) { +func MountRoutes(ctx context.Context, config Configuration) func(chi.Router) { return func(r chi.Router) { var ( // @todo move this 1 level up & join with rest init functions @@ -16,10 +16,10 @@ func MountRoutes(ctx context.Context) func(chi.Router) { repo := repository.New() - go eq.feedSessions(ctx, repo, store) + go eq.feedSessions(ctx, config, repo, store) eq.store(ctx, repo) - websocket := Websocket{}.New(svcUser) + websocket := Websocket{}.New(svcUser, config) r.Group(func(r chi.Router) { r.Route("/websocket", func(r chi.Router) { r.Get("/", websocket.Open) diff --git a/sam/websocket/session.go b/sam/websocket/session.go index 479da6002..766491361 100644 --- a/sam/websocket/session.go +++ b/sam/websocket/session.go @@ -27,13 +27,13 @@ type ( remoteAddr string - config configuration + config Configuration user *types.User } ) -func (Session) New(ctx context.Context, conn *websocket.Conn) *Session { +func (Session) New(ctx context.Context, config Configuration, conn *websocket.Conn) *Session { return &Session{ conn: conn, ctx: ctx, diff --git a/sam/websocket/websocket.go b/sam/websocket/websocket.go index 1e0d8f40e..81c274098 100644 --- a/sam/websocket/websocket.go +++ b/sam/websocket/websocket.go @@ -17,6 +17,7 @@ type ( svc struct { userFinder wsUserFinder } + config Configuration } wsUserFinder interface { @@ -24,8 +25,10 @@ type ( } ) -func (Websocket) New(svcUser wsUserFinder) *Websocket { - ws := &Websocket{} +func (Websocket) New(svcUser wsUserFinder, config Configuration) *Websocket { + ws := &Websocket{ + config: config, + } ws.svc.userFinder = svcUser return ws } @@ -65,7 +68,7 @@ func (ws Websocket) Open(w http.ResponseWriter, r *http.Request) { return } - session := store.Save((&Session{}).New(ctx, conn)) + session := store.Save((&Session{}).New(ctx, ws.config, conn)) session.user = user if err := session.Handle(); err != nil {