From c90108b14f1590b430662cfbd3ca2ff0c86720c4 Mon Sep 17 00:00:00 2001 From: colstuwjx Date: Tue, 17 Sep 2019 18:53:24 +0800 Subject: [PATCH] feat: introduced apollo for paladin with driver registration. --- go.mod | 2 + go.sum | 2 + pkg/conf/paladin/README.md | 74 ++++- pkg/conf/paladin/apollo/apollo.go | 273 ++++++++++++++++++ pkg/conf/paladin/apollo/apollo_test.go | 73 +++++ pkg/conf/paladin/apollo/const.go | 6 + .../apollo/internal/mockserver/mockserver.go | 149 ++++++++++ pkg/conf/paladin/default.go | 25 +- pkg/conf/paladin/driver.go | 9 + pkg/conf/paladin/example_test.go | 39 ++- pkg/conf/paladin/file.go | 4 +- pkg/conf/paladin/map.go | 10 +- pkg/conf/paladin/register.go | 55 ++++ pkg/conf/paladin/toml.go | 2 +- pkg/conf/paladin/value.go | 11 +- 15 files changed, 716 insertions(+), 18 deletions(-) create mode 100644 pkg/conf/paladin/apollo/apollo.go create mode 100644 pkg/conf/paladin/apollo/apollo_test.go create mode 100644 pkg/conf/paladin/apollo/const.go create mode 100644 pkg/conf/paladin/apollo/internal/mockserver/mockserver.go create mode 100644 pkg/conf/paladin/driver.go create mode 100644 pkg/conf/paladin/register.go diff --git a/go.mod b/go.mod index ce078a8e7..e3e0faf48 100644 --- a/go.mod +++ b/go.mod @@ -26,9 +26,11 @@ require ( github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/leodido/go-urn v1.1.0 // indirect github.com/mattn/go-colorable v0.1.2 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/montanaflynn/stats v0.5.0 github.com/openzipkin/zipkin-go v0.2.1 github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3 + github.com/philchia/agollo v2.3.1+incompatible github.com/pkg/errors v0.8.1 github.com/prometheus/client_golang v1.1.0 github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect diff --git a/go.sum b/go.sum index 1f489a404..206af69b1 100644 --- a/go.sum +++ b/go.sum @@ -171,6 +171,8 @@ github.com/openzipkin/zipkin-go v0.2.1 h1:noL5/5Uf1HpVl3wNsfkZhIKbSWCVi5jgqkONNx github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3 h1:zjmNboC3QFuMdJSaZJ7Qvi3HUxWXPdj7wb3rc4jH5HI= github.com/otokaze/mock v0.0.0-20190125081256-8282b7a7c7c3/go.mod h1:pLR8n2aimFxvvDJ6n8JuQWthMGezCYMjuhlaTjPTZf0= +github.com/philchia/agollo v2.3.1+incompatible h1:C4zDDuOcP1Qynikz2rSJQSMjwexv4GfDpwBHJRinhPc= +github.com/philchia/agollo v2.3.1+incompatible/go.mod h1:EXNdWdQkS+QBi0nb/Xm+sBBuQ1PM7/NIPr1JDzOlt8A= github.com/pierrec/lz4 v0.0.0-20190327172049-315a67e90e41/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= diff --git a/pkg/conf/paladin/README.md b/pkg/conf/paladin/README.md index 6db087389..a49a330ba 100644 --- a/pkg/conf/paladin/README.md +++ b/pkg/conf/paladin/README.md @@ -2,17 +2,18 @@ ##### 项目简介 -paladin 是一个config SDK客户端,包括了file、mock几个抽象功能,方便使用本地文件或者sven配置中心,并且集成了对象自动reload功能。 - +paladin 是一个config SDK客户端,包括了file、mock几个抽象功能,方便使用本地文件或者sven\apollo配置中心,并且集成了对象自动reload功能。 local files: ``` demo -conf=/data/conf/app/msm-servie.toml // or dir demo -conf=/data/conf/app/ - ``` -example: + +*注:使用远程配置中心的用户在执行应用,如这里的`demo`时务必**不要**带上`-conf`参数,具体见下文远程配置中心的例子* + +local file example: ``` type exampleConf struct { Bool bool @@ -65,6 +66,71 @@ func ExampleClient() { } ``` +remote config center example: +``` +type exampleConf struct { + Bool bool + Int int64 + Float float64 + String string +} + +func (e *exampleConf) Set(text string) error { + var ec exampleConf + if err := yaml.Unmarshal([]byte(text), &ec); err != nil { + return err + } + *e = ec + return nil +} + +func ExampleApolloClient() { + /* + pass flags or set envs that apollo needs, for example: + + ``` + export APOLLO_APP_ID=SampleApp + export APOLLO_CLUSTER=default + export APOLLO_CACHE_DIR=/tmp + export APOLLO_META_ADDR=localhost:8080 + export APOLLO_NAMESPACES=example.yml + ``` + */ + + if err := paladin.Init(apollo.PaladinDriverApollo); err != nil { + panic(err) + } + var ( + ec exampleConf + eo exampleConf + m paladin.Map + strs []string + ) + // config unmarshal + if err := paladin.Get("example.yml").UnmarshalYAML(&ec); err != nil { + panic(err) + } + // config setter + if err := paladin.Watch("example.yml", &ec); err != nil { + panic(err) + } + // paladin map + if err := paladin.Watch("example.yml", &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.12.x 以上版本编译执行** diff --git a/pkg/conf/paladin/apollo/apollo.go b/pkg/conf/paladin/apollo/apollo.go new file mode 100644 index 000000000..0ce83f9de --- /dev/null +++ b/pkg/conf/paladin/apollo/apollo.go @@ -0,0 +1,273 @@ +package apollo + +import ( + "context" + "errors" + "flag" + "log" + "os" + "strings" + "sync" + "time" + + "github.com/philchia/agollo" + + "github.com/bilibili/kratos/pkg/conf/paladin" +) + +var ( + _ paladin.Client = &apollo{} + defaultValue = "" +) + +type apolloWatcher struct { + keys []string // in apollo, they're called namespaces + C chan paladin.Event +} + +func newApolloWatcher(keys []string) *apolloWatcher { + return &apolloWatcher{keys: keys, C: make(chan paladin.Event, 5)} +} + +func (aw *apolloWatcher) HasKey(key string) bool { + if len(aw.keys) == 0 { + return true + } + for _, k := range aw.keys { + if k == key { + return true + } + } + return false +} + +func (aw *apolloWatcher) Handle(event paladin.Event) { + select { + case aw.C <- event: + default: + log.Printf("paladin: event channel full discard ns %s update event", event.Key) + } +} + +// apollo is apollo config client. +type apollo struct { + client *agollo.Client + values *paladin.Map + wmu sync.RWMutex + watchers map[*apolloWatcher]struct{} +} + +// Config is apollo config client config. +type Config struct { + AppID string `json:"app_id"` + Cluster string `json:"cluster"` + CacheDir string `json:"cache_dir"` + MetaAddr string `json:"meta_addr"` + Namespaces []string `json:"namespaces"` +} + +type apolloDriver struct{} + +var ( + confAppID, confCluster, confCacheDir, confMetaAddr, confNamespaces string +) + +func init() { + addApolloFlags() + paladin.Register(PaladinDriverApollo, &apolloDriver{}) +} + +func addApolloFlags() { + flag.StringVar(&confAppID, "apollo.appid", "", "apollo app id") + flag.StringVar(&confCluster, "apollo.cluster", "", "apollo cluster") + flag.StringVar(&confCacheDir, "apollo.cachedir", "/tmp", "apollo cache dir") + flag.StringVar(&confMetaAddr, "apollo.metaaddr", "", "apollo meta server addr, e.g. localhost:8080") + flag.StringVar(&confNamespaces, "apollo.namespaces", "", "subscribed apollo namespaces, comma separated, e.g. app.yml,mysql.yml") +} + +func buildConfigForApollo() (c *Config, err error) { + if appidFromEnv := os.Getenv("APOLLO_APP_ID"); appidFromEnv != "" { + confAppID = appidFromEnv + } + if confAppID == "" { + err = errors.New("invalid apollo appid, pass it via APOLLO_APP_ID=xxx with env or --apollo.appid=xxx with flag") + return + } + if clusterFromEnv := os.Getenv("APOLLO_CLUSTER"); clusterFromEnv != "" { + confCluster = clusterFromEnv + } + if confAppID == "" { + err = errors.New("invalid apollo cluster, pass it via APOLLO_CLUSTER=xxx with env or --apollo.cluster=xxx with flag") + return + } + if cacheDirFromEnv := os.Getenv("APOLLO_CACHE_DIR"); cacheDirFromEnv != "" { + confCacheDir = cacheDirFromEnv + } + if metaAddrFromEnv := os.Getenv("APOLLO_META_ADDR"); metaAddrFromEnv != "" { + confMetaAddr = metaAddrFromEnv + } + if confMetaAddr == "" { + err = errors.New("invalid apollo meta addr, pass it via APOLLO_META_ADDR=xxx with env or --apollo.metaaddr=xxx with flag") + return + } + if namespacesFromEnv := os.Getenv("APOLLO_NAMESPACES"); namespacesFromEnv != "" { + confNamespaces = namespacesFromEnv + } + namespaceNames := strings.Split(confNamespaces, ",") + if len(namespaceNames) == 0 { + err = errors.New("invalid apollo namespaces, pass it via APOLLO_NAMESPACES=xxx with env or --apollo.namespaces=xxx with flag") + return + } + c = &Config{ + AppID: confAppID, + Cluster: confCluster, + CacheDir: confCacheDir, + MetaAddr: confMetaAddr, + Namespaces: namespaceNames, + } + return +} + +// New new an apollo config client. +// it watches apollo namespaces changes and updates local cache. +// BTW, in our context, namespaces in apollo means keys in paladin. +func (ad *apolloDriver) New() (paladin.Client, error) { + c, err := buildConfigForApollo() + if err != nil { + return nil, err + } + return ad.new(c) +} + +func (ad *apolloDriver) new(conf *Config) (paladin.Client, error) { + if conf == nil { + err := errors.New("invalid apollo conf") + return nil, err + } + client := agollo.NewClient(&agollo.Conf{ + AppID: conf.AppID, + Cluster: conf.Cluster, + NameSpaceNames: conf.Namespaces, // these namespaces will be subscribed at init + CacheDir: conf.CacheDir, + IP: conf.MetaAddr, + }) + err := client.Start() + if err != nil { + return nil, err + } + a := &apollo{ + client: client, + values: new(paladin.Map), + watchers: make(map[*apolloWatcher]struct{}), + } + raws, err := a.loadValues(conf.Namespaces) + if err != nil { + return nil, err + } + a.values.Store(raws) + // watch namespaces by default. + a.WatchEvent(context.TODO(), conf.Namespaces...) + go a.watchproc(conf.Namespaces) + return a, nil +} + +// loadValues load values from apollo namespaces to values +func (a *apollo) loadValues(keys []string) (values map[string]*paladin.Value, err error) { + values = make(map[string]*paladin.Value, len(keys)) + for _, k := range keys { + if values[k], err = a.loadValue(k); err != nil { + return + } + } + return +} + +// loadValue load value from apollo namespace content to value +func (a *apollo) loadValue(key string) (*paladin.Value, error) { + content := a.client.GetNameSpaceContent(key, defaultValue) + return paladin.NewValue(content, content), nil +} + +// reloadValue reload value by key and send event +func (a *apollo) reloadValue(key string) (err error) { + // NOTE: in some case immediately read content from client after receive event + // will get old content due to cache, sleep 100ms make sure get correct content. + time.Sleep(100 * time.Millisecond) + var ( + value *paladin.Value + rawValue string + ) + value, err = a.loadValue(key) + if err != nil { + return + } + rawValue, err = value.Raw() + if err != nil { + return + } + raws := a.values.Load() + raws[key] = value + a.values.Store(raws) + a.wmu.RLock() + n := 0 + for w := range a.watchers { + if w.HasKey(key) { + n++ + // FIXME(Colstuwjx): check change event and send detail type like EventAdd\Update\Delete. + w.Handle(paladin.Event{Event: paladin.EventUpdate, Key: key, Value: rawValue}) + } + } + a.wmu.RUnlock() + log.Printf("paladin: reload config: %s events: %d\n", key, n) + return +} + +// apollo config daemon to watch remote apollo notifications +func (a *apollo) watchproc(keys []string) { + events := a.client.WatchUpdate() + for { + select { + case event := <-events: + if err := a.reloadValue(event.Namespace); err != nil { + log.Printf("paladin: load key: %s error: %s, skipped", event.Namespace, err) + } + } + } +} + +// Get return value by key. +func (a *apollo) Get(key string) *paladin.Value { + return a.values.Get(key) +} + +// GetAll return value map. +func (a *apollo) GetAll() *paladin.Map { + return a.values +} + +// WatchEvent watch with the specified keys. +func (a *apollo) WatchEvent(ctx context.Context, keys ...string) <-chan paladin.Event { + aw := newApolloWatcher(keys) + err := a.client.SubscribeToNamespaces(keys...) + if err != nil { + log.Printf("subscribe namespaces %v failed, %v", keys, err) + return aw.C + } + a.wmu.Lock() + a.watchers[aw] = struct{}{} + a.wmu.Unlock() + return aw.C +} + +// Close close watcher. +func (a *apollo) Close() (err error) { + if err = a.client.Stop(); err != nil { + return + } + a.wmu.RLock() + for w := range a.watchers { + close(w.C) + } + a.wmu.RUnlock() + return +} diff --git a/pkg/conf/paladin/apollo/apollo_test.go b/pkg/conf/paladin/apollo/apollo_test.go new file mode 100644 index 000000000..cef79cb10 --- /dev/null +++ b/pkg/conf/paladin/apollo/apollo_test.go @@ -0,0 +1,73 @@ +package apollo + +import ( + "context" + "fmt" + "log" + "os" + "testing" + "time" + + "github.com/bilibili/kratos/pkg/conf/paladin/apollo/internal/mockserver" +) + +func TestMain(m *testing.M) { + setup() + code := m.Run() + teardown() + os.Exit(code) +} + +func setup() { + go func() { + if err := mockserver.Run(); err != nil { + log.Fatal(err) + } + }() + // wait for mock server to run + time.Sleep(time.Millisecond * 500) +} + +func teardown() { + mockserver.Close() +} + +func TestApollo(t *testing.T) { + var ( + testAppYAML = "app.yml" + testAppYAMLContent1 = "test: test12234\ntest2: test333" + testAppYAMLContent2 = "test: 1111" + testClientJSON = "client.json" + testClientJSONContent = `{"name":"agollo"}` + ) + os.Setenv("APOLLO_APP_ID", "SampleApp") + os.Setenv("APOLLO_CLUSTER", "default") + os.Setenv("APOLLO_CACHE_DIR", "/tmp") + os.Setenv("APOLLO_META_ADDR", "localhost:8080") + os.Setenv("APOLLO_NAMESPACES", fmt.Sprintf("%s,%s", testAppYAML, testClientJSON)) + mockserver.Set(testAppYAML, "content", testAppYAMLContent1) + mockserver.Set(testClientJSON, "content", testClientJSONContent) + ad := &apolloDriver{} + apollo, err := ad.New() + if err != nil { + t.Fatalf("new apollo error, %v", err) + } + value := apollo.Get(testAppYAML) + if content, _ := value.String(); content != testAppYAMLContent1 { + t.Fatalf("got app.yml unexpected value %s", content) + } + value = apollo.Get(testClientJSON) + if content, _ := value.String(); content != testClientJSONContent { + t.Fatalf("got app.yml unexpected value %s", content) + } + mockserver.Set(testAppYAML, "content", testAppYAMLContent2) + updates := apollo.WatchEvent(context.TODO(), testAppYAML) + select { + case <-updates: + case <-time.After(time.Millisecond * 30000): + } + value = apollo.Get(testAppYAML) + if content, _ := value.String(); content != testAppYAMLContent2 { + t.Fatalf("got app.yml unexpected updated value %s", content) + } +} diff --git a/pkg/conf/paladin/apollo/const.go b/pkg/conf/paladin/apollo/const.go new file mode 100644 index 000000000..1aac397fe --- /dev/null +++ b/pkg/conf/paladin/apollo/const.go @@ -0,0 +1,6 @@ +package apollo + +const ( + // PaladinDriverApollo ... + PaladinDriverApollo = "apollo" +) diff --git a/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go b/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go new file mode 100644 index 000000000..5ed0bc2ba --- /dev/null +++ b/pkg/conf/paladin/apollo/internal/mockserver/mockserver.go @@ -0,0 +1,149 @@ +package mockserver + +import ( + "context" + "encoding/json" + "net/http" + "strings" + "sync" + "time" +) + +type notification struct { + NamespaceName string `json:"namespaceName,omitempty"` + NotificationID int `json:"notificationId,omitempty"` +} + +type result struct { + // AppID string `json:"appId"` + // Cluster string `json:"cluster"` + NamespaceName string `json:"namespaceName"` + Configurations map[string]string `json:"configurations"` + ReleaseKey string `json:"releaseKey"` +} + +type mockServer struct { + server http.Server + + lock sync.Mutex + notifications map[string]int + config map[string]map[string]string +} + +func (s *mockServer) NotificationHandler(rw http.ResponseWriter, req *http.Request) { + s.lock.Lock() + defer s.lock.Unlock() + req.ParseForm() + var notifications []notification + if err := json.Unmarshal([]byte(req.FormValue("notifications")), ¬ifications); err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + var changes []notification + for _, noti := range notifications { + if currentID := s.notifications[noti.NamespaceName]; currentID != noti.NotificationID { + changes = append(changes, notification{NamespaceName: noti.NamespaceName, NotificationID: currentID}) + } + } + + if len(changes) == 0 { + rw.WriteHeader(http.StatusNotModified) + return + } + bts, err := json.Marshal(&changes) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.Write(bts) +} + +func (s *mockServer) ConfigHandler(rw http.ResponseWriter, req *http.Request) { + req.ParseForm() + + strs := strings.Split(req.RequestURI, "/") + var namespace, releaseKey = strings.Split(strs[4], "?")[0], req.FormValue("releaseKey") + config := s.Get(namespace) + + var result = result{NamespaceName: namespace, Configurations: config, ReleaseKey: releaseKey} + bts, err := json.Marshal(&result) + if err != nil { + rw.WriteHeader(http.StatusInternalServerError) + return + } + rw.Write(bts) +} + +var server *mockServer + +func (s *mockServer) Set(namespace, key, value string) { + server.lock.Lock() + defer server.lock.Unlock() + + notificationID := s.notifications[namespace] + notificationID++ + s.notifications[namespace] = notificationID + + if kv, ok := s.config[namespace]; ok { + kv[key] = value + return + } + kv := map[string]string{key: value} + s.config[namespace] = kv +} + +func (s *mockServer) Get(namespace string) map[string]string { + server.lock.Lock() + defer server.lock.Unlock() + + return s.config[namespace] +} + +func (s *mockServer) Delete(namespace, key string) { + server.lock.Lock() + defer server.lock.Unlock() + + if kv, ok := s.config[namespace]; ok { + delete(kv, key) + } + + notificationID := s.notifications[namespace] + notificationID++ + s.notifications[namespace] = notificationID +} + +// Set namespace's key value +func Set(namespace, key, value string) { + server.Set(namespace, key, value) +} + +// Delete namespace's key +func Delete(namespace, key string) { + server.Delete(namespace, key) +} + +// Run mock server +func Run() error { + initServer() + return server.server.ListenAndServe() +} + +func initServer() { + server = &mockServer{ + notifications: map[string]int{}, + config: map[string]map[string]string{}, + } + mux := http.NewServeMux() + mux.Handle("/notifications/", http.HandlerFunc(server.NotificationHandler)) + mux.Handle("/configs/", http.HandlerFunc(server.ConfigHandler)) + server.server.Handler = mux + server.server.Addr = ":8080" +} + +// Close mock server +func Close() error { + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) + defer cancel() + + return server.server.Shutdown(ctx) +} diff --git a/pkg/conf/paladin/default.go b/pkg/conf/paladin/default.go index 566c83720..a9410f563 100644 --- a/pkg/conf/paladin/default.go +++ b/pkg/conf/paladin/default.go @@ -2,6 +2,7 @@ package paladin import ( "context" + "errors" "flag" ) @@ -16,12 +17,30 @@ func init() { } // Init init config client. -func Init() (err error) { +// If confPath is set, it inits file client by default +// Otherwise we could pass args to init remote client +// args[0]: driver name, string type +func Init(args ...interface{}) (err error) { if confPath != "" { DefaultClient, err = NewFile(confPath) } else { - // TODO: Get the configuration from the remote service - panic("Please specify a file or dir name by -conf flag.") + var ( + driver Driver + ) + argsLackErr := errors.New("lack of remote config center args") + if len(args) == 0 { + panic(argsLackErr.Error()) + } + argsInvalidErr := errors.New("invalid remote config center args") + driverName, ok := args[0].(string) + if !ok { + panic(argsInvalidErr.Error()) + } + driver, err = GetDriver(driverName) + if err != nil { + return + } + DefaultClient, err = driver.New() } if err != nil { return diff --git a/pkg/conf/paladin/driver.go b/pkg/conf/paladin/driver.go new file mode 100644 index 000000000..9f2151e4c --- /dev/null +++ b/pkg/conf/paladin/driver.go @@ -0,0 +1,9 @@ +package paladin + +// Driver defined paladin remote client impl +// each remote config center driver must do +// 1. implements `New` method +// 2. call `Register` to register itself +type Driver interface { + New() (Client, error) +} diff --git a/pkg/conf/paladin/example_test.go b/pkg/conf/paladin/example_test.go index d3d365457..110e402b8 100644 --- a/pkg/conf/paladin/example_test.go +++ b/pkg/conf/paladin/example_test.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/bilibili/kratos/pkg/conf/paladin" + "github.com/bilibili/kratos/pkg/conf/paladin/apollo" "github.com/BurntSushi/toml" ) @@ -26,7 +27,7 @@ func (e *exampleConf) Set(text string) error { return nil } -// ExampleClient is a example client usage. +// ExampleClient is an example client usage. // exmaple.toml: /* bool = true @@ -56,7 +57,41 @@ func ExampleClient() { }() } -// ExampleMap is a example map usage. +// ExampleApolloClient is an example client for apollo driver usage. +func ExampleApolloClient() { + /* + pass flags or set envs that apollo needs, for example: + + ``` + export APOLLO_APP_ID=SampleApp + export APOLLO_CLUSTER=default + export APOLLO_CACHE_DIR=/tmp + export APOLLO_META_ADDR=localhost:8080 + export APOLLO_NAMESPACES=example.yml + ``` + */ + + if err := paladin.Init(apollo.PaladinDriverApollo); err != nil { + panic(err) + } + var ec exampleConf + // var setter + if err := paladin.Watch("example.yml", &ec); err != nil { + panic(err) + } + if err := paladin.Get("example.yml").UnmarshalYAML(&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 an example map usage. // exmaple.toml: /* bool = true diff --git a/pkg/conf/paladin/file.go b/pkg/conf/paladin/file.go index 0995cf227..7edd6b265 100644 --- a/pkg/conf/paladin/file.go +++ b/pkg/conf/paladin/file.go @@ -31,7 +31,7 @@ func (w *watcher) HasKey(key string) bool { return true } for _, k := range w.keys { - if keyNamed(k) == key { + if KeyNamed(k) == key { return true } } @@ -138,7 +138,7 @@ func (f *file) reloadFile(fpath string) (err error) { if err != nil { return } - key := keyNamed(path.Base(fpath)) + key := KeyNamed(path.Base(fpath)) raws := f.values.Load() raws[key] = value f.values.Store(raws) diff --git a/pkg/conf/paladin/map.go b/pkg/conf/paladin/map.go index c262d4050..fa43dc116 100644 --- a/pkg/conf/paladin/map.go +++ b/pkg/conf/paladin/map.go @@ -5,8 +5,8 @@ import ( "sync/atomic" ) -// keyNamed key naming to lower case. -func keyNamed(key string) string { +// KeyNamed key naming to lower case. +func KeyNamed(key string) string { return strings.ToLower(key) } @@ -19,7 +19,7 @@ type Map struct { func (m *Map) Store(values map[string]*Value) { dst := make(map[string]*Value, len(values)) for k, v := range values { - dst[keyNamed(k)] = v + dst[KeyNamed(k)] = v } m.values.Store(dst) } @@ -36,13 +36,13 @@ func (m *Map) Load() map[string]*Value { // Exist check if values map exist a key. func (m *Map) Exist(key string) bool { - _, ok := m.Load()[keyNamed(key)] + _, 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)] + v, ok := m.Load()[KeyNamed(key)] if ok { return v } diff --git a/pkg/conf/paladin/register.go b/pkg/conf/paladin/register.go new file mode 100644 index 000000000..400497745 --- /dev/null +++ b/pkg/conf/paladin/register.go @@ -0,0 +1,55 @@ +package paladin + +import ( + "fmt" + "sort" + "sync" +) + +var ( + driversMu sync.RWMutex + drivers = make(map[string]Driver) +) + +// Register makes a paladin driver available by the provided name. +// If Register is called twice with the same name or if driver is nil, +// it panics. +func Register(name string, driver Driver) { + driversMu.Lock() + defer driversMu.Unlock() + + if driver == nil { + panic("paladin: driver is nil") + } + + if _, dup := drivers[name]; dup { + panic("paladin: Register called twice for driver " + name) + } + + drivers[name] = driver +} + +// Drivers returns a sorted list of the names of the registered paladin driver. +func Drivers() []string { + driversMu.RLock() + defer driversMu.RUnlock() + + var list []string + for name := range drivers { + list = append(list, name) + } + + sort.Strings(list) + return list +} + +// GetDriver returns a driver implement by name. +func GetDriver(name string) (Driver, error) { + driversMu.RLock() + driveri, ok := drivers[name] + driversMu.RUnlock() + if !ok { + return nil, fmt.Errorf("paladin: unknown driver %q (forgotten import?)", name) + } + return driveri, nil +} diff --git a/pkg/conf/paladin/toml.go b/pkg/conf/paladin/toml.go index 87f92771c..09595fb0b 100644 --- a/pkg/conf/paladin/toml.go +++ b/pkg/conf/paladin/toml.go @@ -28,7 +28,7 @@ func (m *TOML) UnmarshalText(text []byte) error { } values := map[string]*Value{} for k, v := range raws { - k = keyNamed(k) + k = KeyNamed(k) rv := reflect.ValueOf(v) switch rv.Kind() { case reflect.Map: diff --git a/pkg/conf/paladin/value.go b/pkg/conf/paladin/value.go index bafb86b99..733db7df7 100644 --- a/pkg/conf/paladin/value.go +++ b/pkg/conf/paladin/value.go @@ -25,6 +25,14 @@ type Value struct { raw string } +// NewValue new a value +func NewValue(val interface{}, raw string) *Value { + return &Value{ + val: val, + raw: raw, + } +} + // Bool return bool value. func (v *Value) Bool() (bool, error) { if v.val == nil { @@ -112,7 +120,7 @@ func (v *Value) Raw() (string, error) { return v.raw, nil } -// Slice scan a slcie interface, if slice has element it will be discard. +// Slice scan a slice interface, if slice has element it will be discard. func (v *Value) Slice(dst interface{}) error { // NOTE: val is []interface{}, slice is []type if v.val == nil { @@ -167,6 +175,7 @@ func (v *Value) UnmarshalJSON(dst interface{}) error { return json.Unmarshal([]byte(text), dst) } +// UnmarshalYAML unmarshal yaml to struct. func (v *Value) UnmarshalYAML(dst interface{}) error { text, err := v.Raw() if err != nil {