├── Makefile ├── testdata └── app.yml ├── go.mod ├── const.go ├── conf.go ├── logger.go ├── conf_test.go ├── notification_test.go ├── request.go ├── common_test.go ├── common.go ├── LICENSE ├── go.sum ├── request_test.go ├── change_test.go ├── change.go ├── notification.go ├── cache_test.go ├── apollo.go ├── cache.go ├── .gitignore ├── .drone.yml ├── apollo_test.go ├── poller.go ├── README.md ├── internal └── mockserver │ └── mockserver.go └── client.go /Makefile: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /testdata/app.yml: -------------------------------------------------------------------------------- 1 | appId: SampleApp 2 | cluster: default 3 | namespaces: 4 | - application 5 | - notexist 6 | - client.json 7 | ip: 127.0.0.1:8080 -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module gopkg.in/apollo.v0 2 | 3 | require ( 4 | github.com/stretchr/testify v1.3.0 // test 5 | gopkg.in/logger.v1 v1.0.1 6 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae 7 | ) 8 | -------------------------------------------------------------------------------- /const.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultConfName = "app.yml" 11 | defaultDumpFile = ".apollo" 12 | defaultNamespace = "application" 13 | 14 | longPoolInterval = time.Second * 2 15 | longPoolTimeout = time.Second * 90 16 | queryTimeout = time.Second * 2 17 | defaultNotificationID = -1 18 | ) 19 | -------------------------------------------------------------------------------- /conf.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "os" 7 | 8 | "gopkg.in/yaml.v3" 9 | ) 10 | 11 | // Conf ... 12 | type Conf struct { 13 | AppID string `yaml:"appId"` 14 | Cluster string `yaml:"cluster"` 15 | Namespaces []string `yaml:"namespaces,flow"` 16 | IP string `json:"ip"` 17 | } 18 | 19 | // NewConf create Conf from file 20 | func NewConf(name string) (*Conf, error) { 21 | f, err := os.Open(name) 22 | if err != nil { 23 | return nil, err 24 | } 25 | defer f.Close() 26 | 27 | var ret Conf 28 | 29 | if err := yaml.NewDecoder(f).Decode(&ret); err != nil { 30 | return nil, err 31 | } 32 | 33 | return &ret, nil 34 | } 35 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package apollo 2 | 3 | import ( 4 | logger "gopkg.in/logger.v1" 5 | ) 6 | 7 | //Logger interface 8 | type Logger interface { 9 | Warnf(format string, v ...interface{}) 10 | Warn(v ...interface{}) 11 | Errorf(format string, v ...interface{}) 12 | Error(v ...interface{}) 13 | Infof(format string, v ...interface{}) 14 | Info(v ...interface{}) 15 | Debugf(format string, v ...interface{}) 16 | Debug(v ...interface{}) 17 | Fatal(args ...interface{}) 18 | Fatalf(format string, args ...interface{}) 19 | } 20 | 21 | var log Logger 22 | 23 | //SetLogger set user custome logger 24 | func SetLogger(userLog Logger) { 25 | log = userLog 26 | } 27 | 28 | func setDefaultLogger() { 29 | log = logger.Std 30 | } 31 | -------------------------------------------------------------------------------- /conf_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | package apollo 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type TestConfigSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (ts *TestConfigSuite) TestNewConf() { 15 | var tcs = []struct { 16 | name string 17 | wantErr bool 18 | }{ 19 | { 20 | name: "fakename", 21 | wantErr: true, 22 | }, 23 | { 24 | name: "./LICENSE", 25 | wantErr: true, 26 | }, 27 | { 28 | name: "./testdata/" + defaultConfName, 29 | wantErr: false, 30 | }, 31 | } 32 | 33 | for _, tc := range tcs { 34 | _, err := NewConf(tc.name) 35 | if tc.wantErr { 36 | ts.Error(err) 37 | } else { 38 | ts.NoError(err) 39 | } 40 | } 41 | } 42 | 43 | func TestRunConfigSuite(t *testing.T) { 44 | suite.Run(t, new(TestConfigSuite)) 45 | } 46 | -------------------------------------------------------------------------------- /notification_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "testing" 7 | 8 | "github.com/stretchr/testify/suite" 9 | ) 10 | 11 | type NotificationTestSuite struct { 12 | suite.Suite 13 | } 14 | 15 | func (s *NotificationTestSuite) TestNotification() { 16 | repo := new(notificationRepo) 17 | 18 | repo.setNotificationID("namespace", 1) 19 | id, ok := repo.getNotificationID("namespace") 20 | s.True(ok) 21 | s.Equal(1, id) 22 | repo.setNotificationID("namespace", 2) 23 | id2, ok2 := repo.getNotificationID("namespace") 24 | s.True(ok2) 25 | s.Equal(2, id2) 26 | 27 | id3, ok3 := repo.getNotificationID("null") 28 | 29 | s.False(ok3) 30 | s.Equal(defaultNotificationID, id3) 31 | 32 | str := repo.toString() 33 | s.NotEmpty(str) 34 | } 35 | func TestRunNotificationSuite(t *testing.T) { 36 | suite.Run(t, new(NotificationTestSuite)) 37 | } 38 | -------------------------------------------------------------------------------- /request.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | // this is a static check 12 | var _ requester = (*httpRequester)(nil) 13 | 14 | type requester interface { 15 | request(url string) ([]byte, error) 16 | } 17 | 18 | type httpRequester struct { 19 | client *http.Client 20 | } 21 | 22 | func newHTTPRequester(client *http.Client) requester { 23 | return &httpRequester{ 24 | client: client, 25 | } 26 | } 27 | 28 | func (r *httpRequester) request(url string) ([]byte, error) { 29 | resp, err := r.client.Get(url) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer resp.Body.Close() 34 | if resp.StatusCode == http.StatusOK { 35 | body, err := ioutil.ReadAll(resp.Body) 36 | return body, err 37 | } 38 | 39 | // Discard all body if status code is not 200 40 | io.Copy(ioutil.Discard, resp.Body) 41 | return nil, nil 42 | } 43 | -------------------------------------------------------------------------------- /common_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "net/url" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type CommonTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (s *CommonTestSuite) TestLocalIp() { 17 | ip := getLocalIP() 18 | s.NotEmpty(ip) 19 | } 20 | 21 | func (s *CommonTestSuite) TestNotificationURL() { 22 | target := notificationURL( 23 | &Conf{ 24 | IP: "127.0.0.1:8080", 25 | AppID: "SampleApp", 26 | Cluster: "default", 27 | }, "") 28 | _, err := url.Parse(target) 29 | s.NoError(err) 30 | } 31 | 32 | func (s *CommonTestSuite) TestConfigURL() { 33 | target := configURL( 34 | &Conf{ 35 | IP: "127.0.0.1:8080", 36 | AppID: "SampleApp", 37 | Cluster: "default", 38 | }, "application", "") 39 | _, err := url.Parse(target) 40 | s.NoError(err) 41 | } 42 | 43 | func TestRunCommonSuite(t *testing.T) { 44 | cs := new(CommonTestSuite) 45 | suite.Run(t, cs) 46 | } 47 | -------------------------------------------------------------------------------- /common.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "fmt" 7 | "net" 8 | "net/url" 9 | ) 10 | 11 | func getLocalIP() string { 12 | addrs, err := net.InterfaceAddrs() 13 | if err != nil { 14 | return "" 15 | } 16 | for _, a := range addrs { 17 | if ip4 := toIP4(a); ip4 != nil { 18 | return ip4.String() 19 | } 20 | } 21 | return "" 22 | } 23 | 24 | func toIP4(addr net.Addr) net.IP { 25 | if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 26 | return ipnet.IP.To4() 27 | } 28 | return nil 29 | } 30 | 31 | func notificationURL(conf *Conf, notifications string) string { 32 | return fmt.Sprintf("http://%s/notifications/v2?appId=%s&cluster=%s¬ifications=%s", 33 | conf.IP, 34 | url.QueryEscape(conf.AppID), 35 | url.QueryEscape(conf.Cluster), 36 | url.QueryEscape(notifications)) 37 | } 38 | 39 | func configURL(conf *Conf, namespace string, releaseKey string) string { 40 | return fmt.Sprintf("http://%s/configs/%s/%s/%s?releaseKey=%s&ip=%s", 41 | conf.IP, 42 | url.QueryEscape(conf.AppID), 43 | url.QueryEscape(conf.Cluster), 44 | url.QueryEscape(namespace), 45 | releaseKey, 46 | getLocalIP()) 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 go-apollo Authors 4 | Copyright (c) 2017 Phil 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 9 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 10 | gopkg.in/logger.v1 v1.0.1 h1:9icQ68EIU1UkD1k0l/vnokOvbtNM11V6f73+/NdPnfM= 11 | gopkg.in/logger.v1 v1.0.1/go.mod h1:HxIq/YRWNOsNazORTgNrNHFJ15sSREEJH9HL0Oz5x1k= 12 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae h1:ehhBuCxzgQEGk38YjhFv/97fMIc2JGHZAhAWMmEjmu0= 13 | gopkg.in/yaml.v3 v3.0.0-20190502103701-55513cacd4ae/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 14 | -------------------------------------------------------------------------------- /request_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type TestRequestSuite struct { 14 | suite.Suite 15 | } 16 | 17 | func (ts *TestRequestSuite) TestRequest() { 18 | request := newHTTPRequester(&http.Client{}) 19 | 20 | serv := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 21 | rw.Write([]byte("test")) 22 | })) 23 | 24 | bts, err := request.request(serv.URL) 25 | ts.NoError(err) 26 | 27 | ts.Equal(bts, []byte("test")) 28 | 29 | serv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 30 | rw.WriteHeader(http.StatusInternalServerError) 31 | })) 32 | bts, err = request.request(serv.URL) 33 | ts.NoError(err) 34 | 35 | ts.Empty(bts) 36 | 37 | serv = httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 38 | rw.WriteHeader(http.StatusInternalServerError) 39 | })) 40 | serv.Close() 41 | _, err = request.request(serv.URL) 42 | ts.Error(err) 43 | } 44 | 45 | func TestRunRequestSuite(t *testing.T) { 46 | ts := new(TestRequestSuite) 47 | suite.Run(t, ts) 48 | } 49 | -------------------------------------------------------------------------------- /change_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | package apollo 3 | 4 | import ( 5 | "testing" 6 | 7 | "github.com/stretchr/testify/suite" 8 | ) 9 | 10 | type ChangeTestSuite struct { 11 | suite.Suite 12 | } 13 | 14 | func (s *ChangeTestSuite) TestChangeType() { 15 | var tps = []ChangeType{ADD, MODIFY, DELETE, ChangeType(-1)} 16 | var strs = []string{"ADD", "MODIFY", "DELETE", "UNKNOW"} 17 | for i, tp := range tps { 18 | s.True(tp.String() == strs[i]) 19 | } 20 | } 21 | 22 | func (s *ChangeTestSuite) TestMakeDeleteChange() { 23 | change := makeDeleteChange("key", []byte("val")) 24 | s.True(change.ChangeType == DELETE) 25 | s.True(string(change.OldValue) == "val") 26 | } 27 | 28 | func (s *ChangeTestSuite) TestMakeModifyChange() { 29 | change := makeModifyChange("key", []byte("old"), []byte("new")) 30 | s.True(change.ChangeType == MODIFY) 31 | s.True(string(change.OldValue) == "old") 32 | s.True(string(change.NewValue) == "new") 33 | } 34 | 35 | func (s *ChangeTestSuite) TestMakeAddChange() { 36 | change := makeAddChange("key", []byte("value")) 37 | s.True(change.ChangeType == ADD) 38 | s.True(string(change.NewValue) == "value") 39 | 40 | } 41 | 42 | func TestRunChangeSuite(t *testing.T) { 43 | suite.Run(t, new(ChangeTestSuite)) 44 | } 45 | -------------------------------------------------------------------------------- /change.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | // ChangeType for a key 6 | type ChangeType int 7 | 8 | const ( 9 | // ADD a new value 10 | ADD ChangeType = iota 11 | // MODIFY a old value 12 | MODIFY 13 | // DELETE ... 14 | DELETE 15 | ) 16 | 17 | func (c ChangeType) String() string { 18 | switch c { 19 | case ADD: 20 | return "ADD" 21 | case MODIFY: 22 | return "MODIFY" 23 | case DELETE: 24 | return "DELETE" 25 | } 26 | 27 | return "UNKNOW" 28 | } 29 | 30 | // ChangeEvent change event 31 | type ChangeEvent struct { 32 | Namespace string 33 | Changes map[string]*Change 34 | } 35 | 36 | // Change represent a single key change 37 | type Change struct { 38 | OldValue []byte 39 | NewValue []byte 40 | ChangeType ChangeType 41 | } 42 | 43 | func makeDeleteChange(_ string, value []byte) *Change { 44 | return &Change{ 45 | ChangeType: DELETE, 46 | OldValue: value, 47 | } 48 | } 49 | 50 | func makeModifyChange(_ string, oldValue, newValue []byte) *Change { 51 | return &Change{ 52 | ChangeType: MODIFY, 53 | OldValue: oldValue, 54 | NewValue: newValue, 55 | } 56 | } 57 | 58 | func makeAddChange(_ string, value []byte) *Change { 59 | return &Change{ 60 | ChangeType: ADD, 61 | NewValue: value, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /notification.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "encoding/json" 7 | "sync" 8 | ) 9 | 10 | type notification struct { 11 | NamespaceName string `json:"namespaceName,omitempty"` 12 | NotificationID int `json:"notificationId,omitempty"` 13 | } 14 | 15 | type notificationRepo struct { 16 | notifications sync.Map 17 | } 18 | 19 | func (n *notificationRepo) setNotificationID(namesapce string, notificationID int) { 20 | n.notifications.Store(namesapce, notificationID) 21 | } 22 | 23 | func (n *notificationRepo) getNotificationID(namespace string) (int, bool) { 24 | if val, ok := n.notifications.Load(namespace); ok { 25 | if ret, ok := val.(int); ok { 26 | return ret, true 27 | } 28 | } 29 | 30 | return defaultNotificationID, false 31 | } 32 | 33 | func (n *notificationRepo) toString() string { 34 | var notifications []*notification 35 | n.notifications.Range(func(key, val interface{}) bool { 36 | k, _ := key.(string) 37 | v, _ := val.(int) 38 | notifications = append(notifications, ¬ification{ 39 | NamespaceName: k, 40 | NotificationID: v, 41 | }) 42 | 43 | return true 44 | }) 45 | 46 | bts, err := json.Marshal(¬ifications) 47 | if err != nil { 48 | return "" 49 | } 50 | 51 | return string(bts) 52 | } 53 | -------------------------------------------------------------------------------- /cache_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | package apollo 3 | 4 | import ( 5 | "io/ioutil" 6 | "os" 7 | "testing" 8 | 9 | "github.com/stretchr/testify/suite" 10 | ) 11 | 12 | type CacheTestSuite struct { 13 | suite.Suite 14 | } 15 | 16 | func (s *CacheTestSuite) TestCache() { 17 | cache := newCache() 18 | 19 | cache.set("key", []byte("val")) 20 | val, ok := cache.get("key") 21 | 22 | s.True(ok) 23 | s.Equal("val", string(val)) 24 | 25 | cache.set("key", []byte("val2")) 26 | val1, ok1 := cache.get("key") 27 | 28 | s.True(ok1) 29 | s.Equal("val2", string(val1)) 30 | 31 | kv := cache.dump() 32 | 33 | s.Equal(1, len(kv)) 34 | s.Equal("val2", string(kv["key"])) 35 | 36 | cache.delete("key") 37 | _, ok2 := cache.get("key") 38 | s.False(ok2) 39 | } 40 | 41 | func (s *CacheTestSuite) TestCacheDump() { 42 | var caches = newNamespaceCache() 43 | defer caches.drain() 44 | 45 | caches.mustGetCache("namespace").set("key", []byte("val")) 46 | 47 | f, err := ioutil.TempFile(".", "apollo") 48 | s.NoError(err) 49 | f.Close() 50 | defer os.Remove(f.Name()) 51 | 52 | s.NoError(caches.dump(f.Name())) 53 | 54 | var restore = newNamespaceCache() 55 | defer restore.drain() 56 | 57 | s.NoError(restore.load(f.Name())) 58 | 59 | val, _ := restore.mustGetCache("namespace").get("key") 60 | 61 | s.Equal("val", string(val)) 62 | 63 | s.Error(restore.load("null")) 64 | 65 | s.Error(restore.load("./testdata/app.yml")) 66 | } 67 | 68 | func TestRunCacheSuite(t *testing.T) { 69 | suite.Run(t, new(CacheTestSuite)) 70 | } 71 | -------------------------------------------------------------------------------- /apollo.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | //Package apollo ctrip apollo go client 4 | package apollo 5 | 6 | var ( 7 | defaultClient *Client 8 | ) 9 | 10 | // Start apollo 11 | func Start() error { 12 | return StartWithConfFile(defaultConfName) 13 | } 14 | 15 | // StartWithConfFile run apollo with conf file 16 | func StartWithConfFile(name string) error { 17 | log.Debugf("StartWithConfFile run apollo with conf file name: %s", name) 18 | conf, err := NewConf(name) 19 | if err != nil { 20 | return err 21 | } 22 | return StartWithConf(conf) 23 | } 24 | 25 | // StartWithConf run apollo with Conf 26 | func StartWithConf(conf *Conf) error { 27 | if log == nil { 28 | setDefaultLogger() 29 | } 30 | defaultClient = NewClient(conf) 31 | 32 | return defaultClient.Start() 33 | } 34 | 35 | // Stop sync config 36 | func Stop() error { 37 | return defaultClient.Stop() 38 | } 39 | 40 | // WatchUpdate get all updates 41 | func WatchUpdate() <-chan *ChangeEvent { 42 | return defaultClient.WatchUpdate() 43 | } 44 | 45 | // GetStringValueWithNameSpace get value from given namespace 46 | func GetStringValueWithNameSpace(namespace, key, defaultValue string) string { 47 | return defaultClient.GetStringValueWithNameSpace(namespace, key, defaultValue) 48 | } 49 | 50 | // GetStringValue from default namespace 51 | func GetStringValue(key, defaultValue string) string { 52 | return GetStringValueWithNameSpace(defaultNamespace, key, defaultValue) 53 | } 54 | 55 | // GetIntValue from default namespace 56 | func GetIntValue(key string, defaultValue int) int { 57 | return defaultClient.GetIntValue(key, defaultValue) 58 | } 59 | 60 | // GetNameSpaceContent get contents of namespace 61 | func GetNameSpaceContent(namespace, defaultValue string) string { 62 | return defaultClient.GetNameSpaceContent(namespace, defaultValue) 63 | } 64 | 65 | // ListKeys list all keys under given namespace 66 | func ListKeys(namespace string) []string { 67 | return defaultClient.ListKeys(namespace) 68 | } 69 | -------------------------------------------------------------------------------- /cache.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "encoding/gob" 7 | "errors" 8 | "os" 9 | "sync" 10 | ) 11 | 12 | type namespaceCache struct { 13 | lock sync.RWMutex 14 | caches map[string]*cache 15 | } 16 | 17 | func newNamespaceCache() *namespaceCache { 18 | return &namespaceCache{ 19 | caches: map[string]*cache{}, 20 | } 21 | } 22 | 23 | func (n *namespaceCache) mustGetCache(namespace string) *cache { 24 | n.lock.RLock() 25 | if ret, ok := n.caches[namespace]; ok { 26 | n.lock.RUnlock() 27 | return ret 28 | } 29 | n.lock.RUnlock() 30 | 31 | n.lock.Lock() 32 | defer n.lock.Unlock() 33 | 34 | cache := newCache() 35 | n.caches[namespace] = cache 36 | return cache 37 | } 38 | 39 | func (n *namespaceCache) drain() { 40 | n.lock.Lock() 41 | defer n.lock.Unlock() 42 | for namespace := range n.caches { 43 | delete(n.caches, namespace) 44 | } 45 | } 46 | 47 | func (n *namespaceCache) dump(name string) error { 48 | n.lock.Lock() 49 | defer n.lock.Unlock() 50 | var dumps = map[string]map[string][]byte{} 51 | 52 | for namespace, cache := range n.caches { 53 | dumps[namespace] = cache.dump() 54 | } 55 | 56 | f, err := os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) 57 | if err != nil { 58 | return err 59 | } 60 | defer f.Close() 61 | 62 | return gob.NewEncoder(f).Encode(&dumps) 63 | } 64 | 65 | func (n *namespaceCache) load(name string) error { 66 | n.drain() 67 | 68 | f, err := os.OpenFile(name, os.O_RDONLY, 0755) 69 | if err != nil { 70 | return errors.New("open cache file error") 71 | } 72 | defer f.Close() 73 | 74 | var dumps = map[string]map[string][]byte{} 75 | 76 | if err := gob.NewDecoder(f).Decode(&dumps); err != nil { 77 | return errors.New("cache file decoder error") 78 | } 79 | 80 | for namespace, kv := range dumps { 81 | cache := n.mustGetCache(namespace) 82 | for k, v := range kv { 83 | cache.set(k, v) 84 | } 85 | } 86 | 87 | return nil 88 | } 89 | 90 | type cache struct { 91 | kv sync.Map 92 | } 93 | 94 | func newCache() *cache { 95 | return &cache{ 96 | kv: sync.Map{}, 97 | } 98 | } 99 | 100 | func (c *cache) set(key string, val []byte) { 101 | c.kv.Store(key, val) 102 | } 103 | 104 | func (c *cache) get(key string) ([]byte, bool) { 105 | if val, ok := c.kv.Load(key); ok { 106 | if ret, ok := val.([]byte); ok { 107 | return ret, true 108 | } 109 | } 110 | return nil, false 111 | } 112 | 113 | func (c *cache) delete(key string) { 114 | c.kv.Delete(key) 115 | } 116 | 117 | func (c *cache) dump() map[string][]byte { 118 | var ret = map[string][]byte{} 119 | c.kv.Range(func(key, val interface{}) bool { 120 | k, _ := key.(string) 121 | v, _ := val.([]byte) 122 | ret[k] = v 123 | 124 | return true 125 | }) 126 | return ret 127 | } 128 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,osx,vim,linux,windows,sublimetext,visualstudiocode 3 | 4 | ### Go ### 5 | # Binaries for programs and plugins 6 | *.exe 7 | *.exe~ 8 | *.dll 9 | *.so 10 | *.dylib 11 | 12 | # Test binary, build with `go test -c` 13 | *.test 14 | test/* 15 | .apollo 16 | 17 | # Output of the go coverage tool, specifically when used with LiteIDE 18 | *.out 19 | 20 | ### Linux ### 21 | *~ 22 | 23 | # temporary files which can be created if a process still has a handle open of a deleted file 24 | .fuse_hidden* 25 | 26 | # KDE directory preferences 27 | .directory 28 | 29 | # Linux trash folder which might appear on any partition or disk 30 | .Trash-* 31 | 32 | # .nfs files are created when an open file is removed but is still being accessed 33 | .nfs* 34 | 35 | ### OSX ### 36 | *.DS_Store 37 | .AppleDouble 38 | .LSOverride 39 | 40 | # Icon must end with two \r 41 | Icon 42 | 43 | # Thumbnails 44 | ._* 45 | 46 | # Files that might appear in the root of a volume 47 | .DocumentRevisions-V100 48 | .fseventsd 49 | .Spotlight-V100 50 | .TemporaryItems 51 | .Trashes 52 | .VolumeIcon.icns 53 | .com.apple.timemachine.donotpresent 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | ### SublimeText ### 63 | # cache files for sublime text 64 | *.tmlanguage.cache 65 | *.tmPreferences.cache 66 | *.stTheme.cache 67 | 68 | # workspace files are user-specific 69 | *.sublime-workspace 70 | 71 | # project files should be checked into the repository, unless a significant 72 | # proportion of contributors will probably not be using SublimeText 73 | # *.sublime-project 74 | 75 | # sftp configuration file 76 | sftp-config.json 77 | 78 | # Package control specific files 79 | Package Control.last-run 80 | Package Control.ca-list 81 | Package Control.ca-bundle 82 | Package Control.system-ca-bundle 83 | Package Control.cache/ 84 | Package Control.ca-certs/ 85 | Package Control.merged-ca-bundle 86 | Package Control.user-ca-bundle 87 | oscrypto-ca-bundle.crt 88 | bh_unicode_properties.cache 89 | 90 | # Sublime-github package stores a github token in this file 91 | # https://packagecontrol.io/packages/sublime-github 92 | GitHub.sublime-settings 93 | 94 | ### Vim ### 95 | # swap 96 | .sw[a-p] 97 | .*.sw[a-p] 98 | # session 99 | Session.vim 100 | # temporary 101 | .netrwhist 102 | # auto-generated tag files 103 | tags 104 | 105 | ### VisualStudioCode ### 106 | .vscode/* 107 | !.vscode/settings.json 108 | !.vscode/tasks.json 109 | !.vscode/launch.json 110 | !.vscode/extensions.json 111 | .history 112 | 113 | ### Windows ### 114 | # Windows thumbnail cache files 115 | Thumbs.db 116 | ehthumbs.db 117 | ehthumbs_vista.db 118 | 119 | # Folder config file 120 | Desktop.ini 121 | 122 | # Recycle Bin used on file shares 123 | $RECYCLE.BIN/ 124 | 125 | # Windows Installer files 126 | *.cab 127 | *.msi 128 | *.msm 129 | *.msp 130 | 131 | # Windows shortcuts 132 | *.lnk 133 | 134 | 135 | # End of https://www.gitignore.io/api/go,osx,vim,linux,windows,sublimetext,visualstudiocode 136 | /.idea/ 137 | itest/* 138 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: go-1-10 4 | workspace: 5 | base: /go 6 | path: src/gopkg.in/apollo.v0 7 | steps: 8 | - name: test 9 | image: golang:1.10 10 | commands: 11 | - GO_FILES=$(find . -iname '*.go' -type f | egrep -v '/vendor') # All the .go files, excluding vendor/ 12 | - go get -u golang.org/x/lint/golint # Linter 13 | - go get github.com/fzipp/gocyclo 14 | - go get -u github.com/golang/dep/cmd/dep 15 | - dep init 16 | - dep ensure 17 | - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt 18 | - go vet ./... # go vet is the official Go static analyzer 19 | - gocyclo -over 19 $GO_FILES # forbid code with huge functions 20 | - golint -set_exit_status $(go list ./...) # one last linter 21 | - go test -v $(go list ./... | egrep -v '/vendor') 22 | 23 | --- 24 | kind: pipeline 25 | name: go-1-11 26 | 27 | steps: 28 | - name: test 29 | image: golang:1.11 30 | commands: 31 | - GO_FILES=$(find . -iname '*.go' -type f | egrep -v '/vendor') # All the .go files, excluding vendor/ 32 | - go get -u golang.org/x/lint/golint # Linter 33 | - go get -u honnef.co/go/tools/cmd/staticcheck 34 | - go get github.com/fzipp/gocyclo 35 | - go mod download 36 | - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt 37 | - go vet ./... # go vet is the official Go static analyzer 38 | - staticcheck ./... # "go vet on steroids" + linter 39 | - gocyclo -over 19 $GO_FILES # forbid code with huge functions 40 | - golint -set_exit_status $(go list ./...) # one last linter 41 | - go test -v $(go list ./... | egrep -v '/vendor') 42 | --- 43 | kind: pipeline 44 | name: go-1-12 45 | 46 | steps: 47 | - name: test 48 | image: golang:1.12 49 | environment: 50 | COVERALLS_TOKEN: 51 | from_secret: repo_token 52 | commands: 53 | - GO_FILES=$(find . -iname '*.go' -type f | egrep -v '/vendor') # All the .go files, excluding vendor/ 54 | - go get -u golang.org/x/lint/golint # Linter 55 | - go get -u honnef.co/go/tools/cmd/staticcheck 56 | - go get github.com/fzipp/gocyclo 57 | - go get 58 | - test -z $(gofmt -s -l $GO_FILES) # Fail if a .go file hasn't been formatted with gofmt 59 | - go vet ./... # go vet is the official Go static analyzer 60 | - staticcheck ./... # "go vet on steroids" + linter 61 | - gocyclo -over 19 $GO_FILES # forbid code with huge functions 62 | - golint -set_exit_status $(go list ./...) # one last linter 63 | - go get golang.org/x/tools/cmd/cover 64 | - go get github.com/mattn/goveralls 65 | - go test -v -covermode=count -coverprofile=coverage.out 66 | - goveralls -coverprofile=coverage.out -service drone.io -repotoken $COVERALLS_TOKEN -------------------------------------------------------------------------------- /apollo_test.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "os" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/suite" 11 | logger "gopkg.in/logger.v1" 12 | 13 | "gopkg.in/apollo.v0/internal/mockserver" 14 | ) 15 | 16 | type StartWithConfTestSuite struct { 17 | suite.Suite 18 | changeEvent <-chan *ChangeEvent 19 | } 20 | 21 | func (s *StartWithConfTestSuite) SetupSuite() { 22 | startTestApollo(s) 23 | s.changeEvent = WatchUpdate() 24 | mockserver.Set("application", "getkeys", "value") 25 | s.wait() 26 | } 27 | func startTestApollo(s *StartWithConfTestSuite) { 28 | s.Error(Start()) 29 | s.NoError(StartWithConfFile("./testdata/app.yml")) 30 | s.NoError(defaultClient.loadLocal(defaultDumpFile)) 31 | } 32 | func (s *StartWithConfTestSuite) BeforeTest(suiteName, testName string) { 33 | // log.Println("suiteName: " + suiteName) 34 | // log.Println("testName: " + testName) 35 | } 36 | func (s *StartWithConfTestSuite) TearDownSuite() { 37 | s.NoError(Stop()) 38 | os.Remove(defaultDumpFile) 39 | } 40 | func (s *StartWithConfTestSuite) TestLoadLocal() { 41 | err := defaultClient.loadLocal(defaultDumpFile) 42 | s.NoError(err) 43 | } 44 | func (s *StartWithConfTestSuite) TestLogger() { 45 | setDefaultLogger() 46 | s.IsType(&logger.Logger{}, log) 47 | setLogger() 48 | s.Equal(0, logger.GetOutputLevel()) 49 | s.NotNil(log) 50 | } 51 | 52 | func (s *StartWithConfTestSuite) TestGetStringValueWithNameSpace() { 53 | mockserver.Set("application", "key", "value") 54 | s.wait() 55 | val := GetStringValueWithNameSpace("application", "key", "defaultValue") 56 | s.Equal("value", val) 57 | } 58 | 59 | func (s *StartWithConfTestSuite) TestGetStringValue() { 60 | mockserver.Set("application", "key", "newvalue") 61 | s.wait() 62 | val := GetStringValue("key", "defaultValue") 63 | val2 := defaultClient.GetStringValue("key", "defaultValue") 64 | s.Equal("newvalue", val) 65 | s.Equal("newvalue", val2) 66 | } 67 | func (s *StartWithConfTestSuite) TestListKeys() { 68 | s.NotEmpty(ListKeys(defaultNamespace)) 69 | } 70 | func (s *StartWithConfTestSuite) TestGetIntValue() { 71 | mockserver.Set("application", "intkey", "1") 72 | s.wait() 73 | val := GetIntValue("intkey", 0) 74 | s.NotEqual(0, val) 75 | s.Equal(1, val) 76 | } 77 | func (s *StartWithConfTestSuite) TestGetNameSpaceContent() { 78 | 79 | mockserver.Set("client.json", "content", `{"name":"apollo"}`) 80 | s.wait() 81 | 82 | val := GetNameSpaceContent("client.json", "{}") 83 | s.Equal(`{"name":"apollo"}`, val) 84 | } 85 | 86 | func (s *StartWithConfTestSuite) wait() { 87 | select { 88 | case <-s.changeEvent: 89 | case <-time.After(time.Second * 30): 90 | } 91 | } 92 | func startMockServer() { 93 | go func() { 94 | log.Fatal(mockserver.Run()) 95 | }() 96 | // wait for mock server to run 97 | time.Sleep(time.Millisecond * 10) 98 | } 99 | 100 | func TestRunSuite(t *testing.T) { 101 | suite.Run(t, new(StartWithConfTestSuite)) 102 | } 103 | func TestMain(m *testing.M) { 104 | setup() 105 | code := m.Run() 106 | tearDown() 107 | os.Exit(code) 108 | } 109 | func setLogger() { 110 | mLog := logger.Std 111 | mLog.SetOutputLevel(0) 112 | SetLogger(mLog) 113 | } 114 | func setup() { 115 | setLogger() 116 | startMockServer() 117 | } 118 | func tearDown() { 119 | mockserver.Close() 120 | } 121 | -------------------------------------------------------------------------------- /poller.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // this is a static check 14 | var _ poller = (*longPoller)(nil) 15 | 16 | // poller fetch config updates 17 | type poller interface { 18 | // start poll updates 19 | start() 20 | // preload fetch all config to local cache, and update all notifications 21 | preload() error 22 | // stop poll updates 23 | stop() 24 | } 25 | 26 | // notificationHandler handle namespace update notification 27 | type notificationHandler func(namespace string) error 28 | 29 | // longPoller implement poller interface 30 | type longPoller struct { 31 | conf *Conf 32 | 33 | pollerInterval time.Duration 34 | ctx context.Context 35 | cancel context.CancelFunc 36 | 37 | requester requester 38 | 39 | notifications *notificationRepo 40 | handler notificationHandler 41 | } 42 | 43 | // newLongPoller create a Poller 44 | func newLongPoller(conf *Conf, interval time.Duration, handler notificationHandler) poller { 45 | poller := &longPoller{ 46 | conf: conf, 47 | pollerInterval: interval, 48 | requester: newHTTPRequester(&http.Client{Timeout: longPoolTimeout}), 49 | notifications: new(notificationRepo), 50 | handler: handler, 51 | } 52 | for _, namespace := range conf.Namespaces { 53 | poller.notifications.setNotificationID(namespace, defaultNotificationID) 54 | } 55 | 56 | return poller 57 | } 58 | 59 | func (p *longPoller) start() { 60 | go p.watchUpdates() 61 | } 62 | 63 | func (p *longPoller) preload() error { 64 | return p.pumpUpdates() 65 | } 66 | 67 | func (p *longPoller) watchUpdates() { 68 | p.ctx, p.cancel = context.WithCancel(context.Background()) 69 | defer p.cancel() 70 | 71 | timer := time.NewTimer(p.pollerInterval) 72 | defer timer.Stop() 73 | 74 | for { 75 | select { 76 | case <-timer.C: 77 | err := p.pumpUpdates() 78 | //TODO handle error 79 | if err != nil { 80 | fmt.Println("pumpUpdates error", err.Error()) 81 | } 82 | timer.Reset(p.pollerInterval) 83 | 84 | case <-p.ctx.Done(): 85 | return 86 | } 87 | } 88 | } 89 | 90 | func (p *longPoller) stop() { 91 | p.cancel() 92 | } 93 | 94 | func (p *longPoller) updateNotificationConf(notification *notification) { 95 | p.notifications.setNotificationID(notification.NamespaceName, notification.NotificationID) 96 | } 97 | 98 | // pumpUpdates fetch updated namespace, handle updated namespace then update notification id 99 | func (p *longPoller) pumpUpdates() error { 100 | var ret error 101 | 102 | updates, err := p.poll() 103 | if err != nil { 104 | return err 105 | } 106 | 107 | for _, update := range updates { 108 | if err := p.handler(update.NamespaceName); err != nil { 109 | //todo handle error. err maybe overwritten 110 | ret = err 111 | continue 112 | } 113 | p.updateNotificationConf(update) 114 | } 115 | return ret 116 | } 117 | 118 | // poll until a update or timeout 119 | func (p *longPoller) poll() ([]*notification, error) { 120 | notifications := p.notifications.toString() 121 | url := notificationURL(p.conf, notifications) 122 | bts, err := p.requester.request(url) 123 | if err != nil { 124 | return nil, err 125 | 126 | } 127 | var ret []*notification 128 | if len(bts) == 0 { 129 | return nil, nil 130 | } 131 | if err := json.Unmarshal(bts, &ret); err != nil { 132 | return nil, err 133 | } 134 | return ret, nil 135 | } 136 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-apollo 2 | 3 | [![Build Status](https://cloud.drone.io/api/badges/go-apollo/apollo/status.svg)](https://cloud.drone.io/go-apollo/apollo) 4 | [![Go Report Card](https://goreportcard.com/badge/github.com/go-apollo/apollo)](https://goreportcard.com/report/github.com/go-apollo/apollo) 5 | [![](https://godoc.org/gopkg.in/apollo.v0?status.svg)](http://godoc.org/gopkg.in/apollo.v0) 6 | [![Coverage Status](https://coveralls.io/repos/github/go-apollo/apollo/badge.svg?branch=master)](https://coveralls.io/github/go-apollo/apollo?branch=master) 7 | ## Purpose 8 | 9 | The goal of this project is to make the easiest way of using Ctrip apollo for golang applications. This project has been forked from [philchia/agollo](https://github.com/philchia/agollo) since 2018.8 but change a lot. 10 | 11 | ## Contributing 12 | 13 | Fork -> Patch -> Push -> Pull Request 14 | 15 | ## Feature 16 | 17 | - ✅ Multiple namespace support 18 | - ✅ Fail tolerant 19 | - ✅ Custom logger 20 | - ❎ YML to struct 21 | 22 | 23 | ## Required 24 | 25 | **go 1.10** or later 26 | 27 | ## Build 28 | If you want build this project,should use go 1.11+ 29 | ``` 30 | GO111MODULE=on; go mod download 31 | 32 | ``` 33 | 34 | ## Usage 35 | ### Installation 36 | ```bash 37 | # go mod (only go 1.11+) or project in gopath(go 1.10 +) 38 | go get -u gopkg.in/apollo.v0 39 | # if you use dep as your golang dep tool (go 1.10) 40 | dep ensure -add gopkg.in/apollo.v0 41 | ``` 42 | ### Set custom logger(Optional) 43 | go-apoll use gopkg.in/logger.v1 as default logger provider. 44 | Any logger implemented apollo.Logger interface can be use as apollo logger provider(such as [logrus](https://github.com/sirupsen/logrus)). 45 | ```golang 46 | //Logger interface 47 | type Logger interface { 48 | Warnf(format string, v ...interface{}) 49 | Warn(v ...interface{}) 50 | Errorf(format string, v ...interface{}) 51 | Error(v ...interface{}) 52 | Infof(format string, v ...interface{}) 53 | Info(v ...interface{}) 54 | Debugf(format string, v ...interface{}) 55 | Debug(v ...interface{}) 56 | Fatal(args ...interface{}) 57 | Fatalf(format string, args ...interface{}) 58 | } 59 | ``` 60 | set logrus as log provider 61 | ```golang 62 | var log = logrus.New() 63 | log.Formatter = new(logrus.JSONFormatter) 64 | log.Formatter = new(logrus.TextFormatter) //default 65 | log.Formatter.(*logrus.TextFormatter).DisableColors = true // remove colors 66 | log.Formatter.(*logrus.TextFormatter).DisableTimestamp = true // remove timestamp from test output 67 | log.Level = logrus.TraceLevel 68 | log.Out = os.Stdout 69 | 70 | apollo.SetLogger(log) 71 | ``` 72 | 73 | 74 | ### Start use default app.yml config file 75 | 76 | ```golang 77 | apollo.Start() 78 | ``` 79 | 80 | ### Start use given config file path 81 | 82 | ```golang 83 | apollo.StartWithConfFile(name) 84 | ``` 85 | 86 | ### Subscribe to updates 87 | 88 | ```golang 89 | events := apollo.WatchUpdate() 90 | changeEvent := <-events 91 | bytes, _ := json.Marshal(changeEvent) 92 | fmt.Println("event:", string(bytes)) 93 | ``` 94 | 95 | ### Get apollo values 96 | 97 | ```golang 98 | apollo.GetStringValue(Key, defaultValue) 99 | apollo.GetStringValueWithNameSapce(namespace, key, defaultValue) 100 | apollo.GetIntValue(Key, defaultValue) 101 | apollo.GetIntValueWithNameSapce(namespace, key, defaultValue) 102 | ``` 103 | 104 | ### Get namespace file contents 105 | 106 | ```golang 107 | apollo.GetNameSpaceContent(namespace, defaultValue) 108 | ``` 109 | 110 | ## License 111 | 112 | apollo is released under MIT lecense 113 | -------------------------------------------------------------------------------- /internal/mockserver/mockserver.go: -------------------------------------------------------------------------------- 1 | package mockserver 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "net/http" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | type notification struct { 13 | NamespaceName string `json:"namespaceName,omitempty"` 14 | NotificationID int `json:"notificationId,omitempty"` 15 | } 16 | 17 | type result struct { 18 | // AppID string `json:"appId"` 19 | // Cluster string `json:"cluster"` 20 | NamespaceName string `json:"namespaceName"` 21 | Configurations map[string]string `json:"configurations"` 22 | ReleaseKey string `json:"releaseKey"` 23 | } 24 | 25 | type mockServer struct { 26 | server http.Server 27 | 28 | lock sync.Mutex 29 | notifications map[string]int 30 | config map[string]map[string]string 31 | } 32 | 33 | func (s *mockServer) NotificationHandler(rw http.ResponseWriter, req *http.Request) { 34 | s.lock.Lock() 35 | defer s.lock.Unlock() 36 | req.ParseForm() 37 | var notifications []notification 38 | if err := json.Unmarshal([]byte(req.FormValue("notifications")), ¬ifications); err != nil { 39 | rw.WriteHeader(http.StatusInternalServerError) 40 | return 41 | } 42 | var changes []notification 43 | for _, noti := range notifications { 44 | if currentID := s.notifications[noti.NamespaceName]; currentID != noti.NotificationID { 45 | changes = append(changes, notification{NamespaceName: noti.NamespaceName, NotificationID: currentID}) 46 | } 47 | } 48 | 49 | if len(changes) == 0 { 50 | rw.WriteHeader(http.StatusNotModified) 51 | return 52 | } 53 | bts, err := json.Marshal(&changes) 54 | if err != nil { 55 | rw.WriteHeader(http.StatusInternalServerError) 56 | return 57 | } 58 | rw.Write(bts) 59 | } 60 | 61 | func (s *mockServer) ConfigHandler(rw http.ResponseWriter, req *http.Request) { 62 | req.ParseForm() 63 | 64 | strs := strings.Split(req.RequestURI, "/") 65 | var namespace, releaseKey = strings.Split(strs[4], "?")[0], req.FormValue("releaseKey") 66 | config := s.Get(namespace) 67 | 68 | var result = result{NamespaceName: namespace, Configurations: config, ReleaseKey: releaseKey} 69 | bts, err := json.Marshal(&result) 70 | if err != nil { 71 | rw.WriteHeader(http.StatusInternalServerError) 72 | return 73 | } 74 | rw.Write(bts) 75 | } 76 | 77 | var server *mockServer 78 | 79 | func (s *mockServer) Set(namespace, key, value string) { 80 | server.lock.Lock() 81 | defer server.lock.Unlock() 82 | 83 | notificationID := s.notifications[namespace] 84 | notificationID++ 85 | s.notifications[namespace] = notificationID 86 | 87 | if kv, ok := s.config[namespace]; ok { 88 | kv[key] = value 89 | return 90 | } 91 | kv := map[string]string{key: value} 92 | s.config[namespace] = kv 93 | } 94 | 95 | func (s *mockServer) Get(namespace string) map[string]string { 96 | server.lock.Lock() 97 | defer server.lock.Unlock() 98 | 99 | return s.config[namespace] 100 | } 101 | 102 | func (s *mockServer) Delete(namespace, key string) { 103 | server.lock.Lock() 104 | defer server.lock.Unlock() 105 | 106 | if kv, ok := s.config[namespace]; ok { 107 | delete(kv, key) 108 | } 109 | 110 | notificationID := s.notifications[namespace] 111 | notificationID++ 112 | s.notifications[namespace] = notificationID 113 | } 114 | 115 | // Set namespace's key value 116 | func Set(namespace, key, value string) { 117 | server.Set(namespace, key, value) 118 | } 119 | 120 | // Delete namespace's key 121 | func Delete(namespace, key string) { 122 | server.Delete(namespace, key) 123 | } 124 | 125 | // Run mock server 126 | func Run() error { 127 | initServer() 128 | return server.server.ListenAndServe() 129 | } 130 | 131 | func initServer() { 132 | server = &mockServer{ 133 | notifications: map[string]int{}, 134 | config: map[string]map[string]string{}, 135 | } 136 | mux := http.NewServeMux() 137 | mux.Handle("/notifications/", http.HandlerFunc(server.NotificationHandler)) 138 | mux.Handle("/configs/", http.HandlerFunc(server.ConfigHandler)) 139 | server.server.Handler = mux 140 | server.server.Addr = ":8080" 141 | } 142 | 143 | // Close mock server 144 | func Close() error { 145 | ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Second)) 146 | defer cancel() 147 | 148 | return server.server.Shutdown(ctx) 149 | } 150 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | //Copyright (c) 2017 Phil 2 | 3 | package apollo 4 | 5 | import ( 6 | "context" 7 | "encoding/json" 8 | "fmt" 9 | "net/http" 10 | "strconv" 11 | ) 12 | 13 | // Client for apollo 14 | type Client struct { 15 | conf *Conf 16 | 17 | updateChan chan *ChangeEvent 18 | 19 | caches *namespaceCache 20 | releaseKeyRepo *cache 21 | 22 | longPoller poller 23 | requester requester 24 | 25 | ctx context.Context 26 | cancel context.CancelFunc 27 | } 28 | 29 | // result of query config 30 | type result struct { 31 | // AppID string `json:"appId"` 32 | // Cluster string `json:"cluster"` 33 | NamespaceName string `json:"namespaceName"` 34 | Configurations map[string]string `json:"configurations"` 35 | ReleaseKey string `json:"releaseKey"` 36 | } 37 | 38 | // NewClient create client from conf 39 | func NewClient(conf *Conf) *Client { 40 | client := &Client{ 41 | conf: conf, 42 | caches: newNamespaceCache(), 43 | releaseKeyRepo: newCache(), 44 | 45 | requester: newHTTPRequester(&http.Client{Timeout: queryTimeout}), 46 | } 47 | 48 | client.longPoller = newLongPoller(conf, longPoolInterval, client.handleNamespaceUpdate) 49 | client.ctx, client.cancel = context.WithCancel(context.Background()) 50 | return client 51 | } 52 | 53 | // Start sync config 54 | func (c *Client) Start() error { 55 | // preload all config to local first 56 | if err := c.preload(); err != nil { 57 | return err 58 | } 59 | 60 | // start fetch update 61 | go c.longPoller.start() 62 | 63 | return nil 64 | } 65 | 66 | // handleNamespaceUpdate sync config for namespace, delivery changes to subscriber 67 | func (c *Client) handleNamespaceUpdate(namespace string) error { 68 | change, err := c.sync(namespace) 69 | if err != nil { 70 | return fmt.Errorf("handling namespace not updated %s", err) 71 | } 72 | //don't delivery change event if namespace has any change 73 | if change == nil { 74 | return nil 75 | } 76 | //delivery change event if namespace has changed 77 | c.deliveryChangeEvent(change) 78 | return nil 79 | } 80 | 81 | // Stop sync config 82 | func (c *Client) Stop() error { 83 | c.longPoller.stop() 84 | c.cancel() 85 | // close(c.updateChan) 86 | c.updateChan = nil 87 | return nil 88 | } 89 | 90 | // fetchAllConfig fetch from remote, if failed load from local file 91 | func (c *Client) preload() error { 92 | if err := c.longPoller.preload(); err != nil { 93 | log.Errorf("fetchAllConfig fetch from remote: %s", err) 94 | return c.loadLocal(defaultDumpFile) 95 | } 96 | return nil 97 | } 98 | 99 | // loadLocal load caches from local file 100 | func (c *Client) loadLocal(name string) error { 101 | log.Debugf("loadLocal load caches from local file,file name: %s", name) 102 | return c.caches.load(name) 103 | } 104 | 105 | // dump caches to file 106 | func (c *Client) dump(name string) error { 107 | return c.caches.dump(name) 108 | } 109 | 110 | // WatchUpdate get all updates 111 | func (c *Client) WatchUpdate() <-chan *ChangeEvent { 112 | if c.updateChan == nil { 113 | c.updateChan = make(chan *ChangeEvent) 114 | } 115 | return c.updateChan 116 | } 117 | 118 | func (c *Client) mustGetCache(namespace string) *cache { 119 | return c.caches.mustGetCache(namespace) 120 | } 121 | 122 | // GetStringValueWithNameSpace get value from given namespace 123 | func (c *Client) GetStringValueWithNameSpace(namespace, key, defaultValue string) string { 124 | log.Debugf("GetStringValueWithNameSpace get value from given namespace,namespace: %s,key: %s, defaultValue: %s", namespace, key, defaultValue) 125 | cache := c.mustGetCache(namespace) 126 | if ret, ok := cache.get(key); ok { 127 | log.Debugf("GetStringValueWithNameSpace from cache result:\nret: %s \nisOk: %t", ret, ok) 128 | return string(ret) 129 | } 130 | return defaultValue 131 | } 132 | 133 | // GetIntValueWithNameSpace get int value from given namespace 134 | func (c *Client) GetIntValueWithNameSpace(namespace, key string, defaultValue int) int { 135 | sValue := GetStringValueWithNameSpace(namespace, key, "") 136 | intValue, err := strconv.Atoi(sValue) 137 | if err != nil { 138 | log.Errorf("GetIntValue %s err: %s", key, err.Error()) 139 | return defaultValue 140 | } 141 | return intValue 142 | } 143 | 144 | // GetStringValue from default namespace 145 | func (c *Client) GetStringValue(key, defaultValue string) string { 146 | return c.GetStringValueWithNameSpace(defaultNamespace, key, defaultValue) 147 | } 148 | 149 | // GetIntValue from default namespace 150 | func (c *Client) GetIntValue(key string, defaultValue int) int { 151 | return c.GetIntValueWithNameSpace(defaultNamespace, key, defaultValue) 152 | } 153 | 154 | // GetNameSpaceContent get contents of namespace 155 | func (c *Client) GetNameSpaceContent(namespace, defaultValue string) string { 156 | return c.GetStringValueWithNameSpace(namespace, "content", defaultValue) 157 | } 158 | 159 | // ListKeys list all keys under given namespace 160 | func (c *Client) ListKeys(namespace string) []string { 161 | var keys []string 162 | cache := c.mustGetCache(namespace) 163 | cache.kv.Range(func(k, _ interface{}) bool { 164 | str, ok := k.(string) 165 | if ok { 166 | keys = append(keys, str) 167 | } 168 | return true 169 | }) 170 | return keys 171 | } 172 | 173 | // sync namespace config 174 | func (c *Client) sync(namespace string) (*ChangeEvent, error) { 175 | releaseKey := c.getReleaseKey(namespace) 176 | url := configURL(c.conf, namespace, string(releaseKey)) 177 | bts, err := c.requester.request(url) 178 | if err != nil || len(bts) == 0 { 179 | return nil, fmt.Errorf("sync namespace config error, remote error or empty congfig") 180 | } 181 | var result result 182 | if err := json.Unmarshal(bts, &result); err != nil { 183 | return nil, err 184 | } 185 | 186 | return c.handleResult(&result), nil 187 | } 188 | 189 | // deliveryChangeEvent push change to subscriber 190 | func (c *Client) deliveryChangeEvent(change *ChangeEvent) { 191 | if c.updateChan == nil { 192 | return 193 | } 194 | select { 195 | case <-c.ctx.Done(): 196 | case c.updateChan <- change: 197 | } 198 | } 199 | 200 | // handleResult generate changes from query result, and update local cache 201 | func (c *Client) handleResult(result *result) *ChangeEvent { 202 | var ret = ChangeEvent{ 203 | Namespace: result.NamespaceName, 204 | Changes: map[string]*Change{}, 205 | } 206 | 207 | cache := c.mustGetCache(result.NamespaceName) 208 | kv := cache.dump() 209 | for k, v := range kv { 210 | if _, ok := result.Configurations[k]; !ok { 211 | cache.delete(k) 212 | ret.Changes[k] = makeDeleteChange(k, v) 213 | } 214 | } 215 | 216 | for k, v := range result.Configurations { 217 | cache.set(k, []byte(v)) 218 | old, ok := kv[k] 219 | if !ok { 220 | ret.Changes[k] = makeAddChange(k, []byte(v)) 221 | continue 222 | } 223 | if string(old) != string(v) { 224 | ret.Changes[k] = makeModifyChange(k, old, []byte(v)) 225 | } 226 | } 227 | 228 | c.setReleaseKey(result.NamespaceName, []byte(result.ReleaseKey)) 229 | 230 | // dump caches to file 231 | c.dump(defaultDumpFile) 232 | 233 | if len(ret.Changes) == 0 { 234 | return nil 235 | } 236 | 237 | return &ret 238 | } 239 | 240 | func (c *Client) getReleaseKey(namespace string) []byte { 241 | releaseKey, _ := c.releaseKeyRepo.get(namespace) 242 | return releaseKey 243 | } 244 | 245 | func (c *Client) setReleaseKey(namespace string, releaseKey []byte) { 246 | c.releaseKeyRepo.set(namespace, releaseKey) 247 | } 248 | --------------------------------------------------------------------------------