hackerman 2019-07-15 12:52:53 +02:00 committed by GitHub
parent e02bc9eca5
commit 5bace2abf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 198 additions and 18 deletions

11
.circleci/config.yml Normal file
View file

@ -0,0 +1,11 @@
version: 2
jobs:
build:
docker:
- image: circleci/golang:1.12
environment:
- GO111MODULE=on
working_directory: /go/src/github.com/ory/viper
steps:
- checkout
- run: go test -race -v ./...

View file

@ -1,4 +1,4 @@
go_import_path: github.com/spf13/viper
go_import_path: github.com/ory/viper
language: go

6
Makefile Normal file
View file

@ -0,0 +1,6 @@
SHELL=/bin/bash -o pipefail
# Formats the code
.PHONY: format
format:
goreturns -w -local github.com/ory $$(listx .)

View file

@ -1,7 +1,17 @@
![viper logo](https://cloud.githubusercontent.com/assets/173412/10886745/998df88a-8151-11e5-9448-4736db51020d.png)
[![CircleCI](https://circleci.com/gh/ory/viper/tree/master.svg?style=shield](https://circleci.com/gh/ory/viper/tree/master)
Go configuration with fangs!
> This is a fork. It resolves several issues that are left unresolved in [the upstream](https://github.com/ory/viper).
> Issues resolved and features added include:
>
> - Fixed race conditions when reloading configs.
> - Added `HasChanged(key string) bool` which returns true (once!) when a value has changed.
> - Make sure that `viper.AllSettings()` always returns `map[string]interface{}` which was not the case and incompatible
with de/encoders like `json`.
Many Go projects are built using Viper including:
* [Hugo](http://gohugo.io)
@ -13,11 +23,11 @@ Many Go projects are built using Viper including:
* [doctl](https://github.com/digitalocean/doctl)
* [Clairctl](https://github.com/jgsqware/clairctl)
[![Build Status](https://travis-ci.org/spf13/viper.svg)](https://travis-ci.org/spf13/viper) [![Join the chat at https://gitter.im/spf13/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spf13/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/spf13/viper?status.svg)](https://godoc.org/github.com/spf13/viper)
[![Build Status](https://travis-ci.org/ory/viper.svg)](https://travis-ci.org/ory/viper) [![Join the chat at https://gitter.im/ory/viper](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/ory/viper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![GoDoc](https://godoc.org/github.com/ory/viper?status.svg)](https://godoc.org/github.com/ory/viper)
## Install
```console
go get -u github.com/spf13/viper
go get -u github.com/ory/viper
```
## What is Viper?
@ -384,7 +394,7 @@ viper.BindFlagValues("my-flags", fSet)
To enable remote support in Viper, do a blank import of the `viper/remote`
package:
`import _ "github.com/spf13/viper/remote"`
`import _ "github.com/ory/viper/remote"`
Viper will read a config string (as JSON, TOML, YAML, HCL or envfile) retrieved from a path
in a Key/Value store such as etcd or Consul. These values take precedence over

3
go.mod
View file

@ -1,4 +1,4 @@
module github.com/spf13/viper
module github.com/ory/viper
require (
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6 // indirect
@ -28,6 +28,7 @@ require (
github.com/spf13/jwalterweatherman v1.0.0
github.com/spf13/pflag v1.0.3
github.com/stretchr/testify v1.2.2
github.com/subosito/gotenv v1.1.1
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 // indirect
github.com/ugorji/go v1.1.4 // indirect
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect

4
go.sum
View file

@ -71,8 +71,6 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
@ -117,6 +115,8 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/subosito/gotenv v1.1.1 h1:TWxckSF6WVKWbo2M3tMqCtWa9NFUgqM1SSynxmYONOI=
github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5 h1:LnC5Kc/wtumK+WB441p7ynQJzVuNRJiqddSIE3IlSEQ=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw=

View file

@ -11,8 +11,9 @@ import (
"io"
"os"
"github.com/spf13/viper"
crypt "github.com/xordataexchange/crypt/config"
"github.com/ory/viper"
)
type remoteConfigProvider struct{}

12
stub/config.json Normal file
View file

@ -0,0 +1,12 @@
{
"foo": {
"bar": [
{
"baz": 1
},
{
"baz": 2
}
]
}
}

4
stub/config.yaml Normal file
View file

@ -0,0 +1,4 @@
foo:
bar:
- baz: 1
- baz: 2

25
util.go
View file

@ -219,3 +219,28 @@ func deepSearch(m map[string]interface{}, path []string) map[string]interface{}
}
return m
}
// toMapStringInterface is a workaround for https://github.com/ory/viper/issues/730
// and https://github.com/go-yaml/yaml/issues/139
func toMapStringInterface(in interface{}) interface{} {
switch t := in.(type) {
case map[string]interface{}:
for k, v := range t {
t[k] = toMapStringInterface(v)
}
return t
case map[interface{}]interface{}:
nt := make(map[string]interface{})
for k, v := range t {
nt[fmt.Sprintf("%s", k)] = toMapStringInterface(v)
}
return nt
case []interface{}:
for k, v := range t {
t[k] = toMapStringInterface(v)
}
return t
default:
return in
}
}

View file

@ -13,6 +13,8 @@ package viper
import (
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func TestCopyAndInsensitiviseMap(t *testing.T) {
@ -52,3 +54,21 @@ func TestCopyAndInsensitiviseMap(t *testing.T) {
t.Fatal("Input map changed")
}
}
func TestToMapStringInterface(t *testing.T) {
assert.EqualValues(
t,
map[string]interface{}{
"foo": "bar",
"items": map[string]interface{}{
"foo": "bar",
},
},
toMapStringInterface(map[string]interface{}{
"foo": "bar",
"items": map[interface{}]interface{}{
"foo": "bar",
},
}),
)
}

View file

@ -33,14 +33,14 @@ import (
"sync"
"time"
yaml "gopkg.in/yaml.v2"
"gopkg.in/yaml.v2"
"github.com/fsnotify/fsnotify"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/printer"
"github.com/magiconair/properties"
"github.com/mitchellh/mapstructure"
toml "github.com/pelletier/go-toml"
"github.com/pelletier/go-toml"
"github.com/spf13/afero"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
@ -205,6 +205,8 @@ type Viper struct {
properties *properties.Properties
onConfigChange func(fsnotify.Event)
lock sync.RWMutex
}
// New returns an initialized Viper instance.
@ -355,6 +357,8 @@ func (v *Viper) WatchConfig() {
// Viper will use this and not check any of the config paths.
func SetConfigFile(in string) { v.SetConfigFile(in) }
func (v *Viper) SetConfigFile(in string) {
v.lock.Lock()
defer v.lock.Unlock()
if in != "" {
v.configFile = in
}
@ -365,6 +369,8 @@ func (v *Viper) SetConfigFile(in string) {
// variables that start with "SPF_".
func SetEnvPrefix(in string) { v.SetEnvPrefix(in) }
func (v *Viper) SetEnvPrefix(in string) {
v.lock.Lock()
defer v.lock.Unlock()
if in != "" {
v.envPrefix = in
}
@ -383,6 +389,8 @@ func (v *Viper) mergeWithEnvPrefix(in string) string {
// For backward compatibility reasons this is false by default.
func AllowEmptyEnv(allowEmptyEnv bool) { v.AllowEmptyEnv(allowEmptyEnv) }
func (v *Viper) AllowEmptyEnv(allowEmptyEnv bool) {
v.lock.Lock()
defer v.lock.Unlock()
v.allowEmptyEnv = allowEmptyEnv
}
@ -404,13 +412,21 @@ func (v *Viper) getEnv(key string) (string, bool) {
}
// ConfigFileUsed returns the file used to populate the config registry.
func ConfigFileUsed() string { return v.ConfigFileUsed() }
func (v *Viper) ConfigFileUsed() string { return v.configFile }
func ConfigFileUsed() string { return v.ConfigFileUsed() }
func (v *Viper) ConfigFileUsed() string {
v.lock.Lock()
defer v.lock.Unlock()
return v.configFile
}
// AddConfigPath adds a path for Viper to search for the config file in.
// Can be called multiple times to define multiple search paths.
func AddConfigPath(in string) { v.AddConfigPath(in) }
func (v *Viper) AddConfigPath(in string) {
v.lock.Lock()
defer v.lock.Unlock()
if in != "" {
absin := absPathify(in)
jww.INFO.Println("adding", absin, "to paths to search")
@ -432,6 +448,9 @@ func AddRemoteProvider(provider, endpoint, path string) error {
return v.AddRemoteProvider(provider, endpoint, path)
}
func (v *Viper) AddRemoteProvider(provider, endpoint, path string) error {
v.lock.Lock()
defer v.lock.Unlock()
if !stringInSlice(provider, SupportedRemoteProviders) {
return UnsupportedRemoteProviderError(provider)
}
@ -464,6 +483,9 @@ func AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) err
}
func (v *Viper) AddSecureRemoteProvider(provider, endpoint, path, secretkeyring string) error {
v.lock.Lock()
defer v.lock.Unlock()
if !stringInSlice(provider, SupportedRemoteProviders) {
return UnsupportedRemoteProviderError(provider)
}
@ -652,6 +674,9 @@ func (v *Viper) isPathShadowedInAutoEnv(path []string) string {
// "a b c"
func SetTypeByDefaultValue(enable bool) { v.SetTypeByDefaultValue(enable) }
func (v *Viper) SetTypeByDefaultValue(enable bool) {
v.lock.Lock()
defer v.lock.Unlock()
v.typeByDefValue = enable
}
@ -945,6 +970,8 @@ func (v *Viper) BindFlagValues(flags FlagValueSet) (err error) {
//
func BindFlagValue(key string, flag FlagValue) error { return v.BindFlagValue(key, flag) }
func (v *Viper) BindFlagValue(key string, flag FlagValue) error {
v.lock.Lock()
defer v.lock.Unlock()
if flag == nil {
return fmt.Errorf("flag for %q is nil", key)
}
@ -958,6 +985,8 @@ func (v *Viper) BindFlagValue(key string, flag FlagValue) error {
// EnvPrefix will be used when set when env name is not provided.
func BindEnv(input ...string) error { return v.BindEnv(input...) }
func (v *Viper) BindEnv(input ...string) error {
v.lock.Lock()
defer v.lock.Unlock()
var key, envkey string
if len(input) == 0 {
return fmt.Errorf("BindEnv missing key to bind to")
@ -982,6 +1011,8 @@ func (v *Viper) BindEnv(input ...string) error {
// Viper will check to see if an alias exists first.
// Note: this assumes a lower-cased key given.
func (v *Viper) find(lcaseKey string) interface{} {
v.lock.RLock()
defer v.lock.RUnlock()
var (
val interface{}
@ -1132,6 +1163,8 @@ func (v *Viper) IsSet(key string) bool {
// keys set in config, default & flags
func AutomaticEnv() { v.AutomaticEnv() }
func (v *Viper) AutomaticEnv() {
v.lock.Lock()
defer v.lock.Unlock()
v.automaticEnvApplied = true
}
@ -1140,6 +1173,8 @@ func (v *Viper) AutomaticEnv() {
// not match it.
func SetEnvKeyReplacer(r *strings.Replacer) { v.SetEnvKeyReplacer(r) }
func (v *Viper) SetEnvKeyReplacer(r *strings.Replacer) {
v.lock.Lock()
defer v.lock.Unlock()
v.envKeyReplacer = r
}
@ -1153,6 +1188,8 @@ func (v *Viper) RegisterAlias(alias string, key string) {
func (v *Viper) registerAlias(alias string, key string) {
alias = strings.ToLower(alias)
if alias != key && alias != v.realKey(key) {
v.lock.Lock()
defer v.lock.Unlock()
_, exists := v.aliases[alias]
if !exists {
@ -1183,6 +1220,8 @@ func (v *Viper) registerAlias(alias string, key string) {
}
func (v *Viper) realKey(key string) string {
v.lock.RLock()
defer v.lock.RUnlock()
newkey, exists := v.aliases[key]
if exists {
jww.DEBUG.Println("Alias", key, "to", newkey)
@ -1232,6 +1271,9 @@ func (v *Viper) Set(key string, value interface{}) {
lastKey := strings.ToLower(path[len(path)-1])
deepestMap := deepSearch(v.override, path[0:len(path)-1])
v.lock.Lock()
defer v.lock.Unlock()
// set innermost value
deepestMap[lastKey] = value
}
@ -1263,6 +1305,8 @@ func (v *Viper) ReadInConfig() error {
return err
}
v.lock.Lock()
defer v.lock.Unlock()
v.config = config
return nil
}
@ -1292,6 +1336,8 @@ func (v *Viper) MergeInConfig() error {
// key does not exist in the file.
func ReadConfig(in io.Reader) error { return v.ReadConfig(in) }
func (v *Viper) ReadConfig(in io.Reader) error {
v.lock.Lock()
defer v.lock.Unlock()
v.config = make(map[string]interface{})
return v.unmarshalReader(in, v.config)
}
@ -1310,6 +1356,8 @@ func (v *Viper) MergeConfig(in io.Reader) error {
// Note that the map given may be modified.
func MergeConfigMap(cfg map[string]interface{}) error { return v.MergeConfigMap(cfg) }
func (v *Viper) MergeConfigMap(cfg map[string]interface{}) error {
v.lock.Lock()
defer v.lock.Unlock()
if v.config == nil {
v.config = make(map[string]interface{})
}
@ -1374,13 +1422,19 @@ func (v *Viper) writeConfig(filename string, force bool) error {
return fmt.Errorf("File: %s exists. Use WriteConfig to overwrite.", filename)
}
}
var buffer bytes.Buffer
if err := v.marshalWriter(&buffer, configType); err != nil {
return err
}
f, err := v.fs.OpenFile(filename, flags, v.configPermissions)
if err != nil {
return err
}
defer f.Close()
if err := v.marshalWriter(f, configType); err != nil {
if _, err := io.Copy(f, &buffer); err != nil {
return err
}
@ -1456,11 +1510,16 @@ func (v *Viper) unmarshalReader(in io.Reader, c map[string]interface{}) error {
return nil
}
type fileWriter interface {
io.Writer
io.StringWriter
}
// Marshal a map into Writer.
func marshalWriter(f afero.File, configType string) error {
return v.marshalWriter(f, configType)
}
func (v *Viper) marshalWriter(f afero.File, configType string) error {
func (v *Viper) marshalWriter(f fileWriter, configType string) error {
c := v.AllSettings()
switch configType {
case "json":
@ -1648,8 +1707,11 @@ func (v *Viper) WatchRemoteConfigOnChannel() error {
// Retrieve the first found remote configuration.
func (v *Viper) getKeyValueConfig() error {
v.lock.RLock()
defer v.lock.RUnlock()
if RemoteConfig == nil {
return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/spf13/viper/remote'")
return RemoteConfigError("Enable the remote features by doing a blank import of the viper/remote package: '_ github.com/ory/viper/remote'")
}
for _, rp := range v.remoteProviders {
@ -1664,6 +1726,9 @@ func (v *Viper) getKeyValueConfig() error {
}
func (v *Viper) getRemoteConfig(provider RemoteProvider) (map[string]interface{}, error) {
v.lock.RLock()
defer v.lock.RUnlock()
reader, err := RemoteConfig.Get(provider)
if err != nil {
return nil, err
@ -1715,6 +1780,9 @@ func (v *Viper) watchRemoteConfig(provider RemoteProvider) (map[string]interface
// Nested keys are returned with a v.keyDelim (= ".") separator
func AllKeys() []string { return v.AllKeys() }
func (v *Viper) AllKeys() []string {
v.lock.RLock()
defer v.lock.RUnlock()
m := map[string]bool{}
// add all paths, by order of descending priority to ensure correct shadowing
m = v.flattenAndMergeMap(m, castMapStringToMapInterface(v.aliases), "")
@ -1810,12 +1878,15 @@ func (v *Viper) AllSettings() map[string]interface{} {
// set innermost value
deepestMap[lastKey] = value
}
return m
return toMapStringInterface(m).(map[string]interface{}) // This is safe because the input is map[string]interface{}
}
// SetFs sets the filesystem to use to read configuration.
func SetFs(fs afero.Fs) { v.SetFs(fs) }
func (v *Viper) SetFs(fs afero.Fs) {
v.lock.Lock()
defer v.lock.Unlock()
v.fs = fs
}
@ -1824,6 +1895,8 @@ func (v *Viper) SetFs(fs afero.Fs) {
func SetConfigName(in string) { v.SetConfigName(in) }
func (v *Viper) SetConfigName(in string) {
if in != "" {
v.lock.Lock()
defer v.lock.Unlock()
v.configName = in
v.configFile = ""
}
@ -1834,6 +1907,8 @@ func (v *Viper) SetConfigName(in string) {
func SetConfigType(in string) { v.SetConfigType(in) }
func (v *Viper) SetConfigType(in string) {
if in != "" {
v.lock.Lock()
defer v.lock.Unlock()
v.configType = in
}
}
@ -1841,6 +1916,8 @@ func (v *Viper) SetConfigType(in string) {
// SetConfigPermissions sets the permissions for the config file.
func SetConfigPermissions(perm os.FileMode) { v.SetConfigPermissions(perm) }
func (v *Viper) SetConfigPermissions(perm os.FileMode) {
v.lock.Lock()
defer v.lock.Unlock()
v.configPermissions = perm.Perm()
}
@ -1905,6 +1982,8 @@ func (v *Viper) findConfigFile() (string, error) {
// purposes.
func Debug() { v.Debug() }
func (v *Viper) Debug() {
v.lock.RLock()
defer v.lock.RUnlock()
fmt.Printf("Aliases:\n%#v\n", v.aliases)
fmt.Printf("Override:\n%#v\n", v.override)
fmt.Printf("PFlags:\n%#v\n", v.pflags)

View file

@ -245,7 +245,7 @@ func initDirs(t *testing.T) (string, string, func()) {
}
}
//stubs for PFlag Values
// stubs for PFlag Values
type stringValue string
func newStringValue(val string, p *string) *stringValue {
@ -763,7 +763,7 @@ func TestBindPFlag(t *testing.T) {
assert.Equal(t, testString, Get("testvalue"))
flag.Value.Set("testing_mutate")
flag.Changed = true //hack for pflag usage
flag.Changed = true // hack for pflag usage
assert.Equal(t, "testing_mutate", Get("testvalue"))
@ -1750,6 +1750,17 @@ func TestWatchFile(t *testing.T) {
}
func TestArrayOfObjects(t *testing.T) {
SetConfigType("yml")
require.NoError(t, ReadConfig(bytes.NewBufferString(`foo:
bar:
- baz: 1
- baz: 2`)))
SetConfigFile(path.Join(os.TempDir(), fmt.Sprintf("config-%d.json", time.Now().UnixNano())))
require.NoError(t, WriteConfig())
}
func BenchmarkGetBool(b *testing.B) {
key := "BenchmarkGetBool"
v = New()