From 8c0d983297733ded978dae54b2d78c27ed74476b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Krop=C3=A1=C4=8Dek?= Date: Sat, 23 Nov 2024 18:02:41 +0100 Subject: [PATCH] configuration baby --- README.md | 2 +- cmd/secret-santa/main.go | 45 ++++++++++++--- go.mod | 14 +++++ go.sum | 14 +++++ internal/config/app_config.go | 29 ++++++++++ internal/config/config.go | 39 +++++-------- internal/config/db_config.go | 99 +++++++++++++++++++++++++++++++ internal/config/parse.go | 106 ++++++++++++++++++++++++++++++++++ 8 files changed, 315 insertions(+), 33 deletions(-) create mode 100644 internal/config/app_config.go create mode 100644 internal/config/db_config.go create mode 100644 internal/config/parse.go diff --git a/README.md b/README.md index e291bb7..27b41c9 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Why? Because I can. And because I wanted to learn how to write a web application - [x] Web server is up and running - [x] Basic API routes - [x] Database migrations (only for sqlite) - - [ ] Configurable database connection + - [x] Configurable database connection - [ ] Automatic database migrations (will be probably only supported for Docker deployments or with a guide) - [ ] Tests - [ ] Frontend using templates diff --git a/cmd/secret-santa/main.go b/cmd/secret-santa/main.go index 3689b7f..5bb99ea 100644 --- a/cmd/secret-santa/main.go +++ b/cmd/secret-santa/main.go @@ -2,23 +2,50 @@ package main import ( "database/sql" + "flag" "git.katuwoss.dev/JustScreaMy/secret-santa/internal/app" "git.katuwoss.dev/JustScreaMy/secret-santa/internal/config" + _ "github.com/go-sql-driver/mysql" + _ "github.com/jackc/pgx/v5/stdlib" _ "github.com/mattn/go-sqlite3" "log" + "os" ) -func main() { - dbConnection, err := sql.Open("sqlite3", "./test.db") - if err != nil { - log.Fatal(err) +var ( + configPath string +) + +func init() { + flag.StringVar(&configPath, "config", "config.yaml", "path to config file") + flag.Parse() + + // Check if the config file exists + if _, err := os.Stat(configPath); os.IsNotExist(err) { + log.Fatalf("Config file not found: %s", configPath) } + log.Printf("Using config file: %s", configPath) +} + +func main() { + appConfig, err := config.ParseConfig(configPath) + if err != nil { + log.Fatalf("Failed to parse config: %s", err.Error()) + } + + dbConnection, err := sql.Open(appConfig.DB.GetDriver(), appConfig.DB.GenerateDSN()) + + if err != nil { + log.Fatalf("Database connection failed: %s", err.Error()) + } + + if dbConnection.Ping() != nil { + log.Fatalf("Database connection failed: unable to ping database") + } + log.Println("Database sucessfully connected") + application := app.NewApp(dbConnection, &appConfig.App, nil) - appConfig := config.NewAppConfig(nil, nil) - application := app.NewApp(dbConnection, &appConfig, nil) - - log.Printf("Listening on http://%s", appConfig.GenerateIP()) - + log.Printf("Listening on http://%s", appConfig.App.GenerateIP()) log.Fatal(application.Start()) } diff --git a/go.mod b/go.mod index 5ca837e..2077e83 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,8 @@ module git.katuwoss.dev/JustScreaMy/secret-santa go 1.23.2 require ( + github.com/go-sql-driver/mysql v1.6.0 + github.com/jackc/pgx/v5 v5.5.4 github.com/mattn/go-sqlite3 v1.14.24 github.com/rubenv/sql-migrate v1.7.0 github.com/stretchr/testify v1.9.0 @@ -25,13 +27,21 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/klauspost/compress v1.17.4 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -42,6 +52,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect @@ -56,6 +67,9 @@ require ( go.opentelemetry.io/otel/metric v1.24.0 // indirect go.opentelemetry.io/otel/trace v1.24.0 // indirect golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index fc8dce1..998cb9c 100644 --- a/go.sum +++ b/go.sum @@ -31,6 +31,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -40,6 +42,12 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.23.0 h1:/PwmTwZhS0dPkav3cdK9kV1FsAmrL8sThn8IHr/sO+o= +github.com/go-playground/validator/v10 v10.23.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -68,6 +76,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= @@ -92,6 +102,9 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -117,6 +130,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= diff --git a/internal/config/app_config.go b/internal/config/app_config.go new file mode 100644 index 0000000..bc4d4e9 --- /dev/null +++ b/internal/config/app_config.go @@ -0,0 +1,29 @@ +package config + +import "fmt" + +type AppConfig struct { + ListenIP string `json:"listen_ip" yaml:"listen_ip" toml:"listen_ip" validate:"required,ip"` + ListenPort int `json:"listen_port" yaml:"listen_port" toml:"listen_port" validate:"required"` +} + +func (c *AppConfig) GenerateIP() string { + return fmt.Sprintf("%s:%d", c.ListenIP, c.ListenPort) +} + +func NewAppConfig(port *int, ip *string) AppConfig { + defaultPort := 8080 + defaultIp := "127.0.0.1" + + if port == nil { + port = &defaultPort + } + if ip == nil { + ip = &defaultIp + } + + return AppConfig{ + ListenIP: *ip, + ListenPort: *port, + } +} diff --git a/internal/config/config.go b/internal/config/config.go index d0a5efb..b706976 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,29 +1,22 @@ package config -import "fmt" - -type AppConfig struct { - ListenIP string `json:"listen_ip"` - ListenPort int `json:"listen_port"` +type Config struct { + App AppConfig `json:"app" yaml:"app" toml:"app" validate:"required"` + DB DatabaseConfig `json:"database" yaml:"database" toml:"database" validate:"required"` } -func (c *AppConfig) GenerateIP() string { - return fmt.Sprintf("%s:%d", c.ListenIP, c.ListenPort) -} - -func NewAppConfig(port *int, ip *string) AppConfig { - defaultPort := 8080 - defaultIp := "127.0.0.1" - - if port == nil { - port = &defaultPort - } - if ip == nil { - ip = &defaultIp - } - - return AppConfig{ - ListenIP: *ip, - ListenPort: *port, +// NewConfig will create a new Config struct with the given AppConfig and DatabaseConfig. +func NewConfig(app AppConfig, db DatabaseConfig) Config { + return Config{ + App: app, + DB: db, + } +} + +// NewDefaultConfig will create a new Config struct with default values. +func NewDefaultConfig() Config { + return Config{ + App: NewAppConfig(nil, nil), + DB: NewDatabaseConfig(nil, nil, nil, nil, nil, nil), } } diff --git a/internal/config/db_config.go b/internal/config/db_config.go new file mode 100644 index 0000000..3475b46 --- /dev/null +++ b/internal/config/db_config.go @@ -0,0 +1,99 @@ +package config + +import "fmt" + +type DriverType string + +const ( + MySQL DriverType = "mysql" + Postgres DriverType = "postgres" + SQLite DriverType = "sqlite" +) + +// DatabaseConfig will hold the database configuration. +// +// If the driver is SQLite, the Host field should be the path to the database file. +// Other configurations options will be ignored. +type DatabaseConfig struct { + Driver DriverType `json:"driver" yaml:"driver" toml:"driver" validate:"required"` + Host string `json:"host" yaml:"host" toml:"host" validate:"required"` + Port int `json:"port" yaml:"port" toml:"port"` + User string `json:"user" yaml:"user" toml:"user"` + Pass string `json:"pass" yaml:"pass" toml:"pass"` + DBName string `json:"db_name" yaml:"db_name" toml:"db_name"` +} + +func NewDatabaseConfig(driver *DriverType, host, user, pass, dbName *string, port *int) DatabaseConfig { + defaultDriver := SQLite + defaultHost := "./database.db" + defaultPort := 0 + defaultUser := "" + defaultPass := "" + defaultDBName := "" + + if driver == nil { + driver = &defaultDriver + } + if host == nil { + host = &defaultHost + } + if port == nil { + port = &defaultPort + } + if user == nil { + user = &defaultUser + } + if pass == nil { + pass = &defaultPass + } + if dbName == nil { + dbName = &defaultDBName + } + + return DatabaseConfig{ + Driver: *driver, + Host: *host, + Port: *port, + User: *user, + Pass: *pass, + DBName: *dbName, + } +} + +// GenerateDSN generates the Data Source Name (DSN) for the database connection. +func (c *DatabaseConfig) GenerateDSN() string { + switch c.Driver { + case MySQL: + return c.generateMySQLDSN() + case Postgres: + return c.generatePostgresDSN() + case SQLite: + return c.generateSQLiteDSN() + default: + return "" + } +} + +func (c *DatabaseConfig) GetDriver() string { + switch c.Driver { + case MySQL: + return "mysql" + case Postgres: + return "pgx" + case SQLite: + return "sqlite3" + default: + return "" + } + return string(c.Driver) +} + +func (c *DatabaseConfig) generateSQLiteDSN() string { + return c.Host +} +func (c *DatabaseConfig) generateMySQLDSN() string { + return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", c.User, c.Pass, c.Host, c.Port, c.DBName) +} +func (c *DatabaseConfig) generatePostgresDSN() string { + return fmt.Sprintf("postgres://%s:%s@%s:%d/%s", c.User, c.Pass, c.Host, c.Port, c.DBName) +} diff --git a/internal/config/parse.go b/internal/config/parse.go new file mode 100644 index 0000000..40262af --- /dev/null +++ b/internal/config/parse.go @@ -0,0 +1,106 @@ +package config + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" + "io" + "os" + "path/filepath" + + "github.com/pelletier/go-toml/v2" + "gopkg.in/yaml.v3" +) + +type FileType string + +const ( + YAML FileType = "yaml" + TOML FileType = "toml" + JSON FileType = "json" +) + +// getFileType returns the file type based on the file extension. +func getFileType(path string) (FileType, error) { + // Get the file extension + ext := filepath.Ext(path) + + switch ext { + case ".yaml", ".yml": + return YAML, nil + case ".toml": + return TOML, nil + case ".json": + return JSON, nil + default: + return "", fmt.Errorf("unsupported file type: %s", ext) + } +} + +func ParseConfig(path string) (Config, error) { + fileType, err := getFileType(path) + if err != nil { + return Config{}, err + } + + file, err := os.Open(path) + if err != nil { + return Config{}, err + } + defer file.Close() + + data, err := io.ReadAll(file) + + var config Config + + switch fileType { + case YAML: + config, err = parseYAML(data) + case TOML: + config, err = parseTOML(data) + case JSON: + config, err = parseJSON(data) + default: + return Config{}, fmt.Errorf("unsupported file type: %s", fileType) + } + + if err != nil { + return Config{}, err + } + + validate := validator.New(validator.WithRequiredStructEnabled()) + err = validate.Struct(config) + + if err != nil { + return Config{}, err + } + + return config, nil +} + +func parseTOML(data []byte) (Config, error) { + var appConfig Config + err := toml.Unmarshal(data, &appConfig) + if err != nil { + return Config{}, err + } + return appConfig, nil +} + +func parseYAML(data []byte) (Config, error) { + var appConfig Config + err := yaml.Unmarshal(data, &appConfig) + if err != nil { + return Config{}, err + } + return appConfig, nil +} + +func parseJSON(data []byte) (Config, error) { + var appConfig Config + err := json.Unmarshal(data, &appConfig) + if err != nil { + return Config{}, err + } + return appConfig, nil +}