master
Andrey Ivanov 2020-12-08 16:34:13 -05:00 committed by Andrey Ivanov
parent 3418f5087f
commit 8f6a3a13a2
8 changed files with 205 additions and 72 deletions

View File

@ -30,6 +30,5 @@ jobs:
install: go mod download
script:
- make lint
- make fast-test
- make slow-test
- make test
if: (type = push) AND (type = pull_request)

View File

@ -1,13 +1,9 @@
lint: install-lint-deps
golangci-lint run ./pkg/...
fast-test:
go test -race -count 100 -timeout 30s -short ./pkg/...
slow-test:
go test -race -timeout 150s -run Slow ./pkg/...
test:
go test -race -count 100 -timeout 30s ./pkg/...
install-lint-deps:
rm -rf $(shell go env GOPATH)/bin/golangci-lint
(which golangci-lint > /dev/null) || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(shell go env GOPATH)/bin
.PHONY: fast-test slow-test lint
.PHONY: lint test

3
go.mod
View File

@ -5,6 +5,9 @@ go 1.14
require (
github.com/BurntSushi/toml v0.3.1
github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec
github.com/daixiang0/gci v0.2.6 // indirect
github.com/go-sql-driver/mysql v1.5.0
github.com/lib/pq v1.9.0
github.com/pkg/errors v0.9.1 // indirect
github.com/stretchr/testify v1.6.1
gopkg.in/yaml.v2 v2.4.0 // indirect

32
go.sum
View File

@ -2,11 +2,17 @@ github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec h1:tDOPo9NAXCjvoK35HgZyzQSNLmb3chZqN2tnO273Bro=
github.com/amitrai48/logger v0.0.0-20190214092904-448001c055ec/go.mod h1:RZEHP3cxXvQlMuMjkpdh6qXA4b0CpjxnUBNxOpR0r30=
github.com/daixiang0/gci v0.2.6 h1:qMHUJVvI308H4MFNfHNWvoA+KvgpRdmcPnPYER3q+vM=
github.com/daixiang0/gci v0.2.6/go.mod h1:+4dZ7TISfSmqfAGv59ePaHfNzgGtIkHAhhdKggP1JAc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
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=
@ -18,6 +24,7 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
@ -26,8 +33,33 @@ go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394 h1:O3VD5Fds21mB1WVRTbkiz/HTXESx6Jql5ucPZi69oiM=
golang.org/x/tools v0.0.0-20201118003311-bd56c0adb394/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.0.0 h1:1Lc07Kr7qY4U2YPouBjpCLxpiyxIVoxqXgkXLknAOE8=

View File

@ -1,92 +1,123 @@
package config
import (
"database/sql"
"fmt"
"io/ioutil"
"os"
"reflect"
"strconv"
"strings"
"github.com/BurntSushi/toml"
// mysql driver.
_ "github.com/go-sql-driver/mysql"
// psql driver.
_ "github.com/lib/pq"
)
type Interface struct {
str interface{}
}
type Config struct {
ConfigFile string
EnvPrefix string
DSN string
EnvPrefix string
DSN string
}
/*
Логика конфига:
- В конструктор отдаем структуру - она заполняется исходя из логики и модификаторов конструктора
- Данные подтягиваются из файла
- Данные подтягиваются из переменных окружения
- Данные тянутся из базы
*/
func New(configFile string, str interface{}) error {
if configFile != "" {
f, err := os.Open(configFile)
if err != nil {
return fmt.Errorf("can't open config file: %w", err)
}
defer f.Close()
s, err := ioutil.ReadAll(f)
if err != nil {
return fmt.Errorf("can't read content of the config file : %w", err)
}
_, err = toml.Decode(string(s), str)
if err != nil {
return fmt.Errorf("can't parce config file : %w", err)
// Simple constructor.
func New(str interface{}) Interface {
return Interface{str: str}
}
// Method wraps discrete methods.
func (s Interface) Combine(c Config) error {
if c.ConfigFile != "" {
fmt.Printf("try to apply config from file %s...\n", c.ConfigFile)
if err := s.SetFromFile(c.ConfigFile); err != nil {
return fmt.Errorf("can't apply config from file: %w", err)
}
}
err := ApplyEnvVars(str, "APP")
if c.EnvPrefix != "" {
fmt.Printf("try to apply config from environment...\n")
if err := s.SetFromEnv(c.EnvPrefix); err != nil {
return fmt.Errorf("can't apply envvars to config :%w", err)
}
}
if c.DSN != "" {
fmt.Printf("try to apply config from DSN %s...\n", c.DSN)
if err := s.SetFromDB(c.DSN); err != nil {
return fmt.Errorf("can't apply db lines to config :%w", err)
}
}
return nil
}
// Method adds and replace config fields from file.
func (s Interface) SetFromFile(fileName string) error {
f, err := os.Open(fileName)
if err != nil {
return fmt.Errorf("can't apply envvars to config :%w", err)
return fmt.Errorf("can't open config file: %w", err)
}
defer f.Close()
l, err := ioutil.ReadAll(f)
if err != nil {
return fmt.Errorf("can't read content of the config file : %w", err)
}
_, err = toml.Decode(string(l), s.str)
if err != nil {
return fmt.Errorf("can't parce config file : %w", err)
}
return nil
return nil
}
func ApplyEnvVars(c interface{}, prefix string) error {
return applyEnvVar(reflect.ValueOf(c), reflect.TypeOf(c), -1, prefix)
// Method adds and replace config fields from env.
func (s Interface) SetFromEnv(prefix string) error {
return getEnvVar(reflect.ValueOf(s.str), reflect.TypeOf(s.str), -1, prefix)
}
func applyEnvVar(v reflect.Value, t reflect.Type, counter int, prefix string) error {
if v.Kind() != reflect.Ptr {
return fmt.Errorf("not a pointer value")
// Method adds and replace config fields from db.
func (s Interface) SetFromDB(dsn string) error {
m := strings.FieldsFunc(dsn, func(r rune) bool { return r == ':' || r == '@' || r == '/' })
dbName := m[len(m)-1]
if dbName == "" {
return fmt.Errorf("DSN not contains database name: %s", dsn)
}
f := reflect.StructField{}
if counter != -1 {
f = t.Field(counter)
var driver string
switch {
case strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") || strings.HasPrefix(dsn, "psql://"):
driver = "postgresql"
case strings.HasPrefix(dsn, "mysql://"):
driver = "mysql"
default:
driver = "postgresql"
}
v = reflect.Indirect(v)
fName := strings.ToUpper(f.Name)
env := os.Getenv(prefix + fName)
if env != "" {
switch v.Kind() {
case reflect.Int:
envI, err := strconv.Atoi(env)
if err != nil {
return fmt.Errorf("could not parse to int: %w", err)
}
v.SetInt(int64(envI))
case reflect.String:
v.SetString(env)
case reflect.Bool:
envB, err := strconv.ParseBool(env)
if err != nil {
return fmt.Errorf("could not parse bool: %w", err)
}
v.SetBool(envB)
db, err := sql.Open(driver, dsn)
if err != nil {
return fmt.Errorf("can't connect to DB: %w", err)
}
defer db.Close()
res := make(map[string]string)
var key, val string
results, err := db.Query(`SELECT key, value FROM $1`, dbName)
if err != nil || results.Err() != nil {
return fmt.Errorf("can't get key-value pairs from DB: %w", err)
}
defer results.Close()
for results.Next() {
err = results.Scan(&key, &val)
if err != nil {
return fmt.Errorf("can't parse key-value into vars: %w", err)
}
res[key] = val
}
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
if err := applyEnvVar(v.Field(i).Addr(), v.Type(), i, prefix+fName+"_"); err != nil {
return fmt.Errorf("could not apply env var: %w", err)
}
}
if err = parseToStruct(reflect.ValueOf(s.str), reflect.TypeOf(s.str), -1, "", res); err != nil {
return fmt.Errorf("can't parse into struct: %w", err)
}
return nil
}

View File

@ -8,7 +8,6 @@ import (
"testing"
)
func TestNewConfig(t *testing.T) {
badfile, err := ioutil.TempFile("", "conf.")

73
pkg/config/internal.go Normal file
View File

@ -0,0 +1,73 @@
package config
import (
"fmt"
"os"
"reflect"
"strconv"
"strings"
)
func getEnvVar(v reflect.Value, t reflect.Type, counter int, prefix string) error {
if v.Kind() != reflect.Ptr {
return fmt.Errorf("not a pointer value")
}
f := reflect.StructField{}
if counter != -1 {
f = t.Field(counter)
}
v = reflect.Indirect(v)
fName := strings.ToUpper(f.Name)
env := os.Getenv(prefix + fName)
if env != "" {
switch v.Kind() {
case reflect.Int:
envI, err := strconv.Atoi(env)
if err != nil {
return fmt.Errorf("could not parse to int: %w", err)
}
v.SetInt(int64(envI))
case reflect.String:
v.SetString(env)
case reflect.Bool:
envB, err := strconv.ParseBool(env)
if err != nil {
return fmt.Errorf("could not parse bool: %w", err)
}
v.SetBool(envB)
case reflect.Array, reflect.Chan, reflect.Complex128, reflect.Complex64, reflect.Float32, reflect.Float64, reflect.Func, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Int8, reflect.Interface, reflect.Invalid, reflect.Map, reflect.Ptr, reflect.Slice, reflect.Struct, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uint8, reflect.Uintptr, reflect.UnsafePointer:
}
}
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
if err := getEnvVar(v.Field(i).Addr(), v.Type(), i, prefix+fName+"_"); err != nil {
return fmt.Errorf("could not apply env var: %w", err)
}
}
}
return nil
}
func parseToStruct(v reflect.Value, t reflect.Type, counter int, prefix string, kv map[string]string) error {
if v.Kind() != reflect.Ptr {
return fmt.Errorf("not a pointer value")
}
f := reflect.StructField{}
if counter != -1 {
f = t.Field(counter)
}
v = reflect.Indirect(v)
fName := strings.ToUpper(f.Name)
env := kv[prefix+fName]
if env != "" {
v.SetString(env)
}
if v.Kind() == reflect.Struct {
for i := 0; i < v.NumField(); i++ {
if err := parseToStruct(v.Field(i).Addr(), v.Type(), i, prefix+fName+".", kv); err != nil {
return fmt.Errorf("could not apply env var: %w", err)
}
}
}
return nil
}

View File

@ -9,7 +9,7 @@ import (
amitralog "github.com/amitrai48/logger"
)
type LoggerInterface interface {
type Interface interface {
Debugf(format string, args ...interface{})
Infof(format string, args ...interface{})
Warnf(format string, args ...interface{})
@ -29,7 +29,7 @@ type Config struct {
var validLevel = map[string]bool{"debug": true, "info": true, "warn": true, "error": true, "fatal": true}
func New(conf Config) (LoggerInterface, error) {
func New(conf Config) (Interface, error) {
if conf.File == "" || !validLevel[strings.ToLower(conf.Level)] {
return nil, errors.New("invalid logger config")
}