3
0
corteza/pkg/cli/runner.go

295 lines
6.9 KiB
Go

package cli
import (
"context"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"go.uber.org/zap"
"github.com/cortezaproject/corteza-server/internal/db"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/cli/options"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/pkg/sentry"
)
type (
Runner func(ctx context.Context, cmd *cobra.Command, c *Config) error
Runners []Runner
CommandMaker func(ctx context.Context, c *Config) *cobra.Command
CommandMakers []CommandMaker
FlagBinder func(cmd *cobra.Command, c *Config)
FlagBinders []FlagBinder
Mounter func(r chi.Router)
Mounters []Mounter
Config struct {
init bool
// Service name (messaging, system...)
// See comments on other fields for how it is used.
ServiceName string
// Prefix for ENV variables
EnvPrefix string
// Logger name for internal services, defaults to ServiceName
LoggerName string
Log *zap.Logger
// General options
SmtpOpt *options.SMTPOpt
JwtOpt *options.JWTOpt
HttpClientOpt *options.HttpClientOpt
DbOpt *options.DBOpt
ProvisionOpt *options.ProvisionOpt
SentryOpt *options.SentryOpt
StorageOpt *options.StorageOpt
// DB Connection name, defaults to ServiceName
DatabaseName string
// Root command name, , defaults to "corteza-server-<ServiceName>"
RootCommandName string
// Database setup/connection procedure
// Runner autobinds default runner that tries to connect using DbOpt.DSN
RootCommandDBSetup Runners
// All that needs to be initialized before any sub-comman is executed
RootCommandPreRun Runners
// ******************************************************************
// API Server instance
ApiServer *api.Server
// API Server command name
ApiServerCommandName string
// Code that needs to be executed before HTTP server is started
ApiServerPreRun Runners
// Routers that we mount on HTTP server
ApiServerRoutes Mounters
// Sets-up all available subcommands.
AdtSubCommands CommandMakers
// Database migration code
// This is used for "provision migrate-database" command and after db connection is
// established (if --provision-migrate-database is enabled
ProvisionMigrateDatabase Runners
// Access control initial setup
// Reapplies default access control rules for roles "everyone" [1] and "admin" [2]
ProvisionAccessControl Runners
// ******************************************************************
// This callback behaves a bit differently and should be called manually
// from wherever we need service initialized
InitServices func(ctx context.Context, c *Config)
}
)
func init() {
// Have logger ready in case we need to log anything
// before it gets properly initialized through InitGeneralServices
logger.Init()
}
func (rr Runners) Run(ctx context.Context, cmd *cobra.Command, c *Config) (err error) {
for i := range rr {
err = rr[i](ctx, cmd, c)
if err != nil {
return
}
}
return
}
func (rr Mounters) MountRoutes(r chi.Router) {
for i := range rr {
rr[i](r)
}
}
func (bb FlagBinders) Bind(cmd *cobra.Command, c *Config) {
for i := range bb {
bb[i](cmd, c)
}
}
func (mm CommandMakers) Make(ctx context.Context, c *Config) []*cobra.Command {
var (
valid = make([]*cobra.Command, 0)
cmd *cobra.Command
)
for i := range mm {
if cmd = mm[i](ctx, c); cmd != nil {
valid = append(valid, cmd)
}
}
return valid
}
func CombineFlagBinders(rr ...FlagBinders) (out FlagBinders) {
for i := range rr {
out = append(out, rr[i]...)
}
return out
}
func (c *Config) Init() {
if c.init {
return
}
if c.Log == nil {
c.Log = logger.Default()
}
if c.LoggerName == "" {
c.LoggerName = c.ServiceName
}
if c.EnvPrefix == "" {
c.EnvPrefix = c.ServiceName
}
c.Log = c.Log.Named(c.LoggerName)
if c.RootCommandName == "" {
c.RootCommandName = "corteza-server-" + c.ServiceName
}
if c.ApiServerCommandName == "" {
c.ApiServerCommandName = "serve-api"
}
if c.DatabaseName == "" {
c.DatabaseName = c.ServiceName
}
c.SmtpOpt = options.SMTP(c.EnvPrefix)
c.JwtOpt = options.JWT(c.EnvPrefix)
c.HttpClientOpt = options.HttpClient(c.EnvPrefix)
c.DbOpt = options.DB(c.ServiceName)
c.ProvisionOpt = options.Provision(c.ServiceName)
c.SentryOpt = options.Sentry(c.EnvPrefix)
c.StorageOpt = options.Storage(c.EnvPrefix)
if c.RootCommandDBSetup == nil {
c.RootCommandDBSetup = Runners{func(ctx context.Context, cmd *cobra.Command, c *Config) (err error) {
if c.DbOpt != nil {
_, err = db.TryToConnect(ctx, c.Log, c.DatabaseName, *c.DbOpt)
if err != nil {
return errors.Wrap(err, "could not connect to database")
}
}
return
}}
}
if c.ApiServer == nil {
c.ApiServer = api.NewServer(c.Log)
}
for i := range c.ApiServerRoutes {
c.ApiServer.MountRoutes(c.ApiServerRoutes[i])
}
}
// MakeCLI creates command line interface
//
// It tries to construct "serve-api" and "provision" sub-commands
// if configured properly (see Config struct)
func (c *Config) MakeCLI(ctx context.Context) (cmd *cobra.Command) {
c.Init()
cmd = &cobra.Command{
Use: c.RootCommandName,
TraverseChildren: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) (err error) {
if err = sentry.Init(c.SentryOpt); err != nil {
c.Log.Error("could not initialize Sentry", zap.Error(err))
}
defer sentry.Recover()
InitGeneralServices(c.SmtpOpt, c.JwtOpt, c.HttpClientOpt)
err = c.RootCommandDBSetup.Run(ctx, cmd, c)
if err != nil {
c.Log.Error("Failed to connect to the database", zap.Error(err))
return nil
}
err = c.RootCommandPreRun.Run(ctx, cmd, c)
if err != nil {
c.Log.Error("Failed to run command pre-run scripts", zap.Error(err))
return nil
}
return nil
},
}
serveApiCmd := c.ApiServer.Command(ctx, c.ApiServerCommandName, c.EnvPrefix, func(ctx context.Context) (err error) {
defer sentry.Recover()
return c.ApiServerPreRun.Run(ctx, cmd, c)
})
cmd.AddCommand(serveApiCmd)
if len(c.ProvisionMigrateDatabase) > 0 || len(c.ProvisionAccessControl) > 0 {
var (
provisionCmd = &cobra.Command{
Use: "provision",
Short: "Provision tasks",
}
)
// Add only commands with defined callbacks
if len(c.ProvisionMigrateDatabase) > 0 {
provisionCmd.AddCommand(&cobra.Command{
Use: "access-control-rules",
Short: "Reset access control rules & roles",
RunE: func(cmd *cobra.Command, args []string) error {
return c.ProvisionAccessControl.Run(ctx, nil, c)
},
})
}
// Add only commands with defined callbacks
if len(c.ProvisionAccessControl) > 0 {
provisionCmd.AddCommand(&cobra.Command{
Use: "migrate-database",
Short: "Run database migration scripts",
RunE: func(cmd *cobra.Command, args []string) error {
return c.ProvisionMigrateDatabase.Run(ctx, nil, c)
},
})
}
cmd.AddCommand(provisionCmd)
}
cmd.AddCommand(c.AdtSubCommands.Make(ctx, c)...)
return
}