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 (
"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())

View File

@ -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
}

View File

@ -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
}

View File

@ -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,
},
}

View File

@ -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
}

View File

@ -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"}