Skip to content

Commit 4ec95d9

Browse files
authored
make the config direct from yaml and json (#206)
* make the config direct from yaml and json refactor to keep a deprectated LoadFromFile fix imports * fix tests * use the args loader * make test for parsing the default configuration * misleading error * use the Duration in the test * default more values * use the util.Duration for timeout values * import cleanups
1 parent 8ff9829 commit 4ec95d9

15 files changed

+416
-69
lines changed

featureflag/config.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,16 @@
11
package featureflag
22

33
import (
4-
"time"
5-
4+
"github.com/netlify/netlify-commons/util"
65
ld "gopkg.in/launchdarkly/go-server-sdk.v4"
76
)
87

98
type Config struct {
109
Key string `json:"key" yaml:"key"`
11-
RequestTimeout time.Duration `json:"request_timeout" yaml:"request_timeout" mapstructure:"request_timeout" split_words:"true" default:"5s"`
10+
RequestTimeout util.Duration `json:"request_timeout" yaml:"request_timeout" mapstructure:"request_timeout" split_words:"true" default:"5s"`
1211
Enabled bool `json:"enabled" yaml:"enabled" default:"false"`
1312

14-
updateProcessorFactory ld.UpdateProcessorFactory `json:"-"`
13+
updateProcessorFactory ld.UpdateProcessorFactory
1514

1615
// Drop telemetry events (not needed in local-dev/CI environments)
1716
DisableEvents bool `json:"disable_events" yaml:"disable_events" mapstructure:"disable_events" split_words:"true"`

featureflag/featureflag.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ func NewClient(cfg *Config, logger logrus.FieldLogger) (Client, error) {
5050
config.SendEvents = false
5151
}
5252

53-
inner, err := ld.MakeCustomClient(cfg.Key, config, cfg.RequestTimeout)
53+
inner, err := ld.MakeCustomClient(cfg.Key, config, cfg.RequestTimeout.Duration)
5454
if err != nil {
5555
logger.WithError(err).Error("Unable to construct LD client")
5656
}

featureflag/featureflag_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/netlify/netlify-commons/util"
89
"github.com/sirupsen/logrus"
910
"github.com/stretchr/testify/assert"
1011
"github.com/stretchr/testify/require"
@@ -14,7 +15,7 @@ import (
1415
func TestOfflineClient(t *testing.T) {
1516
cfg := Config{
1617
Key: "ABCD",
17-
RequestTimeout: time.Second,
18+
RequestTimeout: util.Duration{time.Second},
1819
Enabled: false,
1920
}
2021
client, err := NewClient(&cfg, nil)
@@ -48,7 +49,7 @@ func TestAllEnabledFlags(t *testing.T) {
4849
fileSource := ldfiledata.NewFileDataSourceFactory(ldfiledata.FilePaths("./fixtures/flags.yml"))
4950
cfg := Config{
5051
Key: "ABCD",
51-
RequestTimeout: time.Second,
52+
RequestTimeout: util.Duration{time.Second},
5253
Enabled: true,
5354
updateProcessorFactory: fileSource,
5455
}
@@ -63,7 +64,7 @@ func TestAllEnabledFlags(t *testing.T) {
6364
func TestLogging(t *testing.T) {
6465
cfg := Config{
6566
Key: "ABCD",
66-
RequestTimeout: time.Second,
67+
RequestTimeout: util.Duration{time.Second},
6768
Enabled: false,
6869
}
6970

nconf/args.go

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,26 +19,9 @@ type RootArgs struct {
1919
}
2020

2121
func (args *RootArgs) Setup(config interface{}, serviceName, version string) (logrus.FieldLogger, error) {
22-
// first load the logger and BugSnag config
23-
rootConfig := &struct {
24-
Log *LoggingConfig
25-
BugSnag *BugSnagConfig
26-
Metrics metriks.Config
27-
Tracing tracing.Config
28-
FeatureFlag featureflag.Config
29-
}{}
30-
31-
loader := func(cfg interface{}) error {
32-
return LoadFromEnv(args.Prefix, args.ConfigFile, cfg)
33-
}
34-
if !strings.HasSuffix(args.ConfigFile, ".env") {
35-
loader = func(cfg interface{}) error {
36-
return LoadFromFile(args.ConfigFile, cfg)
37-
}
38-
}
39-
40-
if err := loader(rootConfig); err != nil {
41-
return nil, errors.Wrap(err, "Failed to load the logging configuration")
22+
rootConfig, err := args.loadDefaultConfig()
23+
if err != nil {
24+
return nil, err
4225
}
4326

4427
log, err := ConfigureLogging(rootConfig.Log)
@@ -71,14 +54,26 @@ func (args *RootArgs) Setup(config interface{}, serviceName, version string) (lo
7154

7255
if config != nil {
7356
// second load the config for this project
74-
if err := loader(config); err != nil {
57+
if err := args.load(config); err != nil {
7558
return log, errors.Wrap(err, "Failed to load the config object")
7659
}
7760
log.Debug("Loaded configuration")
7861
}
7962
return log, nil
8063
}
8164

65+
func (args *RootArgs) load(cfg interface{}) error {
66+
loader := func(cfg interface{}) error {
67+
return LoadFromEnv(args.Prefix, args.ConfigFile, cfg)
68+
}
69+
if !strings.HasSuffix(args.ConfigFile, ".env") {
70+
loader = func(cfg interface{}) error {
71+
return LoadConfigFromFile(args.ConfigFile, cfg)
72+
}
73+
}
74+
return loader(cfg)
75+
}
76+
8277
func (args *RootArgs) MustSetup(config interface{}, serviceName, version string) logrus.FieldLogger {
8378
logger, err := args.Setup(config, serviceName, version)
8479
if err != nil {
@@ -92,6 +87,16 @@ func (args *RootArgs) MustSetup(config interface{}, serviceName, version string)
9287
return logger
9388
}
9489

90+
func (args *RootArgs) loadDefaultConfig() (*RootConfig, error) {
91+
c := DefaultConfig()
92+
93+
if err := args.load(&c); err != nil {
94+
return nil, errors.Wrap(err, "Failed to load the default configuration")
95+
}
96+
97+
return &c, nil
98+
}
99+
95100
func (args *RootArgs) AddFlags(cmd *cobra.Command) *cobra.Command {
96101
cmd.Flags().AddFlag(args.ConfigFlag())
97102
cmd.Flags().AddFlag(args.PrefixFlag())

nconf/args_test.go

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
package nconf
22

33
import (
4+
"encoding/json"
45
"io/ioutil"
6+
"os"
57
"testing"
6-
7-
"github.com/spf13/cobra"
8+
"time"
89

910
"github.com/sirupsen/logrus"
11+
"github.com/spf13/cobra"
1012
"github.com/stretchr/testify/assert"
11-
1213
"github.com/stretchr/testify/require"
14+
"gopkg.in/yaml.v3"
1315
)
1416

1517
func TestArgsLoad(t *testing.T) {
@@ -67,3 +69,98 @@ func TestArgsAddToCmd(t *testing.T) {
6769
require.NoError(t, cmd.Execute())
6870
assert.Equal(t, 1, called)
6971
}
72+
73+
func TestArgsLoadDefault(t *testing.T) {
74+
configVals := map[string]interface{}{
75+
"log": map[string]interface{}{
76+
"level": "debug",
77+
"fields": map[string]interface{}{
78+
"something": 1,
79+
},
80+
},
81+
"bugsnag": map[string]interface{}{
82+
"api_key": "secrets",
83+
"project_package": "package",
84+
},
85+
"metrics": map[string]interface{}{
86+
"enabled": true,
87+
"port": 8125,
88+
"tags": map[string]string{
89+
"env": "prod",
90+
},
91+
},
92+
"tracing": map[string]interface{}{
93+
"enabled": true,
94+
"port": "9125",
95+
"enable_debug": true,
96+
},
97+
"featureflag": map[string]interface{}{
98+
"key": "magicalkey",
99+
"request_timeout": "10s",
100+
"enabled": true,
101+
},
102+
}
103+
104+
scenes := []struct {
105+
ext string
106+
enc func(v interface{}) ([]byte, error)
107+
}{
108+
{"json", json.Marshal},
109+
{"yaml", yaml.Marshal},
110+
}
111+
for _, s := range scenes {
112+
t.Run(s.ext, func(t *testing.T) {
113+
f, err := ioutil.TempFile("", "test-config-*."+s.ext)
114+
require.NoError(t, err)
115+
defer os.Remove(f.Name())
116+
117+
b, err := s.enc(&configVals)
118+
require.NoError(t, err)
119+
_, err = f.Write(b)
120+
require.NoError(t, err)
121+
122+
args := RootArgs{
123+
ConfigFile: f.Name(),
124+
}
125+
cfg, err := args.loadDefaultConfig()
126+
require.NoError(t, err)
127+
128+
// logging
129+
assert.Equal(t, "debug", cfg.Log.Level)
130+
assert.Equal(t, true, cfg.Log.QuoteEmptyFields)
131+
assert.Equal(t, "", cfg.Log.File)
132+
assert.Equal(t, false, cfg.Log.DisableColors)
133+
assert.Equal(t, "", cfg.Log.TSFormat)
134+
135+
assert.Len(t, cfg.Log.Fields, 1)
136+
assert.EqualValues(t, 1, cfg.Log.Fields["something"])
137+
assert.Equal(t, false, cfg.Log.UseNewLogger)
138+
139+
// bugsnag
140+
assert.Equal(t, "", cfg.BugSnag.Environment)
141+
assert.Equal(t, "secrets", cfg.BugSnag.APIKey)
142+
assert.Equal(t, false, cfg.BugSnag.LogHook)
143+
assert.Equal(t, "package", cfg.BugSnag.ProjectPackage)
144+
145+
// metrics
146+
assert.Equal(t, true, cfg.Metrics.Enabled)
147+
assert.Equal(t, "localhost", cfg.Metrics.Host)
148+
assert.Equal(t, 8125, cfg.Metrics.Port)
149+
assert.Equal(t, map[string]string{"env": "prod"}, cfg.Metrics.Tags)
150+
151+
// tracing
152+
assert.Equal(t, true, cfg.Tracing.Enabled)
153+
assert.Equal(t, "localhost", cfg.Tracing.Host)
154+
assert.Equal(t, "9125", cfg.Tracing.Port)
155+
assert.Empty(t, cfg.Tracing.Tags)
156+
assert.Equal(t, true, cfg.Tracing.EnableDebug)
157+
158+
// featureflag
159+
assert.Equal(t, "magicalkey", cfg.FeatureFlag.Key)
160+
assert.Equal(t, 10*time.Second, cfg.FeatureFlag.RequestTimeout.Duration)
161+
assert.Equal(t, true, cfg.FeatureFlag.Enabled)
162+
assert.Equal(t, false, cfg.FeatureFlag.DisableEvents)
163+
assert.Equal(t, "", cfg.FeatureFlag.RelayHost)
164+
})
165+
}
166+
}

nconf/bugsnag.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,28 @@ import (
88

99
type BugSnagConfig struct {
1010
Environment string
11-
APIKey string `envconfig:"api_key"`
12-
LogHook bool `envconfig:"log_hook"`
13-
ProjectPackage string `envconfig:"project_package"`
11+
APIKey string `envconfig:"api_key" json:"api_key" yaml:"api_key"`
12+
LogHook bool `envconfig:"log_hook" json:"log_hook" yaml:"log_hook"`
13+
ProjectPackage string `envconfig:"project_package" json:"project_package" yaml:"project_package"`
1414
}
1515

1616
func SetupBugSnag(config *BugSnagConfig, version string) error {
1717
if config == nil || config.APIKey == "" {
1818
return nil
1919
}
20-
20+
2121
projectPackages := make([]string, 0, 2)
2222
projectPackages = append(projectPackages, "main")
2323
if config.ProjectPackage != "" {
2424
projectPackages = append(projectPackages, config.ProjectPackage)
2525
}
2626

2727
bugsnag.Configure(bugsnag.Configuration{
28-
APIKey: config.APIKey,
29-
ReleaseStage: config.Environment,
30-
AppVersion: version,
31-
ProjectPackages: projectPackages,
32-
PanicHandler: func() {}, // this is to disable panic handling. The lib was forking and restarting the process (causing races)
28+
APIKey: config.APIKey,
29+
ReleaseStage: config.Environment,
30+
AppVersion: version,
31+
ProjectPackages: projectPackages,
32+
PanicHandler: func() {}, // this is to disable panic handling. The lib was forking and restarting the process (causing races)
3333
})
3434

3535
if config.LogHook {

nconf/configuration.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,60 @@
11
package nconf
22

33
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
47
"os"
8+
"path/filepath"
59
"strings"
610

711
"github.com/joho/godotenv"
812
"github.com/kelseyhightower/envconfig"
13+
"github.com/netlify/netlify-commons/featureflag"
14+
"github.com/netlify/netlify-commons/metriks"
15+
"github.com/netlify/netlify-commons/tracing"
916
"github.com/pkg/errors"
1017
"github.com/spf13/viper"
18+
"gopkg.in/yaml.v3"
1119
)
1220

21+
// ErrUnknownConfigFormat indicates the extension of the config file is not supported as a config source
22+
type ErrUnknownConfigFormat struct {
23+
ext string
24+
}
25+
26+
func (e *ErrUnknownConfigFormat) Error() string {
27+
return fmt.Sprintf("Unknown config format: %s", e.ext)
28+
}
29+
30+
type RootConfig struct {
31+
Log LoggingConfig
32+
BugSnag *BugSnagConfig
33+
Metrics metriks.Config
34+
Tracing tracing.Config
35+
FeatureFlag featureflag.Config
36+
}
37+
38+
func DefaultConfig() RootConfig {
39+
return RootConfig{
40+
Log: LoggingConfig{
41+
QuoteEmptyFields: true,
42+
},
43+
Tracing: tracing.Config{
44+
Host: "localhost",
45+
Port: "8126",
46+
},
47+
Metrics: metriks.Config{
48+
Host: "localhost",
49+
Port: 8125,
50+
},
51+
}
52+
}
53+
54+
/*
55+
Deprecated: This method relies on parsing the json/yaml to a map, then running it through mapstructure.
56+
This required that both tags exist (annoying!). And so there is now LoadConfigFromFile.
57+
*/
1358
// LoadFromFile will load the configuration from the specified file based on the file type
1459
// There is only support for .json and .yml now
1560
func LoadFromFile(configFile string, input interface{}) error {
@@ -37,6 +82,30 @@ func LoadFromFile(configFile string, input interface{}) error {
3782
return viper.Unmarshal(input)
3883
}
3984

85+
// LoadConfigFromFile will load the configuration from the specified file based on the file type
86+
// There is only support for .json and .yml now. It will use the underlying json/yaml packages directly.
87+
// meaning those should be the only required tags.
88+
func LoadConfigFromFile(configFile string, input interface{}) error {
89+
if configFile == "" {
90+
return nil
91+
}
92+
93+
// read in all the bytes
94+
data, err := ioutil.ReadFile(configFile)
95+
if err != nil {
96+
return err
97+
}
98+
99+
configExt := filepath.Ext(configFile)
100+
switch configExt {
101+
case ".json":
102+
return json.Unmarshal(data, input)
103+
case ".yaml", ".yml":
104+
return yaml.Unmarshal(data, input)
105+
}
106+
return &ErrUnknownConfigFormat{configExt}
107+
}
108+
40109
func LoadFromEnv(prefix, filename string, face interface{}) error {
41110
var err error
42111
if filename == "" {

0 commit comments

Comments
 (0)