├── .github └── workflows │ ├── golangci-lint.yml │ └── static_check.yml ├── .gitignore ├── LICENSE ├── NOTICE ├── README.md ├── client.go ├── client_test.go ├── exception.go ├── go.mod ├── go.sum ├── options.go ├── options_test.go ├── response.go ├── scripts └── ci │ └── start_latest_sc.sh └── url.go /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - master 8 | pull_request: 9 | jobs: 10 | golangci: 11 | name: lint 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: golangci-lint 16 | uses: golangci/golangci-lint-action@v2 17 | with: 18 | version: v1.55.2 19 | args: --skip-dirs=examples --skip-files=.*_test.go$ 20 | -------------------------------------------------------------------------------- /.github/workflows/static_check.yml: -------------------------------------------------------------------------------- 1 | name: Merge check 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Merge check 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Set up Go 1.20 9 | uses: actions/setup-go@v1 10 | with: 11 | go-version: 1.20 12 | id: go 13 | - name: Check out code into the Go module directory 14 | uses: actions/checkout@v1 15 | - name: Build 16 | run: go build -v . 17 | - name: UT 18 | run: | 19 | bash scripts/ci/start_latest_sc.sh 20 | go test ./... -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Go template 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | glide.lock 16 | .idea/ 17 | vendor/ 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | go-chassis Go SC Client 2 | Copyright 2017-2018 The Apache Software Foundation 3 | 4 | This product includes software developed at 5 | 6 | =============================================================== 7 | BSD 2-Clause "Simplified" License 8 | =============================================================== 9 | 10 | github.com/gorilla/websocket 11 | 12 | 13 | 14 | ================================================================ 15 | MIT License 16 | ================================================================ 17 | 18 | github.com/cenkalti/backoff 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Service Center client for go 2 | This is a service center client which helps the microservice to interact with Service Center 3 | for service-registration, discovery, instance registration etc. 4 | 5 | This client implements [API](https://github.com/apache/servicecomb-service-center/blob/master/docs/openapi/v4.yaml) of Service Center. 6 | 7 | 8 | # Usage 9 | 10 | ```go 11 | registryClient, err := sc.NewClient( 12 | sc.Options{ 13 | Addrs: []string{"127.0.0.1:30100"}, 14 | }) 15 | ``` 16 | declare and register micro service 17 | ```go 18 | var ms = new(discovery.MicroService) 19 | var m = make(map[string]string) 20 | 21 | m["abc"] = "abc" 22 | m["def"] = "def" 23 | 24 | ms.AppId = MSList[0].AppId 25 | ms.ServiceName = MSList[0].ServiceName 26 | ms.Version = MSList[0].Version 27 | ms.Environment = MSList[0].Environment 28 | ms.Properties = m 29 | sid, err := registryClient.RegisterService(ms) 30 | ``` 31 | declare and register instance 32 | ```go 33 | microServiceInstance := &discovery.MicroServiceInstance{ 34 | Endpoints: []string{"rest://127.0.0.1:3000"}, 35 | HostName: hostname, 36 | Status: sc.MSInstanceUP, 37 | } 38 | id, err := registryClient.RegisterMicroServiceInstance(microServiceInstance) 39 | ``` -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "sync" 15 | "time" 16 | 17 | "github.com/cenkalti/backoff/v4" 18 | "github.com/go-chassis/cari/addresspool" 19 | "github.com/go-chassis/cari/discovery" 20 | "github.com/go-chassis/cari/rbac" 21 | "github.com/go-chassis/foundation/httpclient" 22 | "github.com/go-chassis/foundation/httputil" 23 | "github.com/go-chassis/openlog" 24 | "github.com/gorilla/websocket" 25 | "github.com/patrickmn/go-cache" 26 | ) 27 | 28 | // Define constants for the client 29 | const ( 30 | MicroservicePath = "/microservices" 31 | InstancePath = "/instances" 32 | BatchInstancePath = "/instances/action" 33 | SchemaPath = "/schemas" 34 | HeartbeatPath = "/heartbeat" 35 | ExistencePath = "/existence" 36 | WatchPath = "/watcher" 37 | StatusPath = "/status" 38 | DependencyPath = "/dependencies" 39 | PropertiesPath = "/properties" 40 | TokenPath = "/v4/token" 41 | ReadinessPath = "/health/readiness" 42 | HeaderContentType = "Content-Type" 43 | HeaderUserAgent = "User-Agent" 44 | HeaderAuth = "Authorization" 45 | DefaultAddr = "127.0.0.1:30100" 46 | AppsPath = "/apps" 47 | PeerHealthPath = "/v1/syncer/health" 48 | DefaultRetryTimeout = 500 * time.Millisecond 49 | DefaultTokenExpiration = 10 * time.Hour 50 | HeaderRevision = "X-Resource-Revision" 51 | EnvProjectID = "CSE_PROJECT_ID" 52 | ) 53 | 54 | // Define variables for the client 55 | var ( 56 | MSAPIPath = "" 57 | GovernAPIPATH = "" 58 | TenantHeader = "X-Domain-Name" 59 | defineOnce = sync.Once{} 60 | ) 61 | var ( 62 | // ErrNotModified means instance is not changed 63 | ErrNotModified = errors.New("instance is not changed since last query") 64 | // ErrMicroServiceExists means service is registered 65 | ErrMicroServiceExists = errors.New("micro-service already exists") 66 | // ErrMicroServiceNotExists means service is not exists 67 | ErrMicroServiceNotExists = errors.New("micro-service does not exist") 68 | // ErrEmptyCriteria means you gave an empty list of criteria 69 | ErrEmptyCriteria = errors.New("batch find criteria is empty") 70 | ErrNil = errors.New("input is nil") 71 | ) 72 | 73 | // Client communicate to Service-Center 74 | type Client struct { 75 | opt Options 76 | client *httpclient.Requests 77 | protocol string 78 | watchers map[string]bool 79 | mutex sync.Mutex 80 | // addresspool mutex 81 | poolMutex sync.Mutex 82 | wsDialer *websocket.Dialer 83 | // record the websocket connection with the service center 84 | conns map[string]*websocket.Conn 85 | pool *addresspool.Pool 86 | } 87 | 88 | func (c *Client) dialWebsocket(url *url.URL) (*websocket.Conn, *http.Response, error) { 89 | var err error 90 | handshakeReq := &http.Request{Header: c.GetDefaultHeaders(), URL: url} 91 | if c.opt.SignRequest != nil { 92 | if err = c.opt.SignRequest(handshakeReq); err != nil { 93 | openlog.Error("sign websocket request failed" + err.Error()) 94 | return nil, nil, err 95 | } 96 | } else if httpclient.SignRequest != nil { 97 | if err = httpclient.SignRequest(handshakeReq); err != nil { 98 | openlog.Error("sign websocket request failed" + err.Error()) 99 | return nil, nil, err 100 | } 101 | } 102 | 103 | return c.wsDialer.Dial(url.String(), handshakeReq.Header) 104 | } 105 | 106 | type PeerStatusResp struct { 107 | Peers []*Peer `json:"peers"` 108 | } 109 | 110 | type Peer struct { 111 | Name string `json:"name"` 112 | Kind string `json:"kind"` 113 | Mode []string `json:"mode"` 114 | Endpoints []string `json:"endpoints"` 115 | Status string `json:"status"` 116 | } 117 | 118 | // URLParameter maintains the list of parameters to be added in URL 119 | type URLParameter map[string]string 120 | 121 | // NewClient create a the service center client 122 | func NewClient(opt Options) (*Client, error) { 123 | c := &Client{ 124 | opt: opt, 125 | watchers: make(map[string]bool), 126 | conns: make(map[string]*websocket.Conn), 127 | } 128 | options := c.buildClientOptions(opt) 129 | var err error 130 | c.client, err = httpclient.New(options) 131 | if err != nil { 132 | return nil, err 133 | } 134 | c.wsDialer = &websocket.Dialer{ 135 | TLSClientConfig: opt.TLSConfig, 136 | } 137 | c.protocol = "https" 138 | if !c.opt.EnableSSL { 139 | c.wsDialer = websocket.DefaultDialer 140 | c.protocol = "http" 141 | } 142 | // Update the API Base Path based on the project 143 | c.updateAPIPath() 144 | c.pool = addresspool.NewPool(opt.Endpoints, addresspool.Options{ 145 | HttpProbeOptions: &addresspool.HttpProbeOptions{ 146 | Protocol: c.protocol, 147 | Path: MSAPIPath + ReadinessPath, 148 | }, 149 | DiffAzEndpoints: opt.DiffAzEndpoints, 150 | }) 151 | return c, nil 152 | } 153 | 154 | // Reset the service center client 155 | func (c *Client) Reset(opt Options) error { 156 | c.poolMutex.Lock() 157 | defer c.poolMutex.Unlock() 158 | options := c.buildClientOptions(opt) 159 | var err error 160 | c.client, err = httpclient.New(options) 161 | if err != nil { 162 | return err 163 | } 164 | c.protocol = "https" 165 | if !c.opt.EnableSSL { 166 | c.wsDialer = websocket.DefaultDialer 167 | c.protocol = "http" 168 | } 169 | c.pool.ResetAddress(opt.Endpoints) 170 | return nil 171 | } 172 | 173 | // buildClientOptions build options for http client 174 | func (c *Client) buildClientOptions(opt Options) *httpclient.Options { 175 | options := &httpclient.Options{ 176 | TLSConfig: opt.TLSConfig, 177 | Compressed: opt.Compressed, 178 | RequestTimeout: opt.Timeout, 179 | } 180 | if !opt.EnableAuth { 181 | return options 182 | } 183 | if opt.SignRequest != nil { 184 | options.SignRequest = opt.SignRequest 185 | return options 186 | } 187 | // when the authentication is enabled, the token of automatic renewal is added to the request header 188 | if opt.TokenExpiration == 0 { 189 | opt.TokenExpiration = DefaultTokenExpiration 190 | } 191 | tokenCache := cache.New(opt.TokenExpiration, 1*time.Hour) 192 | options.SignRequest = func(req *http.Request) error { 193 | if req.URL.Path == TokenPath { 194 | return nil 195 | } 196 | if opt.AuthToken != "" { 197 | req.Header.Set(HeaderAuth, "Bearer "+opt.AuthToken) 198 | return nil 199 | } 200 | cachedToken, isFound := tokenCache.Get("token") 201 | if isFound { 202 | req.Header.Set(HeaderAuth, "Bearer "+cachedToken.(string)) 203 | } else { 204 | token, err := c.GetToken(opt.AuthUser) 205 | if err != nil { 206 | return err 207 | } 208 | req.Header.Set(HeaderAuth, "Bearer "+token) 209 | tokenCache.Set("token", token, cache.DefaultExpiration) 210 | } 211 | return nil 212 | } 213 | return options 214 | } 215 | 216 | func (c *Client) updateAPIPath() { 217 | defineOnce.Do(func() { 218 | projectID, isExist := os.LookupEnv(EnvProjectID) 219 | if !isExist { 220 | projectID = "default" 221 | } 222 | MSAPIPath = "/v4/" + projectID + "/registry" 223 | GovernAPIPATH = "/v4/" + projectID + "/govern" 224 | }) 225 | } 226 | 227 | func (c *Client) CheckReadiness() int { 228 | return c.pool.CheckReadiness() 229 | } 230 | 231 | // SyncEndpoints gets the endpoints of service-center in the cluster 232 | // if your service center cluster is not behind a load balancing service like ELB,nginx etc 233 | // then you can use this function 234 | func (c *Client) SyncEndpoints() error { 235 | c.poolMutex.Lock() 236 | defer c.poolMutex.Unlock() 237 | instances, err := c.Health() 238 | if err != nil { 239 | return fmt.Errorf("sync SC ep failed. err:%s", err.Error()) 240 | } 241 | return c.pool.SetAddressByInstances(instances) 242 | } 243 | 244 | func (c *Client) formatURL(api string, querys []URLParameter, options *CallOptions) string { 245 | host := c.GetAddress() 246 | if options != nil && len(options.Address) != 0 { 247 | host = options.Address 248 | } 249 | builder := URLBuilder{ 250 | Protocol: c.protocol, 251 | Host: host, 252 | Path: api, 253 | URLParameters: querys, 254 | CallOptions: options, 255 | } 256 | return builder.String() 257 | } 258 | 259 | // GetDefaultHeaders gets the default headers for each request to be made to Service-Center 260 | func (c *Client) GetDefaultHeaders() http.Header { 261 | headers := http.Header{ 262 | HeaderContentType: []string{"application/json"}, 263 | HeaderUserAgent: []string{"go-client"}, 264 | TenantHeader: []string{"default"}, 265 | } 266 | 267 | return headers 268 | } 269 | 270 | // httpDo makes the http request to Service-center with proper header, body and method 271 | func (c *Client) httpDo(method string, rawURL string, headers http.Header, body []byte) (resp *http.Response, err error) { 272 | if len(headers) == 0 { 273 | headers = make(http.Header) 274 | } 275 | for k, v := range c.GetDefaultHeaders() { 276 | headers[k] = v 277 | } 278 | return c.client.Do(context.Background(), method, rawURL, headers, body) 279 | } 280 | 281 | // RegisterService registers the micro-services to Service-Center 282 | func (c *Client) RegisterService(microService *discovery.MicroService) (string, error) { 283 | if microService == nil { 284 | return "", ErrNil 285 | } 286 | request := discovery.CreateServiceRequest{ 287 | Service: microService, 288 | } 289 | 290 | registerURL := c.formatURL(MSAPIPath+MicroservicePath, nil, nil) 291 | body, err := json.Marshal(request) 292 | if err != nil { 293 | return "", NewJSONException(err, string(body)) 294 | } 295 | 296 | resp, err := c.httpDo("POST", registerURL, nil, body) 297 | if err != nil { 298 | return "", err 299 | } 300 | if resp == nil { 301 | return "", fmt.Errorf("RegisterService failed, response is empty, MicroServiceName: %s", microService.ServiceName) 302 | } 303 | body, err = ioutil.ReadAll(resp.Body) 304 | if err != nil { 305 | return "", NewIOException(err) 306 | } 307 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 308 | var response discovery.GetExistenceResponse 309 | err = json.Unmarshal(body, &response) 310 | if err != nil { 311 | return "", NewJSONException(err, string(body)) 312 | } 313 | microService.ServiceId = response.ServiceId 314 | return response.ServiceId, nil 315 | } 316 | if resp.StatusCode == 400 { 317 | return "", fmt.Errorf("client seems to have erred, error: %s", body) 318 | } 319 | return "", fmt.Errorf("register service failed, ServiceName/responseStatusCode/responsebody: %s/%d/%s", 320 | microService.ServiceName, resp.StatusCode, string(body)) 321 | } 322 | 323 | // GetProviders gets a list of provider for a particular consumer 324 | func (c *Client) GetProviders(consumer string, opts ...CallOption) (*MicroServiceProvideResponse, error) { 325 | copts := &CallOptions{} 326 | for _, opt := range opts { 327 | opt(copts) 328 | } 329 | providersURL := c.formatURL(fmt.Sprintf("%s%s/%s/providers", MSAPIPath, MicroservicePath, consumer), nil, copts) 330 | resp, err := c.httpDo("GET", providersURL, nil, nil) 331 | if err != nil { 332 | return nil, fmt.Errorf("get Providers failed, error: %s, MicroServiceid: %s", err, consumer) 333 | } 334 | if resp == nil { 335 | return nil, fmt.Errorf("get Providers failed, response is empty, MicroServiceid: %s", consumer) 336 | } 337 | var body []byte 338 | body, err = ioutil.ReadAll(resp.Body) 339 | if err != nil { 340 | return nil, fmt.Errorf("Get Providers failed, body is empty, error: %s, MicroServiceid: %s", err, consumer) 341 | } 342 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 343 | p := &MicroServiceProvideResponse{} 344 | err = json.Unmarshal(body, p) 345 | if err != nil { 346 | return nil, err 347 | } 348 | return p, nil 349 | } 350 | return nil, fmt.Errorf("get Providers failed, MicroServiceid: %s, response StatusCode: %d, response body: %s", 351 | consumer, resp.StatusCode, string(body)) 352 | } 353 | 354 | // AddSchemas adds a schema contents to the services registered in service-center 355 | func (c *Client) AddSchemas(microServiceID, schemaName, schemaInfo string) error { 356 | if microServiceID == "" { 357 | return errors.New("invalid micro service ID") 358 | } 359 | 360 | schemaURL := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s", MSAPIPath, MicroservicePath, microServiceID, SchemaPath, schemaName), nil, nil) 361 | h := sha256.New() 362 | _, err := h.Write([]byte(schemaInfo)) 363 | if err != nil { 364 | return err 365 | } 366 | request := &discovery.ModifySchemaRequest{ 367 | ServiceId: microServiceID, 368 | SchemaId: schemaName, 369 | Schema: schemaInfo, 370 | Summary: fmt.Sprintf("%x", h.Sum(nil)), 371 | } 372 | body, err := json.Marshal(request) 373 | if err != nil { 374 | return NewJSONException(err, string(body)) 375 | } 376 | 377 | resp, err := c.httpDo("PUT", schemaURL, nil, body) 378 | if err != nil { 379 | return err 380 | } 381 | 382 | if resp == nil { 383 | return fmt.Errorf("add schemas failed, response is empty") 384 | } 385 | 386 | if resp.StatusCode != http.StatusOK { 387 | return NewCommonException("add micro service schema failed. response StatusCode: %d, response body: %s", 388 | resp.StatusCode, string(httputil.ReadBody(resp))) 389 | } 390 | 391 | return nil 392 | } 393 | 394 | // GetSchema gets Schema list for the microservice from service-center 395 | func (c *Client) GetSchema(microServiceID, schemaName string, opts ...CallOption) ([]byte, error) { 396 | if microServiceID == "" { 397 | return []byte(""), errors.New("invalid micro service ID") 398 | } 399 | copts := &CallOptions{} 400 | for _, opt := range opts { 401 | opt(copts) 402 | } 403 | url := c.formatURL(fmt.Sprintf("%s%s/%s/%s/%s", MSAPIPath, MicroservicePath, microServiceID, "schemas", schemaName), nil, copts) 404 | resp, err := c.httpDo("GET", url, nil, nil) 405 | if err != nil { 406 | return []byte(""), err 407 | } 408 | if resp == nil { 409 | return []byte(""), fmt.Errorf("GetSchema failed, response is empty") 410 | } 411 | var body []byte 412 | body, err = ioutil.ReadAll(resp.Body) 413 | if err != nil { 414 | return []byte(""), NewIOException(err) 415 | } 416 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 417 | return body, nil 418 | } 419 | 420 | return []byte(""), err 421 | } 422 | 423 | // GetMicroServiceID gets the microserviceid by appID, serviceName and version 424 | func (c *Client) GetMicroServiceID(appID, microServiceName, version, env string, opts ...CallOption) (string, error) { 425 | copts := &CallOptions{} 426 | for _, opt := range opts { 427 | opt(copts) 428 | } 429 | url := c.formatURL(MSAPIPath+ExistencePath, []URLParameter{ 430 | {"type": "microservice"}, 431 | {"appId": appID}, 432 | {"serviceName": microServiceName}, 433 | {"version": version}, 434 | {"env": env}, 435 | }, copts) 436 | resp, err := c.httpDo("GET", url, nil, nil) 437 | if err != nil { 438 | return "", err 439 | } 440 | if resp == nil { 441 | return "", fmt.Errorf("GetMicroServiceID failed, response is empty, MicroServiceName: %s", microServiceName) 442 | } 443 | var body []byte 444 | body, err = ioutil.ReadAll(resp.Body) 445 | if err != nil { 446 | return "", NewIOException(err) 447 | } 448 | if resp.StatusCode >= 200 && resp.StatusCode < 500 { 449 | var response discovery.GetExistenceResponse 450 | err = json.Unmarshal(body, &response) 451 | if err != nil { 452 | return "", NewJSONException(err, string(body)) 453 | } 454 | return response.ServiceId, nil 455 | } 456 | return "", fmt.Errorf("GetMicroServiceID failed, MicroService: %s@%s#%s, response StatusCode: %d, response body: %s, URL: %s", 457 | microServiceName, appID, version, resp.StatusCode, string(body), url) 458 | } 459 | 460 | // GetAllMicroServices gets list of all the microservices registered with Service-Center 461 | func (c *Client) GetAllMicroServices(opts ...CallOption) ([]*discovery.MicroService, error) { 462 | copts := &CallOptions{} 463 | for _, opt := range opts { 464 | opt(copts) 465 | } 466 | url := c.formatURL(MSAPIPath+MicroservicePath, nil, copts) 467 | resp, err := c.httpDo("GET", url, nil, nil) 468 | if err != nil { 469 | return nil, err 470 | } 471 | if resp == nil { 472 | return nil, fmt.Errorf("GetAllMicroServices failed, response is empty") 473 | } 474 | var body []byte 475 | body, err = ioutil.ReadAll(resp.Body) 476 | if err != nil { 477 | return nil, NewIOException(err) 478 | } 479 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 480 | var response discovery.GetServicesResponse 481 | err = json.Unmarshal(body, &response) 482 | if err != nil { 483 | return nil, NewJSONException(err, string(body)) 484 | } 485 | return response.Services, nil 486 | } 487 | return nil, fmt.Errorf("GetAllMicroServices failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 488 | } 489 | 490 | // GetAllApplications returns the list of all the applications which is registered in governance-center 491 | func (c *Client) GetAllApplications(opts ...CallOption) ([]string, error) { 492 | copts := &CallOptions{} 493 | for _, opt := range opts { 494 | opt(copts) 495 | } 496 | governanceURL := c.formatURL(GovernAPIPATH+AppsPath, nil, copts) 497 | resp, err := c.httpDo("GET", governanceURL, nil, nil) 498 | if err != nil { 499 | return nil, err 500 | } 501 | if resp == nil { 502 | return nil, fmt.Errorf("GetAllApplications failed, response is empty") 503 | } 504 | var body []byte 505 | body, err = ioutil.ReadAll(resp.Body) 506 | if err != nil { 507 | return nil, NewIOException(err) 508 | } 509 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 510 | var response discovery.GetAppsResponse 511 | err = json.Unmarshal(body, &response) 512 | if err != nil { 513 | return nil, NewJSONException(err, string(body)) 514 | } 515 | return response.AppIds, nil 516 | } 517 | return nil, fmt.Errorf("GetAllApplications failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 518 | } 519 | 520 | // GetMicroService returns the microservices by ID 521 | func (c *Client) GetMicroService(microServiceID string, opts ...CallOption) (*discovery.MicroService, error) { 522 | copts := &CallOptions{} 523 | for _, opt := range opts { 524 | opt(copts) 525 | } 526 | microserviceURL := c.formatURL(fmt.Sprintf("%s%s/%s", MSAPIPath, MicroservicePath, microServiceID), nil, copts) 527 | resp, err := c.httpDo("GET", microserviceURL, nil, nil) 528 | if err != nil { 529 | return nil, err 530 | } 531 | if resp == nil { 532 | return nil, fmt.Errorf("GetMicroService failed, response is empty, MicroServiceId: %s", microServiceID) 533 | } 534 | var body []byte 535 | body, err = ioutil.ReadAll(resp.Body) 536 | if err != nil { 537 | return nil, NewIOException(err) 538 | } 539 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 540 | var response discovery.GetServiceResponse 541 | err = json.Unmarshal(body, &response) 542 | if err != nil { 543 | return nil, NewJSONException(err, string(body)) 544 | } 545 | return response.Service, nil 546 | } 547 | return nil, fmt.Errorf("GetMicroService failed, MicroServiceId: %s, response StatusCode: %d, response body: %s\n, microserviceURL: %s", microServiceID, resp.StatusCode, string(body), microserviceURL) 548 | } 549 | 550 | // BatchFindInstances fetch instances based on service name, env, app and version 551 | // finally it return instances grouped by service name 552 | func (c *Client) BatchFindInstances(consumerID string, keys []*discovery.FindService, opts ...CallOption) (*discovery.BatchFindInstancesResponse, error) { 553 | copts := &CallOptions{} 554 | for _, opt := range opts { 555 | opt(copts) 556 | } 557 | if len(keys) == 0 { 558 | return nil, ErrEmptyCriteria 559 | } 560 | url := c.formatURL(MSAPIPath+BatchInstancePath, []URLParameter{ 561 | {"type": "query"}, 562 | }, copts) 563 | r := &discovery.BatchFindInstancesRequest{ 564 | ConsumerServiceId: consumerID, 565 | Services: keys, 566 | } 567 | rBody, err := json.Marshal(r) 568 | if err != nil { 569 | return nil, NewJSONException(err, string(rBody)) 570 | } 571 | resp, err := c.httpDo("POST", url, http.Header{"X-ConsumerId": []string{consumerID}}, rBody) 572 | if err != nil { 573 | return nil, err 574 | } 575 | if resp == nil { 576 | return nil, fmt.Errorf("BatchFindInstances failed, response is empty") 577 | } 578 | body := httputil.ReadBody(resp) 579 | if resp.StatusCode == http.StatusOK { 580 | var response *discovery.BatchFindInstancesResponse 581 | err = json.Unmarshal(body, &response) 582 | if err != nil { 583 | return nil, NewJSONException(err, string(body)) 584 | } 585 | 586 | return response, nil 587 | } 588 | return nil, fmt.Errorf("batch find failed, status %d, body %s", resp.StatusCode, body) 589 | } 590 | 591 | // FindMicroServiceInstances find microservice instance using consumerID, appID, name and version rule 592 | // 593 | // Deprecated: use FindInstances instead 594 | func (c *Client) FindMicroServiceInstances(consumerID, appID, microServiceName, 595 | versionRule string, opts ...CallOption) ([]*discovery.MicroServiceInstance, error) { 596 | rst, err := c.findInstances(consumerID, appID, microServiceName, versionRule, opts...) 597 | if err != nil { 598 | return nil, err 599 | } 600 | return rst.Instances, nil 601 | } 602 | 603 | // FindInstances find microservice instance 604 | func (c *Client) FindInstances(consumerID, appID, microServiceName string, 605 | opts ...CallOption) (*FindMicroServiceInstancesResult, error) { 606 | return c.findInstances(consumerID, appID, microServiceName, "0%2B", opts...) // 0+, all version 607 | } 608 | 609 | // FindInstances find microservice instance using consumerID, appID, name 610 | func (c *Client) findInstances(consumerID, appID, microServiceName, 611 | versionRule string, opts ...CallOption) (*FindMicroServiceInstancesResult, error) { 612 | copts := &CallOptions{} 613 | for _, opt := range opts { 614 | opt(copts) 615 | } 616 | microserviceInstanceURL := c.formatURL(MSAPIPath+InstancePath, []URLParameter{ 617 | {"appId": appID}, 618 | {"serviceName": microServiceName}, 619 | {"version": versionRule}, 620 | }, copts) 621 | 622 | resp, err := c.httpDo("GET", microserviceInstanceURL, http.Header{"X-ConsumerId": []string{consumerID}}, nil) 623 | if err != nil { 624 | return nil, err 625 | } 626 | if resp == nil { 627 | return nil, fmt.Errorf("FindMicroServiceInstances failed, response is empty, appID/MicroServiceName/version: %s/%s/%s", appID, microServiceName, versionRule) 628 | } 629 | var body []byte 630 | body, err = ioutil.ReadAll(resp.Body) 631 | if err != nil { 632 | return nil, NewIOException(err) 633 | } 634 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 635 | var response discovery.GetInstancesResponse 636 | err = json.Unmarshal(body, &response) 637 | if err != nil { 638 | return nil, NewJSONException(err, string(body)) 639 | } 640 | return &FindMicroServiceInstancesResult{ 641 | Instances: response.Instances, 642 | Revision: resp.Header.Get(HeaderRevision), 643 | }, nil 644 | } 645 | if resp.StatusCode == http.StatusNotModified { 646 | return nil, ErrNotModified 647 | } 648 | if resp.StatusCode == http.StatusBadRequest { 649 | if strings.Contains(string(body), "\"errorCode\":\"400012\"") { 650 | return nil, ErrMicroServiceNotExists 651 | } 652 | } 653 | return nil, fmt.Errorf("FindMicroServiceInstances failed, appID/MicroServiceName/version: %s/%s/%s, response StatusCode: %d, response body: %s", 654 | appID, microServiceName, versionRule, resp.StatusCode, string(body)) 655 | } 656 | 657 | // RegisterMicroServiceInstance registers the microservice instance to Servive-Center 658 | func (c *Client) RegisterMicroServiceInstance(microServiceInstance *discovery.MicroServiceInstance) (string, error) { 659 | if microServiceInstance == nil { 660 | return "", errors.New("invalid request parameter") 661 | } 662 | request := &discovery.RegisterInstanceRequest{ 663 | Instance: microServiceInstance, 664 | } 665 | microserviceInstanceURL := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceInstance.ServiceId, InstancePath), nil, nil) 666 | body, err := json.Marshal(request) 667 | if err != nil { 668 | return "", NewJSONException(err, string(body)) 669 | } 670 | resp, err := c.httpDo("POST", microserviceInstanceURL, nil, body) 671 | if err != nil { 672 | return "", err 673 | } 674 | if resp == nil { 675 | return "", fmt.Errorf("register instance failed, response is empty, MicroServiceId = %s", microServiceInstance.ServiceId) 676 | } 677 | body, err = ioutil.ReadAll(resp.Body) 678 | if err != nil { 679 | return "", NewIOException(err) 680 | } 681 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 682 | var response *discovery.RegisterInstanceResponse 683 | err = json.Unmarshal(body, &response) 684 | if err != nil { 685 | return "", NewJSONException(err, string(body)) 686 | } 687 | return response.InstanceId, nil 688 | } 689 | return "", fmt.Errorf("register instance failed, MicroServiceId: %s, response StatusCode: %d, response body: %s", 690 | microServiceInstance.ServiceId, resp.StatusCode, string(body)) 691 | } 692 | 693 | // GetMicroServiceInstances queries the service-center with provider and consumer ID and returns the microservice-instance 694 | func (c *Client) GetMicroServiceInstances(consumerID, providerID string, opts ...CallOption) ([]*discovery.MicroServiceInstance, error) { 695 | copts := &CallOptions{} 696 | for _, opt := range opts { 697 | opt(copts) 698 | } 699 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, providerID, InstancePath), nil, copts) 700 | resp, err := c.httpDo("GET", url, http.Header{ 701 | "X-ConsumerId": []string{consumerID}, 702 | }, nil) 703 | if err != nil { 704 | return nil, err 705 | } 706 | if resp == nil { 707 | return nil, fmt.Errorf("GetMicroServiceInstances failed, response is empty, ConsumerId/ProviderId = %s%s", consumerID, providerID) 708 | } 709 | var body []byte 710 | body, err = ioutil.ReadAll(resp.Body) 711 | if err != nil { 712 | return nil, NewIOException(err) 713 | } 714 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 715 | var response discovery.GetInstancesResponse 716 | err = json.Unmarshal(body, &response) 717 | if err != nil { 718 | return nil, NewJSONException(err, string(body)) 719 | } 720 | return response.Instances, nil 721 | } 722 | return nil, fmt.Errorf("GetMicroServiceInstances failed, ConsumerId/ProviderId: %s%s, response StatusCode: %d, response body: %s", 723 | consumerID, providerID, resp.StatusCode, string(body)) 724 | } 725 | 726 | // GetAllResources retruns all the list of services, instances, providers, consumers in the service-center 727 | func (c *Client) GetAllResources(resource string, opts ...CallOption) ([]*discovery.ServiceDetail, error) { 728 | copts := &CallOptions{} 729 | for _, opt := range opts { 730 | opt(copts) 731 | } 732 | url := c.formatURL(GovernAPIPATH+MicroservicePath, []URLParameter{ 733 | {"options": resource}, 734 | }, copts) 735 | resp, err := c.httpDo("GET", url, nil, nil) 736 | if err != nil { 737 | return nil, err 738 | } 739 | if resp == nil { 740 | return nil, errors.New("GetAllResources failed, response is empty") 741 | } 742 | var body []byte 743 | body, err = ioutil.ReadAll(resp.Body) 744 | if err != nil { 745 | return nil, NewIOException(err) 746 | } 747 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 748 | var response discovery.GetServicesInfoResponse 749 | err = json.Unmarshal(body, &response) 750 | if err != nil { 751 | return nil, NewJSONException(err, string(body)) 752 | } 753 | return response.AllServicesDetail, nil 754 | } 755 | return nil, fmt.Errorf("GetAllResources failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 756 | } 757 | 758 | // Health returns the list of all the endpoints of SC with their status 759 | func (c *Client) Health() ([]*discovery.MicroServiceInstance, error) { 760 | url := c.formatURL(MSAPIPath+"/health", nil, nil) 761 | resp, err := c.httpDo("GET", url, nil, nil) 762 | if err != nil { 763 | return nil, err 764 | } 765 | if resp == nil { 766 | return nil, errors.New("query cluster info failed, response is empty") 767 | } 768 | var body []byte 769 | body, err = ioutil.ReadAll(resp.Body) 770 | if err != nil { 771 | return nil, NewIOException(err) 772 | } 773 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 774 | var response discovery.GetInstancesResponse 775 | err = json.Unmarshal(body, &response) 776 | if err != nil { 777 | return nil, NewJSONException(err, string(body)) 778 | } 779 | return response.Instances, nil 780 | } 781 | return nil, fmt.Errorf("query cluster info failed, response StatusCode: %d, response body: %s", 782 | resp.StatusCode, string(body)) 783 | } 784 | 785 | // Heartbeat sends the heartbeat to service-center for particular service-instance 786 | func (c *Client) Heartbeat(microServiceID, microServiceInstanceID string) (bool, error) { 787 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 788 | InstancePath, microServiceInstanceID, HeartbeatPath), nil, nil) 789 | resp, err := c.httpDo("PUT", url, nil, nil) 790 | if err != nil { 791 | return false, err 792 | } 793 | if resp == nil { 794 | return false, fmt.Errorf("heartbeat failed, response is empty, MicroServiceId/MicroServiceInstanceId: %s%s", microServiceID, microServiceInstanceID) 795 | } 796 | if resp.StatusCode != http.StatusOK { 797 | body, err := ioutil.ReadAll(resp.Body) 798 | if err != nil { 799 | return false, NewIOException(err) 800 | } 801 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 802 | } 803 | return true, nil 804 | } 805 | 806 | // WSHeartbeat creates a web socket connection to service-center to send heartbeat. 807 | // It relies on the ping pong mechanism of websocket to ensure the heartbeat, which is maintained by goroutines. 808 | // After the connection is established, the communication fails and will be retried continuously. The retrial time increases exponentially. 809 | // The callback function is used to re-register the instance. 810 | func (c *Client) WSHeartbeat(microServiceID, microServiceInstanceID string, callback func()) error { 811 | err := c.setupWSConnection(microServiceID, microServiceInstanceID) 812 | if err != nil { 813 | return err 814 | } 815 | go func() { 816 | resetConn := func() error { 817 | return c.setupWSConnection(microServiceID, microServiceInstanceID) 818 | } 819 | for { 820 | conn := c.conns[microServiceInstanceID] 821 | _, _, err = conn.ReadMessage() 822 | if err != nil { 823 | openlog.Error(err.Error()) 824 | closeErr := conn.Close() 825 | if closeErr != nil { 826 | openlog.Error(fmt.Sprintf("failed to close websocket connection %s", closeErr.Error())) 827 | } 828 | if websocket.IsCloseError(err, discovery.ErrWebsocketInstanceNotExists) { 829 | // If the instance does not exist, it is closed normally and should be re-registered 830 | callback() 831 | } 832 | // reconnection 833 | err = backoff.RetryNotify( 834 | resetConn, 835 | backoff.NewExponentialBackOff(), 836 | func(err error, duration time.Duration) { 837 | openlog.Error(fmt.Sprintf("failed err: %s,and it will be executed again in %v", err.Error(), duration)) 838 | }) 839 | } 840 | } 841 | }() 842 | return nil 843 | } 844 | 845 | // setupWSConnection create websocket connection and assign it to the map of the connection 846 | func (c *Client) setupWSConnection(microServiceID, microServiceInstanceID string) error { 847 | scheme := "wss" 848 | if !c.opt.EnableSSL { 849 | scheme = "ws" 850 | } 851 | 852 | u := url.URL{ 853 | Scheme: scheme, 854 | Host: c.GetAddress(), 855 | Path: fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 856 | InstancePath, microServiceInstanceID, "/heartbeat"), 857 | } 858 | 859 | conn, _, err := c.dialWebsocket(&u) 860 | if err != nil { 861 | openlog.Error(fmt.Sprintf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error())) 862 | return err 863 | } 864 | c.conns[microServiceInstanceID] = conn 865 | openlog.Info(fmt.Sprintf("%s's websocket connection established successfully", microServiceInstanceID)) 866 | return nil 867 | } 868 | 869 | // UnregisterMicroServiceInstance un-registers the microservice instance from the service-center 870 | func (c *Client) UnregisterMicroServiceInstance(microServiceID, microServiceInstanceID string) (bool, error) { 871 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s", MSAPIPath, MicroservicePath, microServiceID, 872 | InstancePath, microServiceInstanceID), nil, nil) 873 | resp, err := c.httpDo("DELETE", url, nil, nil) 874 | if err != nil { 875 | return false, err 876 | } 877 | if resp == nil { 878 | return false, fmt.Errorf("unregister instance failed, response is empty, MicroServiceId/MicroServiceInstanceId: %s/%s", microServiceID, microServiceInstanceID) 879 | } 880 | if resp.StatusCode != http.StatusOK { 881 | body, err := ioutil.ReadAll(resp.Body) 882 | if err != nil { 883 | return false, NewIOException(err) 884 | } 885 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 886 | } 887 | return true, nil 888 | } 889 | 890 | // UnregisterMicroService un-registers the microservice from the service-center 891 | func (c *Client) UnregisterMicroService(microServiceID string) (bool, error) { 892 | url := c.formatURL(fmt.Sprintf("%s%s/%s", MSAPIPath, MicroservicePath, microServiceID), []URLParameter{ 893 | {"force": "1"}, 894 | }, nil) 895 | resp, err := c.httpDo("DELETE", url, nil, nil) 896 | if err != nil { 897 | return false, err 898 | } 899 | if resp == nil { 900 | return false, fmt.Errorf("UnregisterMicroService failed, response is empty, MicroServiceId: %s", microServiceID) 901 | } 902 | if resp.StatusCode != http.StatusOK { 903 | body, err := ioutil.ReadAll(resp.Body) 904 | if err != nil { 905 | return false, NewIOException(err) 906 | } 907 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 908 | } 909 | return true, nil 910 | } 911 | 912 | // UpdateMicroServiceInstanceStatus updates the microservicve instance status in service-center 913 | func (c *Client) UpdateMicroServiceInstanceStatus(microServiceID, microServiceInstanceID, status string) (bool, error) { 914 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 915 | InstancePath, microServiceInstanceID, StatusPath), []URLParameter{ 916 | {"value": status}, 917 | }, nil) 918 | resp, err := c.httpDo("PUT", url, nil, nil) 919 | if err != nil { 920 | return false, err 921 | } 922 | if resp == nil { 923 | return false, fmt.Errorf("UpdateMicroServiceInstanceStatus failed, response is empty, MicroServiceId/MicroServiceInstanceId/status: %s%s%s", 924 | microServiceID, microServiceInstanceID, status) 925 | } 926 | if resp.StatusCode != http.StatusOK { 927 | body, err := ioutil.ReadAll(resp.Body) 928 | if err != nil { 929 | return false, NewIOException(err) 930 | } 931 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 932 | } 933 | return true, nil 934 | } 935 | 936 | // UpdateMicroServiceInstanceProperties updates the microserviceinstance prooperties in the service-center 937 | func (c *Client) UpdateMicroServiceInstanceProperties(microServiceID, microServiceInstanceID string, 938 | microServiceInstance *discovery.MicroServiceInstance) (bool, error) { 939 | if microServiceInstance.Properties == nil { 940 | return false, errors.New("invalid request parameter") 941 | } 942 | request := discovery.RegisterInstanceRequest{ 943 | Instance: microServiceInstance, 944 | } 945 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, InstancePath, microServiceInstanceID, PropertiesPath), nil, nil) 946 | body, err := json.Marshal(request.Instance) 947 | if err != nil { 948 | return false, NewJSONException(err, string(body)) 949 | } 950 | 951 | resp, err := c.httpDo("PUT", url, nil, body) 952 | 953 | if err != nil { 954 | return false, err 955 | } 956 | if resp == nil { 957 | return false, fmt.Errorf("UpdateMicroServiceInstanceProperties failed, response is empty, MicroServiceId/microServiceInstanceID: %s/%s", 958 | microServiceID, microServiceInstanceID) 959 | } 960 | if resp.StatusCode != http.StatusOK { 961 | body, err := ioutil.ReadAll(resp.Body) 962 | if err != nil { 963 | return false, NewIOException(err) 964 | } 965 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 966 | } 967 | return true, nil 968 | } 969 | 970 | // UpdateMicroServiceProperties updates the microservice properties in the servive-center 971 | func (c *Client) UpdateMicroServiceProperties(microServiceID string, microService *discovery.MicroService) (bool, error) { 972 | if microService.Properties == nil { 973 | return false, errors.New("invalid request parameter") 974 | } 975 | request := &discovery.CreateServiceRequest{ 976 | Service: microService, 977 | } 978 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, PropertiesPath), nil, nil) 979 | body, err := json.Marshal(request.Service) 980 | if err != nil { 981 | return false, NewJSONException(err, string(body)) 982 | } 983 | 984 | resp, err := c.httpDo("PUT", url, nil, body) 985 | 986 | if err != nil { 987 | return false, err 988 | } 989 | if resp == nil { 990 | return false, fmt.Errorf("UpdateMicroServiceProperties failed, response is empty, MicroServiceId: %s", microServiceID) 991 | } 992 | if resp.StatusCode != http.StatusOK { 993 | body, err := ioutil.ReadAll(resp.Body) 994 | if err != nil { 995 | return false, NewIOException(err) 996 | } 997 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 998 | } 999 | return true, nil 1000 | } 1001 | 1002 | // Close closes the connection with Service-Center 1003 | func (c *Client) Close() error { 1004 | c.mutex.Lock() 1005 | defer c.mutex.Unlock() 1006 | for k, v := range c.conns { 1007 | err := v.Close() 1008 | if err != nil { 1009 | return fmt.Errorf("error:%s, microServiceID = %s", err.Error(), k) 1010 | } 1011 | delete(c.conns, k) 1012 | } 1013 | c.pool.Close() 1014 | return nil 1015 | } 1016 | 1017 | func (c *Client) WatchMicroServiceWithExtraHandle(microServiceID string, callback func(e *MicroServiceInstanceChangedEvent), 1018 | extraHandle func(action string, opts ...CallOption)) error { 1019 | openlog.Info(fmt.Sprintf("WatchMicroServiceWithExtraHandle, microServiceID:%s", microServiceID)) 1020 | c.mutex.Lock() 1021 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1022 | openlog.Info(fmt.Sprintf("WatchMicroServiceWithExtraHandle watch, microServiceID:%s", microServiceID)) 1023 | c.watchers[microServiceID] = true 1024 | scheme := "wss" 1025 | if !c.opt.EnableSSL { 1026 | scheme = "ws" 1027 | } 1028 | host := c.GetAddress() 1029 | u := url.URL{ 1030 | Scheme: scheme, 1031 | Host: host, 1032 | Path: fmt.Sprintf("%s%s/%s%s", MSAPIPath, 1033 | MicroservicePath, microServiceID, WatchPath), 1034 | } 1035 | conn, _, err := c.dialWebsocket(&u) 1036 | if err != nil { 1037 | c.watchers[microServiceID] = false 1038 | c.mutex.Unlock() 1039 | return fmt.Errorf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error()) 1040 | } 1041 | 1042 | c.conns[microServiceID] = conn 1043 | // After successfully subscribing to the service, pull the dependency again. 1044 | // This prevents the event from not being notified after one of the dual engines fails and the other has no dependencies. 1045 | extraHandle("watchSucceed", WithAddress(host)) 1046 | go func() { 1047 | for { 1048 | messageType, message, err := conn.ReadMessage() 1049 | if err != nil { 1050 | openlog.Error(fmt.Sprintf("%s:%s", "conn.ReadMessage()", err.Error())) 1051 | break 1052 | } 1053 | if messageType == websocket.TextMessage { 1054 | var response MicroServiceInstanceChangedEvent 1055 | err := json.Unmarshal(message, &response) 1056 | if err != nil { 1057 | if strings.Contains(string(message), "service does not exist") { 1058 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response), message", string(message))) 1059 | c.mutex.Lock() 1060 | delete(c.conns, microServiceID) 1061 | delete(c.watchers, microServiceID) 1062 | c.mutex.Unlock() 1063 | openlog.Info(fmt.Sprintf("delete conn, microServiceID:%s", microServiceID)) 1064 | extraHandle("serviceNotExist") 1065 | return 1066 | } 1067 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response), message", string(message))) 1068 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response)", err.Error())) 1069 | break 1070 | } 1071 | callback(&response) 1072 | } 1073 | } 1074 | err = conn.Close() 1075 | if err != nil { 1076 | openlog.Error(fmt.Sprintf("%s:%s", "conn.Close()", err.Error())) 1077 | } 1078 | c.mutex.Lock() 1079 | delete(c.conns, microServiceID) 1080 | delete(c.watchers, microServiceID) 1081 | c.mutex.Unlock() 1082 | openlog.Info(fmt.Sprintf("conn stop, microServiceID:%s", microServiceID)) 1083 | c.startBackOffWithExtraHandle(microServiceID, callback, extraHandle) 1084 | }() 1085 | } 1086 | c.mutex.Unlock() 1087 | return nil 1088 | } 1089 | 1090 | func (c *Client) startBackOffWithExtraHandle(microServiceID string, callback func(*MicroServiceInstanceChangedEvent), 1091 | extraHandle func(action string, opts ...CallOption)) { 1092 | boff := &backoff.ExponentialBackOff{ 1093 | InitialInterval: 1000 * time.Millisecond, 1094 | RandomizationFactor: backoff.DefaultRandomizationFactor, 1095 | Multiplier: backoff.DefaultMultiplier, 1096 | MaxInterval: 30000 * time.Millisecond, 1097 | MaxElapsedTime: 0, 1098 | Clock: backoff.SystemClock, 1099 | } 1100 | operation := func() error { 1101 | c.mutex.Lock() 1102 | c.watchers[microServiceID] = false 1103 | c.GetAddress() 1104 | c.mutex.Unlock() 1105 | err := c.WatchMicroServiceWithExtraHandle(microServiceID, callback, extraHandle) 1106 | if err != nil { 1107 | openlog.Error(fmt.Sprintf("%s:%s", "startBackOffWithExtraHandle:WatchMicroServiceWithExtraHandle error", err.Error())) 1108 | return err 1109 | } 1110 | return nil 1111 | } 1112 | 1113 | err := backoff.Retry(operation, boff) 1114 | if err == nil { 1115 | return 1116 | } 1117 | openlog.Error(fmt.Sprintf("%s:%s", "backoff.Retry", err.Error())) 1118 | } 1119 | 1120 | // WatchMicroService creates a web socket connection to service-center to keep a watch on the providers for a micro-service 1121 | func (c *Client) WatchMicroService(microServiceID string, callback func(*MicroServiceInstanceChangedEvent)) error { 1122 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1123 | c.mutex.Lock() 1124 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1125 | c.watchers[microServiceID] = true 1126 | scheme := "wss" 1127 | if !c.opt.EnableSSL { 1128 | scheme = "ws" 1129 | } 1130 | u := url.URL{ 1131 | Scheme: scheme, 1132 | Host: c.GetAddress(), 1133 | Path: fmt.Sprintf("%s%s/%s%s", MSAPIPath, 1134 | MicroservicePath, microServiceID, WatchPath), 1135 | } 1136 | conn, _, err := c.dialWebsocket(&u) 1137 | if err != nil { 1138 | c.watchers[microServiceID] = false 1139 | c.mutex.Unlock() 1140 | return fmt.Errorf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error()) 1141 | } 1142 | 1143 | c.conns[microServiceID] = conn 1144 | go func() { 1145 | for { 1146 | messageType, message, err := conn.ReadMessage() 1147 | if err != nil { 1148 | break 1149 | } 1150 | if messageType == websocket.TextMessage { 1151 | var response MicroServiceInstanceChangedEvent 1152 | err := json.Unmarshal(message, &response) 1153 | if err != nil { 1154 | break 1155 | } 1156 | callback(&response) 1157 | } 1158 | } 1159 | err = conn.Close() 1160 | if err != nil { 1161 | openlog.Error(err.Error()) 1162 | } 1163 | c.mutex.Lock() 1164 | delete(c.conns, microServiceID) 1165 | c.mutex.Unlock() 1166 | c.startBackOff(microServiceID, callback) 1167 | }() 1168 | } 1169 | c.mutex.Unlock() 1170 | } 1171 | return nil 1172 | } 1173 | 1174 | func (c *Client) GetAddress() string { 1175 | return c.pool.GetAvailableAddress() 1176 | } 1177 | 1178 | func (c *Client) startBackOff(microServiceID string, callback func(*MicroServiceInstanceChangedEvent)) { 1179 | boff := &backoff.ExponentialBackOff{ 1180 | InitialInterval: 1000 * time.Millisecond, 1181 | RandomizationFactor: backoff.DefaultRandomizationFactor, 1182 | Multiplier: backoff.DefaultMultiplier, 1183 | MaxInterval: 30000 * time.Millisecond, 1184 | MaxElapsedTime: 0, 1185 | Clock: backoff.SystemClock, 1186 | } 1187 | operation := func() error { 1188 | c.mutex.Lock() 1189 | c.watchers[microServiceID] = false 1190 | c.GetAddress() 1191 | c.mutex.Unlock() 1192 | err := c.WatchMicroService(microServiceID, callback) 1193 | if err != nil { 1194 | return err 1195 | } 1196 | return nil 1197 | } 1198 | 1199 | err := backoff.Retry(operation, boff) 1200 | if err == nil { 1201 | return 1202 | } 1203 | } 1204 | 1205 | // GetToken generate token according to user-password 1206 | func (c *Client) GetToken(a *rbac.AuthUser) (string, error) { 1207 | return c.GetTokenWithExpiration(a, "") 1208 | } 1209 | 1210 | // GetTokenWithExpiration expiration: 15m~24h, default 12h 1211 | func (c *Client) GetTokenWithExpiration(a *rbac.AuthUser, expiration string) (string, error) { 1212 | request := rbac.Account{ 1213 | Name: a.Username, 1214 | Password: a.Password, 1215 | TokenExpirationTime: expiration, 1216 | } 1217 | body, err := json.Marshal(request) 1218 | if err != nil { 1219 | return "", NewJSONException(err, "parse the username or password failed") 1220 | } 1221 | 1222 | tokenUrl := c.formatURL(TokenPath, nil, nil) 1223 | resp, err := c.httpDo(http.MethodPost, tokenUrl, nil, body) 1224 | if err != nil { 1225 | return "", err 1226 | } 1227 | if resp == nil { 1228 | return "", fmt.Errorf("user %s generate token failed: ", a.Username) 1229 | } 1230 | body, err = ioutil.ReadAll(resp.Body) 1231 | if err != nil { 1232 | return "", NewIOException(err) 1233 | } 1234 | 1235 | if resp.StatusCode == http.StatusOK { 1236 | var response rbac.Token 1237 | err = json.Unmarshal(body, &response) 1238 | if err != nil { 1239 | return "", NewJSONException(err, string(body)) 1240 | } 1241 | return response.TokenStr, nil 1242 | } 1243 | return "", fmt.Errorf("user %s generate token failed, response status code: %d", a.Username, resp.StatusCode) 1244 | } 1245 | 1246 | func (c *Client) CheckPeerStatus() (*PeerStatusResp, error) { 1247 | url := c.formatURL(fmt.Sprintf("%s", PeerHealthPath), nil, nil) 1248 | resp, err := c.httpDo(http.MethodGet, url, nil, nil) 1249 | if err != nil { 1250 | return nil, err 1251 | } 1252 | if resp == nil { 1253 | return nil, fmt.Errorf("check the status of peer engine fail") 1254 | } 1255 | body, err := ioutil.ReadAll(resp.Body) 1256 | if err != nil { 1257 | return nil, NewIOException(err) 1258 | } 1259 | if resp.StatusCode != http.StatusOK { 1260 | return nil, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 1261 | } 1262 | 1263 | var response *PeerStatusResp 1264 | err = json.Unmarshal(body, &response) 1265 | if err != nil { 1266 | return nil, NewJSONException(err, string(body)) 1267 | } 1268 | return response, nil 1269 | } 1270 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package sc_test 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "os" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/go-chassis/cari/discovery" 13 | "github.com/go-chassis/cari/rbac" 14 | "github.com/go-chassis/openlog" 15 | "github.com/stretchr/testify/assert" 16 | 17 | "github.com/go-chassis/sc-client" 18 | ) 19 | 20 | func TestClient_RegisterService(t *testing.T) { 21 | c, err := sc.NewClient( 22 | sc.Options{ 23 | Endpoints: []string{"127.0.0.1:30100"}, 24 | }) 25 | assert.NoError(t, err) 26 | 27 | hostname, err := os.Hostname() 28 | assert.NoError(t, err) 29 | 30 | _, err = c.GetAllMicroServices() 31 | assert.NoError(t, err) 32 | 33 | t.Run("given instance with no service id, should return err", func(t *testing.T) { 34 | microServiceInstance := &discovery.MicroServiceInstance{ 35 | Endpoints: []string{"rest://127.0.0.1:3000"}, 36 | HostName: hostname, 37 | Status: sc.MSInstanceUP, 38 | } 39 | s1, err := c.RegisterMicroServiceInstance(microServiceInstance) 40 | assert.Empty(t, s1) 41 | assert.Error(t, err) 42 | 43 | s1, err = c.RegisterMicroServiceInstance(nil) 44 | assert.Empty(t, s1) 45 | assert.Error(t, err) 46 | }) 47 | 48 | t.Run("given wrong service id, should return err", func(t *testing.T) { 49 | msArr, err := c.GetMicroServiceInstances("fakeConsumerID", "fakeProviderID") 50 | assert.Empty(t, msArr) 51 | assert.Error(t, err) 52 | 53 | }) 54 | 55 | t.Run("register service with name only", func(t *testing.T) { 56 | sid, err := c.RegisterService(&discovery.MicroService{ 57 | ServiceName: "simpleService", 58 | }) 59 | assert.NotEmpty(t, sid) 60 | s, err := c.GetMicroService(sid) 61 | assert.NoError(t, err) 62 | assert.Equal(t, "0.0.1", s.Version) 63 | assert.Equal(t, "default", s.AppId) 64 | }) 65 | t.Run("register service with invalid name", func(t *testing.T) { 66 | _, err := c.RegisterService(&discovery.MicroService{ 67 | ServiceName: "simple&Service", 68 | }) 69 | t.Log(err) 70 | assert.Error(t, err) 71 | }) 72 | t.Run("get all apps, not empty", func(t *testing.T) { 73 | apps, err := c.GetAllApplications() 74 | assert.NoError(t, err) 75 | assert.NotEqual(t, 0, len(apps)) 76 | t.Log(len(apps)) 77 | }) 78 | 79 | } 80 | func TestRegistryClient_FindMicroServiceInstances(t *testing.T) { 81 | var sid string 82 | registryClient, err := sc.NewClient( 83 | sc.Options{ 84 | Endpoints: []string{"127.0.0.1:30100"}, 85 | }) 86 | assert.NoError(t, err) 87 | 88 | hostname, err := os.Hostname() 89 | assert.NoError(t, err) 90 | 91 | ms := &discovery.MicroService{ 92 | ServiceName: "scUTServer", 93 | AppId: "default", 94 | Version: "0.0.1", 95 | Schemas: []string{"schema"}, 96 | } 97 | sid, err = registryClient.RegisterService(ms) 98 | if err == sc.ErrMicroServiceExists { 99 | sid, err = registryClient.GetMicroServiceID("default", "scUTServer", "0.0.1", "") 100 | assert.NoError(t, err) 101 | assert.NotNil(t, sid) 102 | } 103 | 104 | err = registryClient.AddSchemas(ms.ServiceId, "schema", "schema") 105 | assert.NoError(t, err) 106 | t.Run("query schema, should return info", func(t *testing.T) { 107 | b, err := registryClient.GetSchema(ms.ServiceId, "schema") 108 | assert.NoError(t, err) 109 | assert.Equal(t, "{\"schema\":\"schema\"}", string(b)) 110 | }) 111 | t.Run("query schema with empty string, should be err", func(t *testing.T) { 112 | _, err := registryClient.GetSchema("", "schema") 113 | assert.Error(t, err) 114 | }) 115 | microServiceInstance := &discovery.MicroServiceInstance{ 116 | ServiceId: sid, 117 | Endpoints: []string{"rest://127.0.0.1:3000"}, 118 | HostName: hostname, 119 | Status: sc.MSInstanceUP, 120 | } 121 | t.Run("unregister instance, should success", func(t *testing.T) { 122 | iid, err := registryClient.RegisterMicroServiceInstance(microServiceInstance) 123 | assert.NoError(t, err) 124 | assert.NotNil(t, iid) 125 | ok, err := registryClient.UnregisterMicroServiceInstance(microServiceInstance.ServiceId, iid) 126 | assert.NoError(t, err) 127 | assert.True(t, ok) 128 | }) 129 | 130 | t.Run("send instance heartbeat (http), should success", func(t *testing.T) { 131 | iid, err := registryClient.RegisterMicroServiceInstance(microServiceInstance) 132 | assert.NoError(t, err) 133 | assert.NotNil(t, iid) 134 | receiveStatus, err := registryClient.Heartbeat(microServiceInstance.ServiceId, iid) 135 | assert.Equal(t, true, receiveStatus) 136 | assert.Nil(t, err) 137 | ok, err := registryClient.UnregisterMicroServiceInstance(microServiceInstance.ServiceId, iid) 138 | assert.NoError(t, err) 139 | assert.True(t, ok) 140 | }) 141 | 142 | t.Run("send instance heartbeat (websocket), should success", func(t *testing.T) { 143 | iid, err := registryClient.RegisterMicroServiceInstance(microServiceInstance) 144 | assert.NoError(t, err) 145 | assert.NotNil(t, iid) 146 | microServiceInstance.InstanceId = iid 147 | callback := func() { 148 | registryClient.RegisterMicroServiceInstance(microServiceInstance) 149 | } 150 | err = registryClient.WSHeartbeat(microServiceInstance.ServiceId, iid, callback) 151 | assert.Nil(t, err) 152 | ok, err := registryClient.UnregisterMicroServiceInstance(microServiceInstance.ServiceId, iid) 153 | assert.NoError(t, err) 154 | assert.True(t, ok) 155 | }) 156 | 157 | var rst *sc.FindMicroServiceInstancesResult 158 | t.Run("register instance and update props, should success", func(t *testing.T) { 159 | iid, err := registryClient.RegisterMicroServiceInstance(microServiceInstance) 160 | assert.NoError(t, err) 161 | assert.NotNil(t, iid) 162 | microServiceInstance.Properties = map[string]string{ 163 | "project": "x"} 164 | ok, err := registryClient.UpdateMicroServiceInstanceProperties(microServiceInstance.ServiceId, 165 | iid, microServiceInstance) 166 | assert.True(t, ok) 167 | assert.NoError(t, err) 168 | rst, err = registryClient.FindInstances(microServiceInstance.ServiceId, 169 | "default", 170 | "scUTServer") 171 | assert.NoError(t, err) 172 | assert.Equal(t, "x", rst.Instances[0].Properties["project"]) 173 | }) 174 | 175 | t.Log("find again, should get no error") 176 | rstInstances, err := registryClient.FindMicroServiceInstances(sid, "default", "scUTServer", "0%2B") 177 | assert.NoError(t, err) 178 | assert.NotEmpty(t, rstInstances) 179 | 180 | t.Log("find again, should get no error") 181 | newRst, err := registryClient.FindInstances(sid, "default", "scUTServer") 182 | assert.NoError(t, err) 183 | assert.NotEmpty(t, newRst.Revision) 184 | assert.NotEmpty(t, newRst.Instances) 185 | assert.Equal(t, newRst.Revision, rst.Revision) 186 | 187 | t.Log("find again without revision, should get nil error") 188 | newRst, err = registryClient.FindInstances(sid, "default", "scUTServer", sc.WithoutRevision()) 189 | assert.NoError(t, err) 190 | assert.NotEmpty(t, newRst.Revision) 191 | assert.NotEmpty(t, newRst.Instances) 192 | assert.Equal(t, newRst.Revision, rst.Revision) 193 | 194 | t.Log("find again with different revision, should get nil error") 195 | newRst, err = registryClient.FindInstances(sid, "default", "scUTServer", sc.WithRevision("123")) 196 | assert.NoError(t, err) 197 | assert.NotEmpty(t, newRst.Revision) 198 | assert.NotEmpty(t, newRst.Instances) 199 | 200 | t.Log("find again with same revision, should get ErrNotModified") 201 | newRst, err = registryClient.FindInstances(sid, "default", "scUTServer", sc.WithRevision(newRst.Revision)) 202 | assert.Equal(t, sc.ErrNotModified, err) 203 | 204 | t.Log("register new and find") 205 | microServiceInstance2 := &discovery.MicroServiceInstance{ 206 | ServiceId: sid, 207 | Endpoints: []string{"rest://127.0.0.1:3001"}, 208 | HostName: hostname + "1", 209 | Status: sc.MSInstanceUP, 210 | } 211 | _, err = registryClient.RegisterMicroServiceInstance(microServiceInstance2) 212 | time.Sleep(3 * time.Second) 213 | _, err = registryClient.FindInstances(sid, "default", "scUTServer") 214 | assert.NoError(t, err) 215 | 216 | _, err = registryClient.FindInstances(sid, "AppIdNotExists", "ServerNotExists") 217 | assert.Equal(t, sc.ErrMicroServiceNotExists, err) 218 | 219 | f := &discovery.FindService{ 220 | Service: &discovery.MicroServiceKey{ 221 | ServiceName: "scUTServer", 222 | AppId: "default", 223 | Version: "0.0.1", 224 | }, 225 | } 226 | fs := []*discovery.FindService{f} 227 | instances, err := registryClient.BatchFindInstances(sid, fs) 228 | t.Log(instances) 229 | assert.NoError(t, err) 230 | 231 | f1 := &discovery.FindService{ 232 | Service: &discovery.MicroServiceKey{ 233 | ServiceName: "empty", 234 | AppId: "default", 235 | Version: "0.0.1", 236 | }, 237 | } 238 | fs = []*discovery.FindService{f1} 239 | instances, err = registryClient.BatchFindInstances(sid, fs) 240 | t.Log(instances) 241 | assert.NoError(t, err) 242 | 243 | f2 := &discovery.FindService{ 244 | Service: &discovery.MicroServiceKey{ 245 | ServiceName: "empty", 246 | AppId: "default", 247 | Version: "latest", 248 | }, 249 | } 250 | fs = []*discovery.FindService{f} 251 | instances, err = registryClient.BatchFindInstances(sid, fs) 252 | t.Log(instances) 253 | assert.NoError(t, err) 254 | 255 | fs = []*discovery.FindService{f2, f} 256 | instances, err = registryClient.BatchFindInstances(sid, fs) 257 | t.Log(instances) 258 | assert.NoError(t, err) 259 | 260 | fs = []*discovery.FindService{} 261 | instances, err = registryClient.BatchFindInstances(sid, fs) 262 | assert.Equal(t, sc.ErrEmptyCriteria, err) 263 | } 264 | func TestClient_Health(t *testing.T) { 265 | c, err := sc.NewClient( 266 | sc.Options{ 267 | Endpoints: []string{"127.0.0.1:30100"}, 268 | }) 269 | assert.NoError(t, err) 270 | _, err = c.Health() 271 | assert.NoError(t, err) 272 | } 273 | 274 | func TestClient_CheckPeerStatus(t *testing.T) { 275 | c, err := sc.NewClient( 276 | sc.Options{ 277 | Endpoints: []string{"127.0.0.1:30100"}, 278 | }) 279 | assert.NoError(t, err) 280 | _, err = c.CheckPeerStatus() 281 | assert.Equal(t, "Common exception", err.(*sc.RegistryException).Title) 282 | } 283 | 284 | func TestClient_Auth(t *testing.T) { 285 | _, err := os.Hostname() 286 | if err != nil { 287 | openlog.Error("Get hostname failed.") 288 | return 289 | } 290 | opt := &sc.Options{} 291 | if !opt.EnableAuth { 292 | // service-center need to open the rbac module 293 | return 294 | } 295 | // root account login 296 | c, err := sc.NewClient( 297 | sc.Options{ 298 | Endpoints: []string{"127.0.0.1:30100"}, 299 | EnableAuth: true, 300 | AuthUser: &rbac.AuthUser{ 301 | Username: "root", 302 | Password: "Complicated_password1", 303 | }, 304 | }) 305 | assert.NoError(t, err) 306 | 307 | httpHeader := c.GetDefaultHeaders() 308 | assert.NotEmpty(t, httpHeader) 309 | 310 | t.Run("get the root account token", func(t *testing.T) { 311 | root_token, err := c.GetToken(&rbac.AuthUser{ 312 | Username: "root", 313 | Password: "Complicated_password1", 314 | }) 315 | assert.NoError(t, err) 316 | assert.NotEmpty(t, root_token) 317 | }) 318 | } 319 | 320 | func TestClient_DataRace(t *testing.T) { 321 | c, err := sc.NewClient( 322 | sc.Options{ 323 | Endpoints: []string{"127.0.0.1:30100"}, 324 | }) 325 | assert.NoError(t, err) 326 | 327 | MSList, err := c.GetAllMicroServices() 328 | assert.NotEmpty(t, MSList) 329 | assert.NoError(t, err) 330 | 331 | t.Run("should not race detected", func(t *testing.T) { 332 | wg := sync.WaitGroup{} 333 | wg.Add(2) 334 | go func() { 335 | sc.NewClient( 336 | sc.Options{ 337 | Endpoints: []string{"127.0.0.1:30100"}, 338 | }) 339 | wg.Done() 340 | }() 341 | 342 | go func() { 343 | c.GetAllMicroServices() 344 | wg.Done() 345 | }() 346 | 347 | wg.Wait() 348 | }) 349 | } 350 | 351 | func TestClient_SyncEndpoints(t *testing.T) { 352 | os.Setenv("CHASSIS_SC_HEALTH_CHECK_INTERVAL", "1") 353 | 354 | anotherScServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 355 | writer.WriteHeader(http.StatusOK) 356 | return 357 | })) 358 | 359 | scServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 360 | resp := &discovery.GetInstancesResponse{ 361 | Instances: []*discovery.MicroServiceInstance{ 362 | { 363 | Endpoints: []string{"rest://" + anotherScServer.Listener.Addr().String()}, 364 | HostName: "test", 365 | Status: sc.MSInstanceUP, 366 | DataCenterInfo: &discovery.DataCenterInfo{ 367 | Name: "engine1", 368 | Region: "cn", 369 | AvailableZone: "az1", 370 | }, 371 | }, 372 | }, 373 | } 374 | instanceBytes, err := json.Marshal(resp) 375 | if err != nil { 376 | writer.Write([]byte(err.Error())) 377 | writer.WriteHeader(http.StatusInternalServerError) 378 | } 379 | writer.Write(instanceBytes) 380 | writer.WriteHeader(http.StatusOK) 381 | return 382 | })) 383 | 384 | c, err := sc.NewClient( 385 | sc.Options{ 386 | Endpoints: []string{scServer.Listener.Addr().String()}, 387 | }) 388 | assert.NoError(t, err) 389 | assert.Equal(t, scServer.Listener.Addr().String(), c.GetAddress()) // default 390 | 391 | err = c.SyncEndpoints() 392 | assert.Equal(t, scServer.Listener.Addr().String(), c.GetAddress()) 393 | 394 | scServer.Close() 395 | time.Sleep(3*time.Second + 100*time.Millisecond) 396 | // sc stopped, should use the synced address 397 | assert.Equal(t, anotherScServer.Listener.Addr().String(), c.GetAddress()) 398 | } 399 | -------------------------------------------------------------------------------- /exception.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | // RegistryException structure contains message and error information for the exception caused by service-center 8 | type RegistryException struct { 9 | Title string 10 | Err error 11 | Message string 12 | } 13 | 14 | // Error gets the Error message from the Error 15 | func (e *RegistryException) Error() string { 16 | if e.Err == nil { 17 | return fmt.Sprintf("%s(%s)", e.Title, e.Message) 18 | } 19 | return fmt.Sprintf("%s(%s), %s", e.Title, e.Err.Error(), e.Message) 20 | } 21 | 22 | func formatMessage(args []interface{}) string { 23 | if len(args) == 0 { 24 | return "" 25 | } 26 | format, ok := args[0].(string) 27 | if !ok { 28 | return fmt.Sprintf("%v", args) 29 | } 30 | return fmt.Sprintf(format, args[1:]...) 31 | } 32 | 33 | func newException(t string, e error, message string) *RegistryException { 34 | return &RegistryException{ 35 | Title: t, 36 | Err: e, 37 | Message: message, 38 | } 39 | } 40 | 41 | // NewCommonException creates a generic exception 42 | func NewCommonException(format string, args ...interface{}) error { 43 | return newException("Common exception", nil, fmt.Sprintf(format, args...)) 44 | } 45 | 46 | // NewJSONException creates a JSON exception 47 | func NewJSONException(e error, args ...interface{}) error { 48 | return newException("JSON exception", e, formatMessage(args)) 49 | } 50 | 51 | // NewIOException create and IO exception 52 | func NewIOException(e error, args ...interface{}) error { 53 | return newException("IO exception", e, formatMessage(args)) 54 | } 55 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-chassis/sc-client 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/cenkalti/backoff/v4 v4.1.1 7 | github.com/go-chassis/cari v0.9.1-0.20250703032518-a1c3e9de70ad 8 | github.com/go-chassis/foundation v0.4.0 9 | github.com/go-chassis/openlog v1.1.3 10 | github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7 11 | github.com/patrickmn/go-cache v2.1.0+incompatible 12 | github.com/stretchr/testify v1.7.2 13 | ) 14 | 15 | require ( 16 | github.com/davecgh/go-spew v1.1.1 // indirect 17 | github.com/deckarep/golang-set v1.7.1 // indirect 18 | github.com/gogo/protobuf v1.3.2 // indirect 19 | github.com/karlseguin/ccache/v2 v2.0.8 // indirect 20 | github.com/kr/pretty v0.1.0 // indirect 21 | github.com/kr/text v0.2.0 // indirect 22 | github.com/pmezard/go-difflib v1.0.0 // indirect 23 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 24 | gopkg.in/yaml.v3 v3.0.1 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cenkalti/backoff/v4 v4.1.1 h1:G2HAfAmvm/GcKan2oOQpBXOd2tT2G57ZnZGWa1PxPBQ= 2 | github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 3 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/deckarep/golang-set v1.7.1 h1:SCQV0S6gTtp6itiFrTqI+pfmJ4LN85S1YzhDf9rTHJQ= 8 | github.com/deckarep/golang-set v1.7.1/go.mod h1:93vsz/8Wt4joVM7c2AVqh+YRMiUSc14yDtF28KmMOgQ= 9 | github.com/go-chassis/cari v0.9.1-0.20250703032518-a1c3e9de70ad h1:7grqGrSTj4++a6cOh+s2pBhp6TXsoTYTkyBFMZ5jcE4= 10 | github.com/go-chassis/cari v0.9.1-0.20250703032518-a1c3e9de70ad/go.mod h1:ibqLyh+Q+1n9PlldW3glD9G+2s/yeSyVMCCkQWKRwuE= 11 | github.com/go-chassis/foundation v0.4.0 h1:z0xETnSxF+vRXWjoIhOdzt6rywjZ4sB++utEl4YgWEY= 12 | github.com/go-chassis/foundation v0.4.0/go.mod h1:6NsIUaHghTFRGfCBcZN011zl196F6OR5QvD9N+P4oWU= 13 | github.com/go-chassis/openlog v1.1.3 h1:XqIOvZ8YPJ9o9lLtLBskQNNWolK5kC6a4Sv7r4s9sZ4= 14 | github.com/go-chassis/openlog v1.1.3/go.mod h1:+eYCADVxWyJkwsFMUBrMxyQlNqW+UUsCxvR2LrYZUaA= 15 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 16 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 17 | github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig= 18 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 19 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 20 | github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7 h1:L89uC9ATI61/V2eNgZYtQHyjjyjEplemB+aky4HdyzQ= 21 | github.com/gorilla/websocket v1.4.3-0.20210424162022-e8629af678b7/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 22 | github.com/karlseguin/ccache/v2 v2.0.8 h1:lT38cE//uyf6KcFok0rlgXtGFBWxkI6h/qg4tbFyDnA= 23 | github.com/karlseguin/ccache/v2 v2.0.8/go.mod h1:2BDThcfQMf/c0jnZowt16eW405XIqZPavt+HoYEtcxQ= 24 | github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003 h1:vJ0Snvo+SLMY72r5J4sEfkuE7AFbixEP2qRbEcum/wA= 25 | github.com/karlseguin/expect v1.0.2-0.20190806010014-778a5f0c6003/go.mod h1:zNBxMY8P21owkeogJELCLeHIt+voOSduHYTFUbwRAV8= 26 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 27 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 28 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 29 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 30 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 31 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 32 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 33 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 34 | github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= 35 | github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= 36 | github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 37 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 38 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 39 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 40 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= 42 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 43 | github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ= 44 | github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM= 45 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 46 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 47 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 48 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 49 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 50 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 51 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 52 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 53 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 54 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 55 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 56 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 58 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 59 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 60 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 61 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 62 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 63 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 64 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 65 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 66 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 67 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 68 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 69 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 70 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 71 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 72 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 73 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 74 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 75 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 76 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 77 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 78 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 79 | -------------------------------------------------------------------------------- /options.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/go-chassis/cari/rbac" 10 | ) 11 | 12 | // Options is the list of dynamic parameter's which can be passed to the Client while creating a new client 13 | type Options struct { 14 | DiffAzEndpoints []string 15 | Endpoints []string 16 | EnableSSL bool 17 | Timeout time.Duration 18 | TLSConfig *tls.Config 19 | // Other options can be stored in a context 20 | Context context.Context 21 | Compressed bool 22 | Verbose bool 23 | EnableAuth bool 24 | AuthUser *rbac.AuthUser 25 | AuthToken string 26 | TokenExpiration time.Duration 27 | SignRequest func(*http.Request) error 28 | } 29 | 30 | // CallOptions is options when you call a API 31 | type CallOptions struct { 32 | WithoutRevision bool 33 | Revision string 34 | WithGlobal bool 35 | Address string 36 | } 37 | 38 | // WithoutRevision ignore current revision number 39 | func WithoutRevision() CallOption { 40 | return func(o *CallOptions) { 41 | o.WithoutRevision = true 42 | } 43 | } 44 | 45 | // WithRevision query resources with the revision 46 | func WithRevision(revision string) CallOption { 47 | return func(o *CallOptions) { 48 | o.Revision = revision 49 | } 50 | } 51 | 52 | // WithGlobal query resources include other aggregated SC 53 | func WithGlobal() CallOption { 54 | return func(o *CallOptions) { 55 | o.WithGlobal = true 56 | } 57 | } 58 | 59 | // WithAddress query resources with the sc address 60 | func WithAddress(address string) CallOption { 61 | return func(o *CallOptions) { 62 | o.Address = address 63 | } 64 | } 65 | 66 | // CallOption is receiver for options and chang the attribute of it 67 | type CallOption func(*CallOptions) 68 | -------------------------------------------------------------------------------- /options_test.go: -------------------------------------------------------------------------------- 1 | package sc_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/go-chassis/sc-client" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestWithGlobal(t *testing.T) { 11 | o := sc.WithGlobal() 12 | opts := &sc.CallOptions{} 13 | o(opts) 14 | assert.True(t, opts.WithGlobal) 15 | } 16 | -------------------------------------------------------------------------------- /response.go: -------------------------------------------------------------------------------- 1 | package sc 2 | 3 | import ( 4 | "github.com/go-chassis/cari/discovery" 5 | ) 6 | 7 | const ( 8 | // EventCreate is a constant of type string 9 | EventCreate string = "CREATE" 10 | // EventUpdate is a constant of type string 11 | EventUpdate string = "UPDATE" 12 | // EventDelete is a constant of type string 13 | EventDelete string = "DELETE" 14 | // EventError is a constant of type string 15 | EventError string = "ERROR" 16 | // MicorserviceUp is a constant of type string 17 | MicorserviceUp string = "UP" 18 | // MicroserviceDown is a constant of type string 19 | MicroserviceDown string = "DOWN" 20 | // MSInstanceUP is a constant of type string 21 | MSInstanceUP string = "UP" 22 | // MSIinstanceDown is a constant of type string 23 | MSIinstanceDown string = "DOWN" 24 | // CheckByHeartbeat is a constant of type string 25 | CheckByHeartbeat string = "push" 26 | // DefaultLeaseRenewalInterval is a constant of type int which declares default lease renewal time 27 | DefaultLeaseRenewalInterval = 30 28 | ) 29 | 30 | // MicroServiceProvideResponse is a struct with provider information 31 | type MicroServiceProvideResponse struct { 32 | Services []*discovery.MicroService `json:"providers,omitempty"` 33 | } 34 | 35 | // MicroServiceInstanceChangedEvent is a struct to store the Changed event information 36 | type MicroServiceInstanceChangedEvent struct { 37 | Action string `protobuf:"bytes,2,opt,name=action" json:"action,omitempty"` 38 | Key *discovery.MicroServiceKey `protobuf:"bytes,3,opt,name=key" json:"key,omitempty"` 39 | Instance *discovery.MicroServiceInstance `protobuf:"bytes,4,opt,name=instance" json:"instance,omitempty"` 40 | } 41 | 42 | type FindMicroServiceInstancesResult struct { 43 | Instances []*discovery.MicroServiceInstance 44 | Revision string 45 | } 46 | -------------------------------------------------------------------------------- /scripts/ci/start_latest_sc.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | kill -9 $(lsof -i:30100 |awk '{print $2}' | tail -n 2) 4 | 5 | echo "delete old contain server-center ...." 6 | docker rm -f service-center 7 | 8 | echo "docker pull servicecomb/service-center images" 9 | docker pull servicecomb/service-center 10 | 11 | docker run -d -p 30100:30100 --name=service-center servicecomb/service-center 12 | sleep 30 -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | // Licensed to the Apache Software Foundation (ASF) under one or more 2 | // contributor license agreements. See the NOTICE file distributed with 3 | // this work for additional information regarding copyright ownership. 4 | // The ASF licenses this file to You under the Apache License, Version 2.0 5 | // (the "License"); you may not use this file except in compliance with 6 | // the License. You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package sc 17 | 18 | import ( 19 | "fmt" 20 | "net/url" 21 | "strings" 22 | ) 23 | 24 | // URLBuilder is the string builder to build request url 25 | type URLBuilder struct { 26 | Protocol string 27 | Host string 28 | Path string 29 | URLParameters []URLParameter 30 | CallOptions *CallOptions 31 | } 32 | 33 | func (b *URLBuilder) encodeParams(params []URLParameter) string { 34 | encoded := []string{} 35 | for _, param := range params { 36 | for k, v := range param { 37 | if k == "" || v == "" { 38 | continue 39 | } 40 | encoded = append(encoded, fmt.Sprintf("%s=%s", k, url.QueryEscape(v))) 41 | } 42 | } 43 | return strings.Join(encoded, "&") 44 | } 45 | 46 | // String is the method to return url string 47 | func (b *URLBuilder) String() string { 48 | querys := b.URLParameters 49 | if b.CallOptions != nil { 50 | if !b.CallOptions.WithoutRevision && len(b.CallOptions.Revision) > 0 { 51 | querys = append(querys, URLParameter{"rev": b.CallOptions.Revision}) 52 | } 53 | if b.CallOptions.WithGlobal { 54 | querys = append(querys, URLParameter{"global": "true"}) 55 | } 56 | } 57 | urlString := fmt.Sprintf("%s://%s%s", b.Protocol, b.Host, b.Path) 58 | queryString := b.encodeParams(querys) 59 | if len(queryString) > 0 { 60 | urlString += "?" + queryString 61 | } 62 | return urlString 63 | } 64 | --------------------------------------------------------------------------------