diff --git a/conf/app.example.toml b/conf/taskcafe.example.toml similarity index 100% rename from conf/app.example.toml rename to conf/taskcafe.example.toml diff --git a/internal/commands/commands.go b/internal/commands/commands.go index b8f6a82..f8b7dc8 100644 --- a/internal/commands/commands.go +++ b/internal/commands/commands.go @@ -2,7 +2,11 @@ package commands import ( "fmt" + "net/http" + "strings" + "github.com/spf13/cobra" + "github.com/spf13/viper" ) const TaskcafeConfDirEnvName = "TASKCAFE_CONFIG_DIR" @@ -26,6 +30,7 @@ var commandError error var configDir string var verbose bool var noColor bool +var cfgFile string var rootCmd = &cobra.Command{ Use: "taskcafe", @@ -33,6 +38,36 @@ var rootCmd = &cobra.Command{ Version: version, } +var migration http.FileSystem + +func init() { + cobra.OnInitialize(initConfig) + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file path") + migration = http.Dir("./migrations") +} + +func initConfig() { + if cfgFile != "" { + // Use config file from the flag. + viper.SetConfigFile(cfgFile) + } else { + // Search config in home directory with name ".cobra" (without extension). + viper.AddConfigPath("./conf") + viper.AddConfigPath(".") + viper.AddConfigPath("/etc/taskcafe") + viper.SetConfigName("taskcafe") + } + + viper.SetEnvPrefix("TASKCAFE") + viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) + viper.AutomaticEnv() + + if err := viper.ReadInConfig(); err != nil { + panic(err) + } +} + func Execute() { rootCmd.SetVersionTemplate(versionTemplate) rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd()) diff --git a/internal/commands/migrate_prod.go b/internal/commands/commands_prod.go similarity index 100% rename from internal/commands/migrate_prod.go rename to internal/commands/commands_prod.go diff --git a/internal/commands/migrate.go b/internal/commands/migrate.go index 910255a..10557e7 100644 --- a/internal/commands/migrate.go +++ b/internal/commands/migrate.go @@ -2,16 +2,15 @@ package commands import ( "fmt" - "net/http" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4/database/postgres" _ "github.com/golang-migrate/migrate/v4/source/file" "github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/jmoiron/sqlx" - "github.com/jordanknott/taskcafe/internal/config" log "github.com/sirupsen/logrus" ) @@ -28,33 +27,24 @@ func (l *MigrateLog) Verbose() bool { return l.verbose } -var migration http.FileSystem - -func init() { - migration = http.Dir("./migrations") -} - func newMigrateCmd() *cobra.Command { - return &cobra.Command{ + c := &cobra.Command{ Use: "migrate", Short: "Run the database schema migrations", Long: "Run the database schema migrations", RunE: func(cmd *cobra.Command, args []string) error { - appConfig, err := config.LoadConfig("conf/app.toml") - if err != nil { - return err - } connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable", - appConfig.Database.User, - appConfig.Database.Password, - appConfig.Database.Host, - appConfig.Database.Name, + viper.GetString("database.user"), + viper.GetString("database.password"), + viper.GetString("database.host"), + viper.GetString("database.name"), ) db, err := sqlx.Connect("postgres", connection) if err != nil { return err } defer db.Close() + driver, err := postgres.WithInstance(db.DB, &postgres.Config{}) if err != nil { return err @@ -71,10 +61,15 @@ func newMigrateCmd() *cobra.Command { logger := &MigrateLog{} m.Log = logger err = m.Up() - if err != nil { + if err != nil && err != migrate.ErrNoChange { return err } return nil }, } + viper.SetDefault("database.host", "127.0.0.1") + viper.SetDefault("database.name", "taskcafe") + viper.SetDefault("database.user", "taskcafe") + viper.SetDefault("database.password", "taskcafe_test") + return c } diff --git a/internal/commands/web.go b/internal/commands/web.go index b661880..8e0a4e8 100644 --- a/internal/commands/web.go +++ b/internal/commands/web.go @@ -2,26 +2,28 @@ package commands import ( "fmt" + "github.com/golang-migrate/migrate/v4" + "github.com/golang-migrate/migrate/v4/database/postgres" + _ "github.com/golang-migrate/migrate/v4/source/file" + "github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/spf13/cobra" + "github.com/spf13/viper" "net/http" "time" "github.com/jmoiron/sqlx" - "github.com/jordanknott/taskcafe/internal/config" "github.com/jordanknott/taskcafe/internal/route" log "github.com/sirupsen/logrus" ) +var autoMigrate bool + func newWebCmd() *cobra.Command { - return &cobra.Command{ + cc := &cobra.Command{ Use: "web", Short: "Run the web server", Long: "Run the web & api server", - Run: func(cmd *cobra.Command, args []string) { - appConfig, err := config.LoadConfig("conf/app.toml") - if err != nil { - log.WithError(err).Error("loading config") - } + RunE: func(cmd *cobra.Command, args []string) error { Formatter := new(log.TextFormatter) Formatter.TimestampFormat = "02-01-2006 15:04:05" Formatter.FullTimestamp = true @@ -29,24 +31,62 @@ func newWebCmd() *cobra.Command { log.SetLevel(log.InfoLevel) connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable", - appConfig.Database.User, - appConfig.Database.Password, - appConfig.Database.Host, - appConfig.Database.Name, + viper.GetString("database.user"), + viper.GetString("database.password"), + viper.GetString("database.host"), + viper.GetString("database.name"), ) db, err := sqlx.Connect("postgres", connection) - if err != nil { log.Panic(err) } db.SetMaxOpenConns(25) db.SetMaxIdleConns(25) db.SetConnMaxLifetime(5 * time.Minute) - defer db.Close() - log.WithFields(log.Fields{"url": appConfig.General.Host}).Info("starting server") - r, _ := route.NewRouter(appConfig, db) - http.ListenAndServe(appConfig.General.Host, r) + if viper.GetBool("migrate") { + log.Info("running auto schema migrations") + if err = runMigration(db); err != nil { + return err + } + } + + log.WithFields(log.Fields{"url": viper.GetString("server.hostname")}).Info("starting server") + r, _ := route.NewRouter(db) + http.ListenAndServe(viper.GetString("server.hostname"), r) + return nil }, } + cc.Flags().Bool("migrate", false, "if true, auto run's schema migrations before starting the web server") + viper.BindPFlag("migrate", cc.Flags().Lookup("migrate")) + viper.SetDefault("migrate", false) + viper.SetDefault("server.hostname", "0.0.0.0:3333") + viper.SetDefault("database.host", "127.0.0.1") + viper.SetDefault("database.name", "taskcafe") + viper.SetDefault("database.user", "taskcafe") + viper.SetDefault("database.password", "taskcafe_test") + return cc +} + +func runMigration(db *sqlx.DB) error { + driver, err := postgres.WithInstance(db.DB, &postgres.Config{}) + if err != nil { + return err + } + + src, err := httpfs.New(migration, "./") + if err != nil { + return err + } + m, err := migrate.NewWithInstance("httpfs", src, "postgres", driver) + if err != nil { + return err + } + logger := &MigrateLog{} + m.Log = logger + err = m.Up() + if err != nil && err != migrate.ErrNoChange { + return err + } + return nil } diff --git a/internal/graph/graph.go b/internal/graph/graph.go index a5e8407..b2ddc6e 100644 --- a/internal/graph/graph.go +++ b/internal/graph/graph.go @@ -16,17 +16,15 @@ import ( "github.com/99designs/gqlgen/graphql/playground" "github.com/google/uuid" "github.com/jordanknott/taskcafe/internal/auth" - "github.com/jordanknott/taskcafe/internal/config" "github.com/jordanknott/taskcafe/internal/db" log "github.com/sirupsen/logrus" "github.com/vektah/gqlparser/v2/gqlerror" ) // NewHandler returns a new graphql endpoint handler. -func NewHandler(config config.AppConfig, repo db.Repository) http.Handler { +func NewHandler(repo db.Repository) http.Handler { c := Config{ Resolvers: &Resolver{ - Config: config, Repository: repo, }, } diff --git a/internal/graph/resolver.go b/internal/graph/resolver.go index 6782cd1..7caf3fb 100644 --- a/internal/graph/resolver.go +++ b/internal/graph/resolver.go @@ -5,12 +5,10 @@ package graph import ( "sync" - "github.com/jordanknott/taskcafe/internal/config" "github.com/jordanknott/taskcafe/internal/db" ) type Resolver struct { - Config config.AppConfig Repository db.Repository mu sync.Mutex } diff --git a/internal/route/route.go b/internal/route/route.go index 228dc50..f76b046 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -10,7 +10,6 @@ import ( "github.com/jmoiron/sqlx" log "github.com/sirupsen/logrus" - "github.com/jordanknott/taskcafe/internal/config" "github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/frontend" "github.com/jordanknott/taskcafe/internal/graph" @@ -60,11 +59,10 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } type TaskcafeHandler struct { - config config.AppConfig - repo db.Repository + repo db.Repository } -func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, error) { +func NewRouter(dbConnection *sqlx.DB) (chi.Router, error) { formatter := new(log.TextFormatter) formatter.TimestampFormat = "02-01-2006 15:04:05" formatter.FullTimestamp = true @@ -92,7 +90,7 @@ func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, erro r.Use(middleware.Timeout(60 * time.Second)) repository := db.NewRepository(dbConnection) - taskcafeHandler := TaskcafeHandler{config, *repository} + taskcafeHandler := TaskcafeHandler{*repository} var imgServer = http.FileServer(http.Dir("./uploads/")) r.Group(func(mux chi.Router) { @@ -105,7 +103,7 @@ func NewRouter(config config.AppConfig, dbConnection *sqlx.DB) (chi.Router, erro mux.Use(AuthenticationMiddleware) mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload) mux.Post("/auth/install", taskcafeHandler.InstallHandler) - mux.Handle("/graphql", graph.NewHandler(config, *repository)) + mux.Handle("/graphql", graph.NewHandler(*repository)) }) frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}