feat(env): add config env source (#1181)
* add config/env * feat(env): add config env source * fix: resolve map array & add test case * remove return stop error * using gob encoding to deep copy map * fix ci failedpull/1186/head
parent
21a729bc3a
commit
7f394d0d0a
@ -0,0 +1,62 @@ |
|||||||
|
package env |
||||||
|
|
||||||
|
import ( |
||||||
|
"os" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/go-kratos/kratos/v2/config" |
||||||
|
) |
||||||
|
|
||||||
|
type env struct { |
||||||
|
prefixs []string |
||||||
|
} |
||||||
|
|
||||||
|
func NewSource(prefixs ...string) config.Source { |
||||||
|
return &env{prefixs: prefixs} |
||||||
|
} |
||||||
|
|
||||||
|
func (e *env) Load() (kv []*config.KeyValue, err error) { |
||||||
|
for _, envstr := range os.Environ() { |
||||||
|
var k, v string |
||||||
|
subs := strings.SplitN(envstr, "=", 2) |
||||||
|
k = subs[0] |
||||||
|
if len(subs) > 1 { |
||||||
|
v = subs[1] |
||||||
|
} |
||||||
|
|
||||||
|
if len(e.prefixs) > 0 { |
||||||
|
p, ok := matchPrefix(e.prefixs, envstr) |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
// trim prefix
|
||||||
|
k = k[len(p):] |
||||||
|
if k[0] == '_' { |
||||||
|
k = k[1:] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
kv = append(kv, &config.KeyValue{ |
||||||
|
Key: k, |
||||||
|
Value: []byte(v), |
||||||
|
}) |
||||||
|
} |
||||||
|
return |
||||||
|
} |
||||||
|
|
||||||
|
func (e *env) Watch() (config.Watcher, error) { |
||||||
|
w, err := NewWatcher() |
||||||
|
if err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
return w, nil |
||||||
|
} |
||||||
|
|
||||||
|
func matchPrefix(prefixs []string, s string) (string, bool) { |
||||||
|
for _, p := range prefixs { |
||||||
|
if strings.HasPrefix(s, p) { |
||||||
|
return p, true |
||||||
|
} |
||||||
|
} |
||||||
|
return "", false |
||||||
|
} |
@ -0,0 +1,247 @@ |
|||||||
|
package env |
||||||
|
|
||||||
|
import ( |
||||||
|
"io/ioutil" |
||||||
|
"os" |
||||||
|
"path/filepath" |
||||||
|
"reflect" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/go-kratos/kratos/v2/config" |
||||||
|
"github.com/go-kratos/kratos/v2/config/file" |
||||||
|
"github.com/stretchr/testify/assert" |
||||||
|
) |
||||||
|
|
||||||
|
const _testJSON = ` |
||||||
|
{ |
||||||
|
"test":{ |
||||||
|
"server":{ |
||||||
|
"name":"$SERVICE_NAME", |
||||||
|
"addr":"${ADDR:127.0.0.1}", |
||||||
|
"port":"${PORT:8080}" |
||||||
|
} |
||||||
|
}, |
||||||
|
"foo":[ |
||||||
|
{ |
||||||
|
"name":"Tom", |
||||||
|
"age":"${AGE}" |
||||||
|
} |
||||||
|
] |
||||||
|
}` |
||||||
|
|
||||||
|
func TestEnvWithPrefix(t *testing.T) { |
||||||
|
var ( |
||||||
|
path = filepath.Join(os.TempDir(), "test_config") |
||||||
|
filename = filepath.Join(path, "test.json") |
||||||
|
data = []byte(_testJSON) |
||||||
|
) |
||||||
|
defer os.Remove(path) |
||||||
|
if err := os.MkdirAll(path, 0700); err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
if err := ioutil.WriteFile(filename, data, 0666); err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
|
||||||
|
// set env
|
||||||
|
prefix1, prefix2 := "KRATOS_", "FOO" |
||||||
|
envs := map[string]string{ |
||||||
|
prefix1 + "SERVICE_NAME": "kratos_app", |
||||||
|
prefix2 + "ADDR": "192.168.0.1", |
||||||
|
prefix1 + "AGE": "20", |
||||||
|
} |
||||||
|
|
||||||
|
for k, v := range envs { |
||||||
|
if err := os.Setenv(k, v); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
c := config.New(config.WithSource( |
||||||
|
NewSource(prefix1, prefix2), |
||||||
|
file.NewSource(path), |
||||||
|
)) |
||||||
|
|
||||||
|
if err := c.Load(); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
|
||||||
|
tests := []struct { |
||||||
|
name string |
||||||
|
path string |
||||||
|
expect interface{} |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "test $KEY", |
||||||
|
path: "test.server.name", |
||||||
|
expect: "kratos_app", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY:DEFAULT} without default", |
||||||
|
path: "test.server.addr", |
||||||
|
expect: "192.168.0.1", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY:DEFAULT} with default", |
||||||
|
path: "test.server.port", |
||||||
|
expect: "8080", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY} in array", |
||||||
|
path: "foo", |
||||||
|
expect: []interface{}{ |
||||||
|
map[string]interface{}{ |
||||||
|
"name": "Tom", |
||||||
|
"age": "20", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, test := range tests { |
||||||
|
t.Run(test.name, func(t *testing.T) { |
||||||
|
var err error |
||||||
|
v := c.Value(test.path) |
||||||
|
if v.Load() != nil { |
||||||
|
var actual interface{} |
||||||
|
switch test.expect.(type) { |
||||||
|
case int: |
||||||
|
if actual, err = v.Int(); err == nil { |
||||||
|
assert.Equal(t, test.expect, int(actual.(int64)), "int value should be equal") |
||||||
|
} |
||||||
|
case string: |
||||||
|
if actual, err = v.String(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "string value should be equal") |
||||||
|
} |
||||||
|
case bool: |
||||||
|
if actual, err = v.Bool(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "bool value should be equal") |
||||||
|
} |
||||||
|
case float64: |
||||||
|
if actual, err = v.Float(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "float64 value should be equal") |
||||||
|
} |
||||||
|
default: |
||||||
|
actual = v.Load() |
||||||
|
if !reflect.DeepEqual(test.expect, actual) { |
||||||
|
t.Logf("\nexpect: %#v\nactural: %#v", test.expect, actual) |
||||||
|
t.Fail() |
||||||
|
} |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Error("value path not found") |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
func TestEnvWithoutPrefix(t *testing.T) { |
||||||
|
var ( |
||||||
|
path = filepath.Join(os.TempDir(), "test_config") |
||||||
|
filename = filepath.Join(path, "test.json") |
||||||
|
data = []byte(_testJSON) |
||||||
|
) |
||||||
|
defer os.Remove(path) |
||||||
|
if err := os.MkdirAll(path, 0700); err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
if err := ioutil.WriteFile(filename, data, 0666); err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
|
||||||
|
// set env
|
||||||
|
envs := map[string]string{ |
||||||
|
"SERVICE_NAME": "kratos_app", |
||||||
|
"ADDR": "192.168.0.1", |
||||||
|
"AGE": "20", |
||||||
|
} |
||||||
|
|
||||||
|
for k, v := range envs { |
||||||
|
if err := os.Setenv(k, v); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
c := config.New(config.WithSource( |
||||||
|
NewSource(), |
||||||
|
file.NewSource(path), |
||||||
|
)) |
||||||
|
|
||||||
|
if err := c.Load(); err != nil { |
||||||
|
t.Fatal(err) |
||||||
|
} |
||||||
|
|
||||||
|
tests := []struct { |
||||||
|
name string |
||||||
|
path string |
||||||
|
expect interface{} |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "test $KEY", |
||||||
|
path: "test.server.name", |
||||||
|
expect: "kratos_app", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY:DEFAULT} without default", |
||||||
|
path: "test.server.addr", |
||||||
|
expect: "192.168.0.1", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY:DEFAULT} with default", |
||||||
|
path: "test.server.port", |
||||||
|
expect: "8080", |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "test ${KEY} in array", |
||||||
|
path: "foo", |
||||||
|
expect: []interface{}{ |
||||||
|
map[string]interface{}{ |
||||||
|
"name": "Tom", |
||||||
|
"age": "20", |
||||||
|
}, |
||||||
|
}, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, test := range tests { |
||||||
|
t.Run(test.name, func(t *testing.T) { |
||||||
|
var err error |
||||||
|
v := c.Value(test.path) |
||||||
|
if v.Load() != nil { |
||||||
|
var actual interface{} |
||||||
|
switch test.expect.(type) { |
||||||
|
case int: |
||||||
|
if actual, err = v.Int(); err == nil { |
||||||
|
assert.Equal(t, test.expect, int(actual.(int64)), "int value should be equal") |
||||||
|
} |
||||||
|
case string: |
||||||
|
if actual, err = v.String(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "string value should be equal") |
||||||
|
} |
||||||
|
case bool: |
||||||
|
if actual, err = v.Bool(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "bool value should be equal") |
||||||
|
} |
||||||
|
case float64: |
||||||
|
if actual, err = v.Float(); err == nil { |
||||||
|
assert.Equal(t, test.expect, actual, "float64 value should be equal") |
||||||
|
} |
||||||
|
default: |
||||||
|
actual = v.Load() |
||||||
|
if !reflect.DeepEqual(test.expect, actual) { |
||||||
|
t.Logf("\nexpect: %#v\nactural: %#v", test.expect, actual) |
||||||
|
t.Fail() |
||||||
|
} |
||||||
|
} |
||||||
|
if err != nil { |
||||||
|
t.Error(err) |
||||||
|
} |
||||||
|
} else { |
||||||
|
t.Error("value path not found") |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package env |
||||||
|
|
||||||
|
import ( |
||||||
|
"github.com/go-kratos/kratos/v2/config" |
||||||
|
) |
||||||
|
|
||||||
|
type watcher struct { |
||||||
|
exit chan struct{} |
||||||
|
} |
||||||
|
|
||||||
|
func NewWatcher() (config.Watcher, error) { |
||||||
|
return &watcher{exit: make(chan struct{})}, nil |
||||||
|
} |
||||||
|
|
||||||
|
// Next will be blocked until the Stop method is called
|
||||||
|
func (w *watcher) Next() ([]*config.KeyValue, error) { |
||||||
|
<-w.exit |
||||||
|
return nil, nil |
||||||
|
} |
||||||
|
|
||||||
|
func (w *watcher) Stop() error { |
||||||
|
close(w.exit) |
||||||
|
return nil |
||||||
|
} |
Loading…
Reference in new issue