Create & Init Project...

This commit is contained in:
2019-04-22 18:49:16 +08:00
commit fc4fa37393
25440 changed files with 4054998 additions and 0 deletions

View File

@@ -0,0 +1,80 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
"go_test",
)
go_library(
name = "go_default_library",
srcs = [
"client.go",
"default.go",
"file.go",
"helper.go",
"map.go",
"mock.go",
"sven.go",
"toml.go",
"value.go",
],
importpath = "go-common/library/conf/paladin",
tags = ["automanaged"],
visibility = ["//visibility:public"],
deps = [
"//library/conf/env:go_default_library",
"//library/ecode:go_default_library",
"//library/log:go_default_library",
"//library/net/ip:go_default_library",
"//library/net/netutil:go_default_library",
"//vendor/github.com/BurntSushi/toml:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/pkg/errors:go_default_library",
],
)
go_test(
name = "go_default_xtest",
srcs = [
"example_test.go",
"file_test.go",
"map_test.go",
"mock_test.go",
],
tags = ["automanaged"],
deps = [
"//library/conf/paladin:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
visibility = ["//visibility:public"],
)
go_test(
name = "go_default_test",
srcs = [
"sven_test.go",
"value_test.go",
],
embed = [":go_default_library"],
tags = ["automanaged"],
deps = [
"//library/conf/env:go_default_library",
"//vendor/github.com/naoina/toml:go_default_library",
"//vendor/github.com/stretchr/testify/assert:go_default_library",
],
)

View File

@@ -0,0 +1,24 @@
### paladin
##### Version 1.1.2
> 1.修改map key to lower case
##### Version 1.1.1
> 1.修复错误时会panic
##### Version 1.1.0
> 1.修正Var统一显示处理panic
> 2.修复本地文件路径问题
> 3.添加helper快捷处理default value
##### Version 1.0.2
> 1.修下unmarshal方法为toml
##### Version 1.0.1
> 1.default/map/value添加Unmarshal方法
##### Version 1.0.0
> 1.支持sven、file、mock配置读取
> 2.支持struct、paladin.Map对象解析
> 3.支持Set接口进行配置Reload
> 4.支持Watch自定义订阅key value变化

View File

@@ -0,0 +1,10 @@
# Owner
maojian
# Author
chenzhihui
# Reviewer
maojian
haoguanwei
lintanghui

View File

@@ -0,0 +1,10 @@
# See the OWNERS docs at https://go.k8s.io/owners
approvers:
- chenzhihui
- maojian
reviewers:
- chenzhihui
- haoguanwei
- lintanghui
- maojian

View File

@@ -0,0 +1,86 @@
#### paladin
##### 项目简介
paladin 是一个config SDK客户端包括了sven、file、mock几个抽象功能方便使用本地文件或者sven配置中心并且集成了对象自动reload功能。
sven:
```
caster配置项
配置地址CONF_HOST: config.bilibili.co
配置版本CONF_VERSION: docker-1/server-1
配置路径CONF_PATH: /data/conf/app
配置TokenCONF_TOKEN: token
配置指定版本CONF_APPOINT: 27600
依赖环境变量:
TREE_ID/DEPLOY_ENV/ZONE/HOSTNAME/POD_IP
```
local files:
```
/data/app/msm-service -conf=/data/conf/app/msm-servie.toml
// or multi file
/data/app/msm-service -conf=/data/conf/app/
```
example:
```
type exampleConf struct {
Bool bool
Int int64
Float float64
String string
}
func (e *exampleConf) Set(text string) error {
var ec exampleConf
if err := toml.Unmarshal([]byte(text), &ec); err != nil {
return err
}
*e = ec
return nil
}
func ExampleClient() {
if err := paladin.Init(); err != nil {
panic(err)
}
var (
ec exampleConf
eo exampleConf
m paladin.TOML
strs []string
)
// config unmarshal
if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil {
panic(err)
}
// config setter
if err := paladin.Watch("example.toml", &ec); err != nil {
panic(err)
}
// paladin map
if err := paladin.Watch("example.toml", &m); err != nil {
panic(err)
}
s, err := m.Value("key").String()
b, err := m.Value("key").Bool()
i, err := m.Value("key").Int64()
f, err := m.Value("key").Float64()
// value slice
err = m.Value("strings").Slice(&strs)
// watch key
for event := range paladin.WatchEvent(context.TODO(), "key") {
fmt.Println(event)
}
}
```
##### 编译环境
- **请只用 Golang v1.8.x 以上版本编译执行**
##### 依赖包
> 1. github.com/naoina/toml
> 2. github.com/pkg/errors

View File

@@ -0,0 +1,49 @@
package paladin
import (
"context"
)
const (
// EventAdd config add event.
EventAdd EventType = iota
// EventUpdate config update event.
EventUpdate
// EventRemove config remove event.
EventRemove
)
// EventType is config event.
type EventType int
// Event is watch event.
type Event struct {
Event EventType
Key string
Value string
}
// Watcher is config watcher.
type Watcher interface {
WatchEvent(context.Context, ...string) <-chan Event
Close() error
}
// Setter is value setter.
type Setter interface {
Set(string) error
}
// Getter is value getter.
type Getter interface {
// Get a config value by a config key(may be a sven filename).
Get(string) *Value
// GetAll return all config key->value map.
GetAll() *Map
}
// Client is config client.
type Client interface {
Watcher
Getter
}

View File

@@ -0,0 +1,84 @@
package paladin
import (
"context"
"flag"
"go-common/library/log"
)
var (
// DefaultClient default client.
DefaultClient Client
confPath string
vars = make(map[string][]Setter) // NOTE: no thread safe
)
func init() {
flag.StringVar(&confPath, "conf", "", "default config path")
}
// Init init config client.
func Init() (err error) {
if confPath != "" {
DefaultClient, err = NewFile(confPath)
} else {
DefaultClient, err = NewSven()
}
if err != nil {
return
}
go func() {
for event := range DefaultClient.WatchEvent(context.Background()) {
if event.Event != EventUpdate && event.Event != EventAdd {
continue
}
if sets, ok := vars[event.Key]; ok {
for _, s := range sets {
if err := s.Set(event.Value); err != nil {
log.Error("paladin: vars:%v event:%v error(%v)", s, event, err)
}
}
}
}
}()
return
}
// Watch watch on a key. The configuration implements the setter interface, which is invoked when the configuration changes.
func Watch(key string, s Setter) error {
v := DefaultClient.Get(key)
str, err := v.Raw()
if err != nil {
return err
}
if err := s.Set(str); err != nil {
return err
}
vars[key] = append(vars[key], s)
return nil
}
// WatchEvent watch on multi keys. Events are returned when the configuration changes.
func WatchEvent(ctx context.Context, keys ...string) <-chan Event {
return DefaultClient.WatchEvent(ctx, keys...)
}
// Get return value by key.
func Get(key string) *Value {
return DefaultClient.Get(key)
}
// GetAll return all config map.
func GetAll() *Map {
return DefaultClient.GetAll()
}
// Keys return values key.
func Keys() []string {
return DefaultClient.GetAll().Keys()
}
// Close close watcher.
func Close() error {
return DefaultClient.Close()
}

View File

@@ -0,0 +1,112 @@
package paladin_test
import (
"context"
"fmt"
"go-common/library/conf/paladin"
"github.com/naoina/toml"
)
type exampleConf struct {
Bool bool
Int int64
Float float64
String string
Strings []string
}
func (e *exampleConf) Set(text string) error {
var ec exampleConf
if err := toml.Unmarshal([]byte(text), &ec); err != nil {
return err
}
*e = ec
return nil
}
// ExampleClient is a example client usage.
// exmaple.toml:
/*
bool = true
int = 100
float = 100.1
string = "text"
strings = ["a", "b", "c"]
*/
func ExampleClient() {
if err := paladin.Init(); err != nil {
panic(err)
}
var ec exampleConf
// var setter
if err := paladin.Watch("example.toml", &ec); err != nil {
panic(err)
}
if err := paladin.Get("example.toml").UnmarshalTOML(&ec); err != nil {
panic(err)
}
// use exampleConf
// watch event key
go func() {
for event := range paladin.WatchEvent(context.TODO(), "key") {
fmt.Println(event)
}
}()
}
// ExampleMap is a example map usage.
// exmaple.toml:
/*
bool = true
int = 100
float = 100.1
string = "text"
strings = ["a", "b", "c"]
[object]
string = "text"
bool = true
int = 100
float = 100.1
strings = ["a", "b", "c"]
*/
func ExampleMap() {
var (
m paladin.TOML
strs []string
)
// paladin toml
if err := paladin.Watch("example.toml", &m); err != nil {
panic(err)
}
// value string
s, err := m.Get("string").String()
if err != nil {
s = "default"
}
fmt.Println(s)
// value bool
b, err := m.Get("bool").Bool()
if err != nil {
b = false
}
fmt.Println(b)
// value int
i, err := m.Get("int").Int64()
if err != nil {
i = 100
}
fmt.Println(i)
// value float
f, err := m.Get("float").Float64()
if err != nil {
f = 100.1
}
fmt.Println(f)
// value slice
if err = m.Get("strings").Slice(&strs); err == nil {
fmt.Println(strs)
}
}

View File

@@ -0,0 +1,82 @@
package paladin
import (
"context"
"errors"
"io/ioutil"
"os"
"path"
"path/filepath"
)
var _ Client = &file{}
// file is file config client.
type file struct {
ch chan Event
values *Map
}
// NewFile new a config file client.
// conf = /data/conf/app/
// conf = /data/conf/app/xxx.toml
func NewFile(base string) (Client, error) {
// paltform slash
base = filepath.FromSlash(base)
fi, err := os.Stat(base)
if err != nil {
panic(err)
}
// dirs or file to paths
var paths []string
if fi.IsDir() {
files, err := ioutil.ReadDir(base)
if err != nil {
panic(err)
}
for _, file := range files {
if !file.IsDir() {
paths = append(paths, path.Join(base, file.Name()))
}
}
} else {
paths = append(paths, base)
}
// laod config file to values
values := make(map[string]*Value, len(paths))
for _, file := range paths {
if file == "" {
return nil, errors.New("paladin: path is empty")
}
b, err := ioutil.ReadFile(file)
if err != nil {
return nil, err
}
s := string(b)
values[path.Base(file)] = &Value{val: s, raw: s}
}
m := new(Map)
m.Store(values)
return &file{values: m, ch: make(chan Event, 10)}, nil
}
// Get return value by key.
func (f *file) Get(key string) *Value {
return f.values.Get(key)
}
// GetAll return value map.
func (f *file) GetAll() *Map {
return f.values
}
// WatchEvent watch multi key.
func (f *file) WatchEvent(ctx context.Context, key ...string) <-chan Event {
return f.ch
}
// Close close watcher.
func (f *file) Close() error {
close(f.ch)
return nil
}

View File

@@ -0,0 +1,67 @@
package paladin_test
import (
"io/ioutil"
"os"
"testing"
"go-common/library/conf/paladin"
"github.com/stretchr/testify/assert"
)
func TestNewFile(t *testing.T) {
// test data
path := "/tmp/test_conf/"
assert.Nil(t, os.MkdirAll(path, 0700))
assert.Nil(t, ioutil.WriteFile(path+"test.toml", []byte(`
text = "hello"
number = 100
slice = [1, 2, 3]
sliceStr = ["1", "2", "3"]
`), 0644))
// test client
cli, err := paladin.NewFile(path + "test.toml")
assert.Nil(t, err)
assert.NotNil(t, cli)
// test map
m := paladin.Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), "text")
s, err := m.Get("text").String()
assert.Nil(t, err)
assert.Equal(t, s, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err)
assert.Equal(t, n, int64(100), "number")
}
func TestNewFilePath(t *testing.T) {
// test data
path := "/tmp/test_conf/"
assert.Nil(t, os.MkdirAll(path, 0700))
assert.Nil(t, ioutil.WriteFile(path+"test.toml", []byte(`
text = "hello"
number = 100
`), 0644))
assert.Nil(t, ioutil.WriteFile(path+"abc.toml", []byte(`
text = "hello"
number = 100
`), 0644))
// test client
cli, err := paladin.NewFile(path)
assert.Nil(t, err)
assert.NotNil(t, cli)
// test map
m := paladin.Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), "text")
s, err := m.Get("text").String()
assert.Nil(t, err, s)
assert.Equal(t, s, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err, s)
assert.Equal(t, n, int64(100), "number")
}

View File

@@ -0,0 +1,76 @@
package paladin
import "time"
// Bool return bool value.
func Bool(v *Value, def bool) bool {
b, err := v.Bool()
if err != nil {
return def
}
return b
}
// Int return int value.
func Int(v *Value, def int) int {
i, err := v.Int()
if err != nil {
return def
}
return i
}
// Int32 return int32 value.
func Int32(v *Value, def int32) int32 {
i, err := v.Int32()
if err != nil {
return def
}
return i
}
// Int64 return int64 value.
func Int64(v *Value, def int64) int64 {
i, err := v.Int64()
if err != nil {
return def
}
return i
}
// Float32 return float32 value.
func Float32(v *Value, def float32) float32 {
f, err := v.Float32()
if err != nil {
return def
}
return f
}
// Float64 return float32 value.
func Float64(v *Value, def float64) float64 {
f, err := v.Float64()
if err != nil {
return def
}
return f
}
// String return string value.
func String(v *Value, def string) string {
s, err := v.String()
if err != nil {
return def
}
return s
}
// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func Duration(v *Value, def time.Duration) time.Duration {
dur, err := v.Duration()
if err != nil {
return def
}
return dur
}

View File

@@ -0,0 +1,55 @@
package paladin
import (
"strings"
"sync/atomic"
)
// keyNamed key naming to lower case.
func keyNamed(key string) string {
return strings.ToLower(key)
}
// Map is config map, key(filename) -> value(file).
type Map struct {
values atomic.Value
}
// Store sets the value of the Value to values map.
func (m *Map) Store(values map[string]*Value) {
dst := make(map[string]*Value, len(values))
for k, v := range values {
dst[keyNamed(k)] = v
}
m.values.Store(dst)
}
// Load returns the value set by the most recent Store.
func (m *Map) Load() map[string]*Value {
return m.values.Load().(map[string]*Value)
}
// Exist check if values map exist a key.
func (m *Map) Exist(key string) bool {
_, ok := m.Load()[keyNamed(key)]
return ok
}
// Get return get value by key.
func (m *Map) Get(key string) *Value {
v, ok := m.Load()[keyNamed(key)]
if ok {
return v
}
return &Value{}
}
// Keys return map keys.
func (m *Map) Keys() []string {
values := m.Load()
keys := make([]string, 0, len(values))
for key := range values {
keys = append(keys, key)
}
return keys
}

View File

@@ -0,0 +1,94 @@
package paladin_test
import (
"testing"
"go-common/library/conf/paladin"
"github.com/naoina/toml"
"github.com/stretchr/testify/assert"
)
type fruit struct {
Fruit []struct {
Name string
}
}
func (f *fruit) Set(text string) error {
return toml.Unmarshal([]byte(text), f)
}
func TestMap(t *testing.T) {
s := `
# kv
text = "hello"
number = 100
point = 100.1
boolean = true
KeyCase = "test"
# slice
numbers = [1, 2, 3]
strings = ["a", "b", "c"]
empty = []
[[fruit]]
name = "apple"
[[fruit]]
name = "banana"
# table
[database]
server = "192.168.1.1"
connection_max = 5000
enabled = true
[pool]
[pool.breaker]
xxx = "xxx"
`
m := paladin.Map{}
assert.Nil(t, m.Set(s), s)
str, err := m.Get("text").String()
assert.Nil(t, err)
assert.Equal(t, str, "hello", "text")
n, err := m.Get("number").Int64()
assert.Nil(t, err)
assert.Equal(t, n, int64(100), "number")
p, err := m.Get("point").Float64()
assert.Nil(t, err)
assert.Equal(t, p, 100.1, "point")
b, err := m.Get("boolean").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true, "boolean")
// key lower case
lb, err := m.Get("Boolean").Bool()
assert.Nil(t, err)
assert.Equal(t, lb, true, "boolean")
lt, err := m.Get("KeyCase").String()
assert.Nil(t, err)
assert.Equal(t, lt, "test", "key case")
var sliceInt []int64
err = m.Get("numbers").Slice(&sliceInt)
assert.Nil(t, err)
assert.Equal(t, sliceInt, []int64{1, 2, 3})
var sliceStr []string
err = m.Get("strings").Slice(&sliceStr)
assert.Nil(t, err)
assert.Equal(t, sliceStr, []string{"a", "b", "c"})
err = m.Get("strings").Slice(&sliceStr)
assert.Nil(t, err)
assert.Equal(t, sliceStr, []string{"a", "b", "c"})
// errors
err = m.Get("strings").Slice(sliceInt)
assert.NotNil(t, err)
err = m.Get("strings").Slice(&sliceInt)
assert.NotNil(t, err)
var obj struct {
Name string
}
err = m.Get("strings").Slice(obj)
assert.NotNil(t, err)
err = m.Get("strings").Slice(&obj)
assert.NotNil(t, err)
}

View File

@@ -0,0 +1,45 @@
package paladin
import (
"context"
)
var _ Client = &mock{}
// mock is mock config client.
type mock struct {
ch chan Event
values *Map
}
// NewMock new a config mock client.
func NewMock(vs map[string]string) Client {
values := make(map[string]*Value, len(vs))
for k, v := range vs {
values[k] = &Value{val: v, raw: v}
}
m := new(Map)
m.Store(values)
return &mock{values: m, ch: make(chan Event)}
}
// Get return value by key.
func (m *mock) Get(key string) *Value {
return m.values.Get(key)
}
// GetAll return value map.
func (m *mock) GetAll() *Map {
return m.values
}
// WatchEvent watch multi key.
func (m *mock) WatchEvent(ctx context.Context, key ...string) <-chan Event {
return m.ch
}
// Close close watcher.
func (m *mock) Close() error {
close(m.ch)
return nil
}

View File

@@ -0,0 +1,37 @@
package paladin_test
import (
"testing"
"go-common/library/conf/paladin"
"github.com/stretchr/testify/assert"
)
func TestMock(t *testing.T) {
cs := map[string]string{
"key_toml": `
key_bool = true
key_int = 100
key_float = 100.1
key_string = "text"
`,
}
cli := paladin.NewMock(cs)
// test vlaue
var m paladin.TOML
err := cli.Get("key_toml").Unmarshal(&m)
assert.Nil(t, err)
b, err := m.Get("key_bool").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true)
i, err := m.Get("key_int").Int64()
assert.Nil(t, err)
assert.Equal(t, i, int64(100))
f, err := m.Get("key_float").Float64()
assert.Nil(t, err)
assert.Equal(t, f, float64(100.1))
s, err := m.Get("key_string").String()
assert.Nil(t, err)
assert.Equal(t, s, "text")
}

View File

@@ -0,0 +1,372 @@
package paladin
import (
"context"
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"os"
"path"
"strconv"
"sync"
"time"
"go-common/library/conf/env"
"go-common/library/ecode"
"go-common/library/log"
xip "go-common/library/net/ip"
"go-common/library/net/netutil"
"github.com/pkg/errors"
)
const (
_apiGet = "http://%s/config/v2/get?%s"
_apiCheck = "http://%s/config/v2/check?%s"
_maxLoadRetries = 3
)
var (
_ Client = &sven{}
svenHost string
svenVersion string
svenPath string
svenToken string
svenAppoint string
svenTreeid string
_debug bool
)
func init() {
flag.StringVar(&svenHost, "conf_host", os.Getenv("CONF_HOST"), `config api host.`)
flag.StringVar(&svenVersion, "conf_version", os.Getenv("CONF_VERSION"), `app version.`)
flag.StringVar(&svenPath, "conf_path", os.Getenv("CONF_PATH"), `config file path.`)
flag.StringVar(&svenToken, "conf_token", os.Getenv("CONF_TOKEN"), `config token.`)
flag.StringVar(&svenAppoint, "conf_appoint", os.Getenv("CONF_APPOINT"), `config appoint.`)
flag.StringVar(&svenTreeid, "tree_id", os.Getenv("TREE_ID"), `tree id.`)
if env.DeployEnv == env.DeployEnvDev {
_debug = true
}
}
type watcher struct {
keys []string
ch chan Event
}
func newWatcher(keys []string) *watcher {
return &watcher{keys: keys, ch: make(chan Event, 5)}
}
func (w *watcher) HasKey(key string) bool {
if len(w.keys) == 0 {
return true
}
for _, k := range w.keys {
if k == key {
return true
}
}
return false
}
func (w *watcher) Handle(event Event) {
select {
case w.ch <- event:
default:
log.Error("paladin: discard event:%+v", event)
}
}
func (w *watcher) Chan() <-chan Event {
return w.ch
}
func (w *watcher) Close() {
close(w.ch)
}
// sven is sven config client.
type sven struct {
values *Map
wmu sync.RWMutex
watchers map[*watcher]struct{}
httpCli *http.Client
backoff *netutil.BackoffConfig
}
// NewSven new a config client.
func NewSven() (Client, error) {
s := &sven{
values: new(Map),
watchers: make(map[*watcher]struct{}),
httpCli: &http.Client{Timeout: 60 * time.Second},
backoff: &netutil.BackoffConfig{
MaxDelay: 5 * time.Second,
BaseDelay: 1.0 * time.Second,
Factor: 1.6,
Jitter: 0.2,
},
}
if err := s.checkEnv(); err != nil {
return nil, err
}
ver, err := s.load()
if err != nil {
return nil, err
}
go s.watchproc(ver)
return s, nil
}
func (s *sven) checkEnv() error {
if svenHost == "" || svenVersion == "" || svenPath == "" || svenToken == "" || svenTreeid == "" {
return fmt.Errorf("config env invalid. conf_host(%s) conf_version(%s) conf_path(%s) conf_token(%s) conf_appoint(%s) tree_id(%s)", svenHost, svenVersion, svenPath, svenToken, svenAppoint, svenTreeid)
}
return nil
}
// Get return value by key.
func (s *sven) Get(key string) *Value {
return s.values.Get(key)
}
// GetAll return value map.
func (s *sven) GetAll() *Map {
return s.values
}
// WatchEvent watch with the specified keys.
func (s *sven) WatchEvent(ctx context.Context, keys ...string) <-chan Event {
w := newWatcher(keys)
s.wmu.Lock()
s.watchers[w] = struct{}{}
s.wmu.Unlock()
return w.Chan()
}
// Close close watcher.
func (s *sven) Close() (err error) {
s.wmu.RLock()
for w := range s.watchers {
w.Close()
}
s.wmu.RUnlock()
return
}
func (s *sven) fireEvent(event Event) {
s.wmu.RLock()
for w := range s.watchers {
if w.HasKey(event.Key) {
w.Handle(event)
}
}
s.wmu.RUnlock()
}
func (s *sven) load() (ver int64, err error) {
var (
v *version
cs []*content
)
if v, err = s.check(-1); err != nil {
log.Error("paladin: s.check(-1) error(%v)", err)
return
}
for i := 0; i < _maxLoadRetries; i++ {
if cs, err = s.config(v); err == nil {
all := make(map[string]*Value, len(cs))
for _, v := range cs {
all[v.Name] = &Value{val: v.Config, raw: v.Config}
}
s.values.Store(all)
return v.Version, nil
}
log.Error("paladin: s.config(%v) error(%v)", ver, err)
time.Sleep(s.backoff.Backoff(i))
}
return 0, err
}
func (s *sven) watchproc(ver int64) {
var retry int
for {
v, err := s.check(ver)
if err != nil {
if ecode.NotModified.Equal(err) {
time.Sleep(time.Second)
continue
}
log.Error("paladin: s.check(%d) error(%v)", ver, err)
retry++
time.Sleep(s.backoff.Backoff(retry))
continue
}
cs, err := s.config(v)
if err != nil {
log.Error("paladin: s.config(%v) error(%v)", ver, err)
retry++
time.Sleep(s.backoff.Backoff(retry))
continue
}
all := s.values.Load()
news := make(map[string]*Value, len(cs))
for _, v := range cs {
if _, ok := all[v.Name]; !ok {
go s.fireEvent(Event{Event: EventAdd, Key: v.Name, Value: v.Config})
} else if v.Config != "" {
go s.fireEvent(Event{Event: EventUpdate, Key: v.Name, Value: v.Config})
} else {
go s.fireEvent(Event{Event: EventRemove, Key: v.Name, Value: v.Config})
}
news[v.Name] = &Value{val: v.Config, raw: v.Config}
}
for k, v := range all {
if _, ok := news[k]; !ok {
news[k] = v
}
}
s.values.Store(news)
ver = v.Version
retry = 0
}
}
type version struct {
Version int64 `json:"version"`
Diffs []int64 `json:"diffs"`
}
type config struct {
Version int64 `json:"version"`
Content string `json:"content"`
Md5 string `json:"md5"`
}
type content struct {
Cid int64 `json:"cid"`
Name string `json:"name"`
Config string `json:"config"`
}
func (s *sven) check(ver int64) (v *version, err error) {
params := newParams()
params.Set("version", strconv.FormatInt(ver, 10))
params.Set("appoint", svenAppoint)
var res struct {
Code int `json:"code"`
Data *version `json:"data"`
}
uri := fmt.Sprintf(_apiCheck, svenHost, params.Encode())
if _debug {
fmt.Printf("paladin: check(%d) uri(%s)\n", ver, uri)
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return
}
resp, err := s.httpCli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = errors.Errorf("paladin: httpCli.GET(%s) error(%d)", params.Encode(), resp.StatusCode)
return
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = json.Unmarshal(b, &res); err != nil {
return
}
if ec := ecode.Int(res.Code); !ec.Equal(ecode.OK) {
err = ec
return
}
if res.Data == nil {
err = errors.Errorf("paladin: http version is nil. params(%s)", params.Encode())
return
}
v = res.Data
return
}
func (s *sven) config(ver *version) (cts []*content, err error) {
ids, _ := json.Marshal(ver.Diffs)
params := newParams()
params.Set("version", strconv.FormatInt(ver.Version, 10))
params.Set("ids", string(ids))
var res struct {
Code int `json:"code"`
Data *config `json:"data"`
}
uri := fmt.Sprintf(_apiGet, svenHost, params.Encode())
if _debug {
fmt.Printf("paladin: config(%+v) uri(%s)\n", ver, uri)
}
req, err := http.NewRequest("GET", uri, nil)
if err != nil {
return
}
resp, err := s.httpCli.Do(req)
if err != nil {
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = errors.Errorf("paladin: httpCli.GET(%s) error(%d)", params.Encode(), resp.StatusCode)
return
}
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return
}
if err = json.Unmarshal(b, &res); err != nil {
return
}
if !ecode.Int(res.Code).Equal(ecode.OK) || res.Data == nil {
err = errors.Errorf("paladin: http config is nil. params(%s) ecode(%d)", params.Encode(), res.Code)
return
}
if err = json.Unmarshal([]byte(res.Data.Content), &cts); err != nil {
return
}
for _, c := range cts {
if err = ioutil.WriteFile(path.Join(svenPath, c.Name), []byte(c.Config), 0644); err != nil {
return
}
}
return
}
func newParams() url.Values {
params := url.Values{}
params.Set("service", serviceName())
params.Set("build", svenVersion)
params.Set("token", svenToken)
params.Set("hostname", env.Hostname)
params.Set("ip", ipAddr())
return params
}
func ipAddr() string {
if env.IP != "" {
return env.IP
}
return xip.InternalIP()
}
func serviceName() string {
return fmt.Sprintf("%s_%s_%s", svenTreeid, env.DeployEnv, env.Zone)
}

View File

@@ -0,0 +1,119 @@
package paladin
import (
"context"
"testing"
"time"
"go-common/library/conf/env"
"github.com/naoina/toml"
"github.com/stretchr/testify/assert"
)
type testObj struct {
Bool bool
Int int64
Float float64
String string
}
func (t *testObj) Set(text string) error {
return toml.Unmarshal([]byte(text), t)
}
type testConf struct {
Bool bool
Int int64
Float float64
String string
Object *testObj
}
func (t *testConf) Set(text string) error {
return toml.Unmarshal([]byte(text), t)
}
func TestSven(t *testing.T) {
svenHost = "config.bilibili.co"
svenVersion = "server-1"
svenPath = "/tmp"
svenToken = "1afe5efaf45e11e7b3f8c6cd4f230d8c"
svenAppoint = ""
svenTreeid = "2888"
env.Region = "sh"
env.Zone = "sh001"
env.Hostname = "test"
env.DeployEnv = "dev"
env.AppID = "main.common-arch.msm-service"
sven, err := NewSven()
assert.Nil(t, err)
testSvenMap(t, sven)
testSvenValue(t, sven)
testWatch(t, sven)
}
func testSvenMap(t *testing.T, cli Client) {
m := Map{}
text, err := cli.Get("test.toml").String()
assert.Nil(t, err)
assert.Nil(t, m.Set(text), text)
b, err := m.Get("bool").Bool()
assert.Nil(t, err)
assert.Equal(t, b, true, "bool")
// int64
i, err := m.Get("int").Int64()
assert.Nil(t, err)
assert.Equal(t, i, int64(100), "int64")
// float64
f, err := m.Get("float").Float64()
assert.Nil(t, err)
assert.Equal(t, f, 100.1, "float64")
// string
s, err := m.Get("string").String()
assert.Nil(t, err)
assert.Equal(t, s, "text", "string")
// error
n, err := m.Get("not_exsit").String()
assert.NotNil(t, err)
assert.Equal(t, n, "", "not_exsit")
obj := new(testObj)
text, err = m.Get("object").Raw()
assert.Nil(t, err)
assert.Nil(t, obj.Set(text))
assert.Equal(t, obj.Bool, true, "bool")
assert.Equal(t, obj.Int, int64(100), "int64")
assert.Equal(t, obj.Float, 100.1, "float64")
assert.Equal(t, obj.String, "text", "string")
}
func testSvenValue(t *testing.T, cli Client) {
v := new(testConf)
text, err := cli.Get("test.toml").Raw()
assert.Nil(t, err)
assert.Nil(t, v.Set(text))
assert.Equal(t, v.Bool, true, "bool")
assert.Equal(t, v.Int, int64(100), "int64")
assert.Equal(t, v.Float, 100.1, "float64")
assert.Equal(t, v.String, "text", "string")
assert.Equal(t, v.Object.Bool, true, "bool")
assert.Equal(t, v.Object.Int, int64(100), "int64")
assert.Equal(t, v.Object.Float, 100.1, "float64")
assert.Equal(t, v.Object.String, "text", "string")
}
func testWatch(t *testing.T, cli Client) {
ch := cli.WatchEvent(context.Background())
select {
case <-time.After(time.Second):
t.Log("watch timeout")
case e := <-ch:
s, err := cli.Get("static").String()
assert.Nil(t, err)
assert.Equal(t, s, e.Value, "watch value")
t.Logf("watch event:%+v", e)
}
}

View File

@@ -0,0 +1,68 @@
package paladin
import (
"reflect"
"strconv"
"github.com/naoina/toml"
"github.com/pkg/errors"
)
// TOML is toml map.
type TOML = Map
// Set set the map by value.
func (m *TOML) Set(text string) error {
if err := m.UnmarshalText([]byte(text)); err != nil {
return err
}
return nil
}
// UnmarshalText implemented toml.
func (m *TOML) UnmarshalText(text []byte) error {
raws := map[string]interface{}{}
if err := toml.Unmarshal(text, &raws); err != nil {
return err
}
values := map[string]*Value{}
for k, v := range raws {
k = keyNamed(k)
rv := reflect.ValueOf(v)
switch rv.Kind() {
case reflect.Map:
b, err := toml.Marshal(v)
if err != nil {
return err
}
// NOTE: value is map[string]interface{}
values[k] = &Value{val: v, raw: string(b)}
case reflect.Slice:
raw := map[string]interface{}{
k: v,
}
b, err := toml.Marshal(raw)
if err != nil {
return err
}
// NOTE: value is []interface{}
values[k] = &Value{val: v, raw: string(b)}
case reflect.Bool:
b := v.(bool)
values[k] = &Value{val: b, raw: strconv.FormatBool(b)}
case reflect.Int64:
i := v.(int64)
values[k] = &Value{val: i, raw: strconv.FormatInt(i, 10)}
case reflect.Float64:
f := v.(float64)
values[k] = &Value{val: f, raw: strconv.FormatFloat(f, 'f', -1, 64)}
case reflect.String:
s := v.(string)
values[k] = &Value{val: s, raw: s}
default:
return errors.Errorf("UnmarshalTOML: unknown kind(%v)", rv.Kind())
}
}
m.Store(values)
return nil
}

View File

@@ -0,0 +1,170 @@
package paladin
import (
"encoding"
"reflect"
"time"
"github.com/BurntSushi/toml"
"github.com/pkg/errors"
)
// ErrNotExist value key not exist.
var (
ErrNotExist = errors.New("paladin: value key not exist")
ErrTypeAssertion = errors.New("paladin: value type assertion no match")
ErrDifferentTypes = errors.New("paladin: value different types")
)
// Value is config value, maybe a json/toml/ini/string file.
type Value struct {
val interface{}
slice interface{}
raw string
}
// Bool return bool value.
func (v *Value) Bool() (bool, error) {
if v.val == nil {
return false, ErrNotExist
}
b, ok := v.val.(bool)
if !ok {
return false, ErrTypeAssertion
}
return b, nil
}
// Int return int value.
func (v *Value) Int() (int, error) {
i, err := v.Int64()
if err != nil {
return 0, nil
}
return int(i), nil
}
// Int32 return int32 value.
func (v *Value) Int32() (int32, error) {
i, err := v.Int64()
if err != nil {
return 0, nil
}
return int32(i), nil
}
// Int64 return int64 value.
func (v *Value) Int64() (int64, error) {
if v.val == nil {
return 0, ErrNotExist
}
i, ok := v.val.(int64)
if !ok {
return 0, ErrTypeAssertion
}
return i, nil
}
// Float32 return float32 value.
func (v *Value) Float32() (float32, error) {
f, err := v.Float64()
if err != nil {
return 0.0, err
}
return float32(f), nil
}
// Float64 return float64 value.
func (v *Value) Float64() (float64, error) {
if v.val == nil {
return 0.0, ErrNotExist
}
f, ok := v.val.(float64)
if !ok {
return 0.0, ErrTypeAssertion
}
return f, nil
}
// String return string value.
func (v *Value) String() (string, error) {
if v.val == nil {
return "", ErrNotExist
}
s, ok := v.val.(string)
if !ok {
return "", ErrTypeAssertion
}
return s, nil
}
// Duration parses a duration string. A duration string is a possibly signed sequence of decimal numbers
// each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
func (v *Value) Duration() (time.Duration, error) {
s, err := v.String()
if err != nil {
return time.Duration(0), err
}
return time.ParseDuration(s)
}
// Raw return raw value.
func (v *Value) Raw() (string, error) {
if v.val == nil {
return "", ErrNotExist
}
return v.raw, nil
}
// Slice scan a slcie interface.
func (v *Value) Slice(dst interface{}) error {
// NOTE: val is []interface{}, slice is []type
if v.val == nil {
return ErrNotExist
}
rv := reflect.ValueOf(dst)
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice {
return ErrDifferentTypes
}
el := rv.Elem()
kind := el.Type().Elem().Kind()
if v.slice == nil {
src, ok := v.val.([]interface{})
if !ok {
return ErrDifferentTypes
}
for _, s := range src {
if reflect.TypeOf(s).Kind() != kind {
return ErrTypeAssertion
}
el = reflect.Append(el, reflect.ValueOf(s))
}
v.slice = el.Interface()
rv.Elem().Set(el)
return nil
}
sv := reflect.ValueOf(v.slice)
if sv.Type().Elem().Kind() != kind {
return ErrTypeAssertion
}
rv.Elem().Set(sv)
return nil
}
// Unmarshal is the interface implemented by an object that can unmarshal a textual representation of itself.
func (v *Value) Unmarshal(un encoding.TextUnmarshaler) error {
text, err := v.Raw()
if err != nil {
return err
}
return un.UnmarshalText([]byte(text))
}
// UnmarshalTOML unmarhsal toml to struct.
func (v *Value) UnmarshalTOML(dst interface{}) error {
text, err := v.Raw()
if err != nil {
return err
}
return toml.Unmarshal([]byte(text), dst)
}

View File

@@ -0,0 +1,206 @@
package paladin
import (
"fmt"
"testing"
"time"
"github.com/stretchr/testify/assert"
)
type testUnmarshler struct {
Text string
Int int
}
func TestValueUnmarshal(t *testing.T) {
s := `
int = 100
text = "hello"
`
v := Value{val: s, raw: s}
obj := new(testUnmarshler)
assert.Nil(t, v.UnmarshalTOML(obj))
// error
v = Value{val: nil, raw: ""}
assert.NotNil(t, v.UnmarshalTOML(obj))
}
func TestValue(t *testing.T) {
var tests = []struct {
in interface{}
out interface{}
}{
{
"text",
"text",
},
{
time.Duration(time.Second * 10),
"10s",
},
{
int64(100),
int64(100),
},
{
float64(100.1),
float64(100.1),
},
{
true,
true,
},
{
nil,
nil,
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.in), func(t *testing.T) {
v := Value{val: test.in, raw: fmt.Sprint(test.in)}
switch test.in.(type) {
case nil:
s, err := v.String()
assert.NotNil(t, err)
assert.Equal(t, s, "", test.in)
i, err := v.Int64()
assert.NotNil(t, err)
assert.Equal(t, i, int64(0), test.in)
f, err := v.Float64()
assert.NotNil(t, err)
assert.Equal(t, f, float64(0.0), test.in)
b, err := v.Bool()
assert.NotNil(t, err)
assert.Equal(t, b, false, test.in)
case string:
val, err := v.String()
assert.Nil(t, err)
assert.Equal(t, val, test.out.(string), test.in)
case int64:
val, err := v.Int()
assert.Nil(t, err)
assert.Equal(t, val, int(test.out.(int64)), test.in)
val32, err := v.Int32()
assert.Nil(t, err)
assert.Equal(t, val32, int32(test.out.(int64)), test.in)
val64, err := v.Int64()
assert.Nil(t, err)
assert.Equal(t, val64, test.out.(int64), test.in)
case float64:
val32, err := v.Float32()
assert.Nil(t, err)
assert.Equal(t, val32, float32(test.out.(float64)), test.in)
val64, err := v.Float64()
assert.Nil(t, err)
assert.Equal(t, val64, test.out.(float64), test.in)
case bool:
val, err := v.Bool()
assert.Nil(t, err)
assert.Equal(t, val, test.out.(bool), test.in)
case time.Duration:
v.val = test.out
val, err := v.Duration()
assert.Nil(t, err)
assert.Equal(t, val, test.in.(time.Duration), test.out)
}
})
}
}
func TestValueSlice(t *testing.T) {
var tests = []struct {
in interface{}
out interface{}
}{
{
nil,
nil,
},
{
[]interface{}{"a", "b", "c"},
[]string{"a", "b", "c"},
},
{
[]interface{}{1, 2, 3},
[]int64{1, 2, 3},
},
{
[]interface{}{1.1, 1.2, 1.3},
[]float64{1.1, 1.2, 1.3},
},
{
[]interface{}{true, false, true},
[]bool{true, false, true},
},
}
for _, test := range tests {
t.Run(fmt.Sprint(test.in), func(t *testing.T) {
v := Value{val: test.in, raw: fmt.Sprint(test.in)}
switch test.in.(type) {
case nil:
var s []string
assert.NotNil(t, v.Slice(&s))
case []string:
var s []string
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []int64:
var s []int64
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []float64:
var s []float64
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
case []bool:
var s []bool
assert.Nil(t, v.Slice(&s))
assert.Equal(t, s, test.out)
}
})
}
}
func BenchmarkValueInt(b *testing.B) {
v := &Value{val: int64(100), raw: "100"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Int64()
}
})
}
func BenchmarkValueFloat(b *testing.B) {
v := &Value{val: float64(100.1), raw: "100.1"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Float64()
}
})
}
func BenchmarkValueBool(b *testing.B) {
v := &Value{val: true, raw: "true"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.Bool()
}
})
}
func BenchmarkValueString(b *testing.B) {
v := &Value{val: "text", raw: "text"}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
v.String()
}
})
}
func BenchmarkValueSlice(b *testing.B) {
v := &Value{val: []interface{}{1, 2, 3}, raw: "100"}
b.RunParallel(func(pb *testing.PB) {
var slice []int64
for pb.Next() {
v.Slice(&slice)
}
})
}