feat: replace config system with viper based system

allows for config settings to be easily set through ENV variables,
config files, or CLI flags

adds flag to run migration on web server start (fixes #29)
This commit is contained in:
Jordan Knott 2020-08-12 22:17:42 -05:00
parent b28e000320
commit c2ef8a7d56
8 changed files with 109 additions and 45 deletions

View File

@ -2,7 +2,11 @@ package commands
import ( import (
"fmt" "fmt"
"net/http"
"strings"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
) )
const TaskcafeConfDirEnvName = "TASKCAFE_CONFIG_DIR" const TaskcafeConfDirEnvName = "TASKCAFE_CONFIG_DIR"
@ -26,6 +30,7 @@ var commandError error
var configDir string var configDir string
var verbose bool var verbose bool
var noColor bool var noColor bool
var cfgFile string
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "taskcafe", Use: "taskcafe",
@ -33,6 +38,36 @@ var rootCmd = &cobra.Command{
Version: version, 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() { func Execute() {
rootCmd.SetVersionTemplate(versionTemplate) rootCmd.SetVersionTemplate(versionTemplate)
rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd()) rootCmd.AddCommand(newWebCmd(), newMigrateCmd(), newTokenCmd())

View File

@ -2,16 +2,15 @@ package commands
import ( import (
"fmt" "fmt"
"net/http"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/golang-migrate/migrate/v4" "github.com/golang-migrate/migrate/v4"
"github.com/golang-migrate/migrate/v4/database/postgres" "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file" _ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/golang-migrate/migrate/v4/source/httpfs" "github.com/golang-migrate/migrate/v4/source/httpfs"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
@ -28,33 +27,24 @@ func (l *MigrateLog) Verbose() bool {
return l.verbose return l.verbose
} }
var migration http.FileSystem
func init() {
migration = http.Dir("./migrations")
}
func newMigrateCmd() *cobra.Command { func newMigrateCmd() *cobra.Command {
return &cobra.Command{ c := &cobra.Command{
Use: "migrate", Use: "migrate",
Short: "Run the database schema migrations", Short: "Run the database schema migrations",
Long: "Run the database schema migrations", Long: "Run the database schema migrations",
RunE: func(cmd *cobra.Command, args []string) error { 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", connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
appConfig.Database.User, viper.GetString("database.user"),
appConfig.Database.Password, viper.GetString("database.password"),
appConfig.Database.Host, viper.GetString("database.host"),
appConfig.Database.Name, viper.GetString("database.name"),
) )
db, err := sqlx.Connect("postgres", connection) db, err := sqlx.Connect("postgres", connection)
if err != nil { if err != nil {
return err return err
} }
defer db.Close() defer db.Close()
driver, err := postgres.WithInstance(db.DB, &postgres.Config{}) driver, err := postgres.WithInstance(db.DB, &postgres.Config{})
if err != nil { if err != nil {
return err return err
@ -71,10 +61,15 @@ func newMigrateCmd() *cobra.Command {
logger := &MigrateLog{} logger := &MigrateLog{}
m.Log = logger m.Log = logger
err = m.Up() err = m.Up()
if err != nil { if err != nil && err != migrate.ErrNoChange {
return err return err
} }
return nil 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
} }

View File

@ -2,26 +2,28 @@ package commands
import ( import (
"fmt" "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/cobra"
"github.com/spf13/viper"
"net/http" "net/http"
"time" "time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/route" "github.com/jordanknott/taskcafe/internal/route"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
) )
var autoMigrate bool
func newWebCmd() *cobra.Command { func newWebCmd() *cobra.Command {
return &cobra.Command{ cc := &cobra.Command{
Use: "web", Use: "web",
Short: "Run the web server", Short: "Run the web server",
Long: "Run the web & api server", Long: "Run the web & api server",
Run: func(cmd *cobra.Command, args []string) { RunE: func(cmd *cobra.Command, args []string) error {
appConfig, err := config.LoadConfig("conf/app.toml")
if err != nil {
log.WithError(err).Error("loading config")
}
Formatter := new(log.TextFormatter) Formatter := new(log.TextFormatter)
Formatter.TimestampFormat = "02-01-2006 15:04:05" Formatter.TimestampFormat = "02-01-2006 15:04:05"
Formatter.FullTimestamp = true Formatter.FullTimestamp = true
@ -29,24 +31,62 @@ func newWebCmd() *cobra.Command {
log.SetLevel(log.InfoLevel) log.SetLevel(log.InfoLevel)
connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable", connection := fmt.Sprintf("user=%s password=%s host=%s dbname=%s sslmode=disable",
appConfig.Database.User, viper.GetString("database.user"),
appConfig.Database.Password, viper.GetString("database.password"),
appConfig.Database.Host, viper.GetString("database.host"),
appConfig.Database.Name, viper.GetString("database.name"),
) )
db, err := sqlx.Connect("postgres", connection) db, err := sqlx.Connect("postgres", connection)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
db.SetMaxOpenConns(25) db.SetMaxOpenConns(25)
db.SetMaxIdleConns(25) db.SetMaxIdleConns(25)
db.SetConnMaxLifetime(5 * time.Minute) db.SetConnMaxLifetime(5 * time.Minute)
defer db.Close() defer db.Close()
log.WithFields(log.Fields{"url": appConfig.General.Host}).Info("starting server") if viper.GetBool("migrate") {
r, _ := route.NewRouter(appConfig, db) log.Info("running auto schema migrations")
http.ListenAndServe(appConfig.General.Host, r) 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
} }

View File

@ -16,17 +16,15 @@ import (
"github.com/99designs/gqlgen/graphql/playground" "github.com/99designs/gqlgen/graphql/playground"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/jordanknott/taskcafe/internal/auth" "github.com/jordanknott/taskcafe/internal/auth"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/vektah/gqlparser/v2/gqlerror" "github.com/vektah/gqlparser/v2/gqlerror"
) )
// NewHandler returns a new graphql endpoint handler. // 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{ c := Config{
Resolvers: &Resolver{ Resolvers: &Resolver{
Config: config,
Repository: repo, Repository: repo,
}, },
} }

View File

@ -5,12 +5,10 @@ package graph
import ( import (
"sync" "sync"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
) )
type Resolver struct { type Resolver struct {
Config config.AppConfig
Repository db.Repository Repository db.Repository
mu sync.Mutex mu sync.Mutex
} }

View File

@ -10,7 +10,6 @@ import (
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/jordanknott/taskcafe/internal/config"
"github.com/jordanknott/taskcafe/internal/db" "github.com/jordanknott/taskcafe/internal/db"
"github.com/jordanknott/taskcafe/internal/frontend" "github.com/jordanknott/taskcafe/internal/frontend"
"github.com/jordanknott/taskcafe/internal/graph" "github.com/jordanknott/taskcafe/internal/graph"
@ -60,11 +59,10 @@ func (h FrontendHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
type TaskcafeHandler struct { 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 := new(log.TextFormatter)
formatter.TimestampFormat = "02-01-2006 15:04:05" formatter.TimestampFormat = "02-01-2006 15:04:05"
formatter.FullTimestamp = true 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)) r.Use(middleware.Timeout(60 * time.Second))
repository := db.NewRepository(dbConnection) repository := db.NewRepository(dbConnection)
taskcafeHandler := TaskcafeHandler{config, *repository} taskcafeHandler := TaskcafeHandler{*repository}
var imgServer = http.FileServer(http.Dir("./uploads/")) var imgServer = http.FileServer(http.Dir("./uploads/"))
r.Group(func(mux chi.Router) { 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.Use(AuthenticationMiddleware)
mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload) mux.Post("/users/me/avatar", taskcafeHandler.ProfileImageUpload)
mux.Post("/auth/install", taskcafeHandler.InstallHandler) 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"} frontend := FrontendHandler{staticPath: "build", indexPath: "index.html"}