├── .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 | }) 150 | return c, nil 151 | } 152 | 153 | // Reset the service center client 154 | func (c *Client) Reset(opt Options) error { 155 | c.poolMutex.Lock() 156 | defer c.poolMutex.Unlock() 157 | options := c.buildClientOptions(opt) 158 | var err error 159 | c.client, err = httpclient.New(options) 160 | if err != nil { 161 | return err 162 | } 163 | c.protocol = "https" 164 | if !c.opt.EnableSSL { 165 | c.wsDialer = websocket.DefaultDialer 166 | c.protocol = "http" 167 | } 168 | c.pool.ResetAddress(opt.Endpoints) 169 | return nil 170 | } 171 | 172 | // buildClientOptions build options for http client 173 | func (c *Client) buildClientOptions(opt Options) *httpclient.Options { 174 | options := &httpclient.Options{ 175 | TLSConfig: opt.TLSConfig, 176 | Compressed: opt.Compressed, 177 | RequestTimeout: opt.Timeout, 178 | } 179 | if !opt.EnableAuth { 180 | return options 181 | } 182 | if opt.SignRequest != nil { 183 | options.SignRequest = opt.SignRequest 184 | return options 185 | } 186 | // when the authentication is enabled, the token of automatic renewal is added to the request header 187 | if opt.TokenExpiration == 0 { 188 | opt.TokenExpiration = DefaultTokenExpiration 189 | } 190 | tokenCache := cache.New(opt.TokenExpiration, 1*time.Hour) 191 | options.SignRequest = func(req *http.Request) error { 192 | if req.URL.Path == TokenPath { 193 | return nil 194 | } 195 | if opt.AuthToken != "" { 196 | req.Header.Set(HeaderAuth, "Bearer "+opt.AuthToken) 197 | return nil 198 | } 199 | cachedToken, isFound := tokenCache.Get("token") 200 | if isFound { 201 | req.Header.Set(HeaderAuth, "Bearer "+cachedToken.(string)) 202 | } else { 203 | token, err := c.GetToken(opt.AuthUser) 204 | if err != nil { 205 | return err 206 | } 207 | req.Header.Set(HeaderAuth, "Bearer "+token) 208 | tokenCache.Set("token", token, cache.DefaultExpiration) 209 | } 210 | return nil 211 | } 212 | return options 213 | } 214 | 215 | func (c *Client) updateAPIPath() { 216 | defineOnce.Do(func() { 217 | projectID, isExist := os.LookupEnv(EnvProjectID) 218 | if !isExist { 219 | projectID = "default" 220 | } 221 | MSAPIPath = "/v4/" + projectID + "/registry" 222 | GovernAPIPATH = "/v4/" + projectID + "/govern" 223 | }) 224 | } 225 | 226 | func (c *Client) CheckReadiness() int { 227 | return c.pool.CheckReadiness() 228 | } 229 | 230 | // SyncEndpoints gets the endpoints of service-center in the cluster 231 | // if your service center cluster is not behind a load balancing service like ELB,nginx etc 232 | // then you can use this function 233 | func (c *Client) SyncEndpoints() error { 234 | c.poolMutex.Lock() 235 | defer c.poolMutex.Unlock() 236 | instances, err := c.Health() 237 | if err != nil { 238 | return fmt.Errorf("sync SC ep failed. err:%s", err.Error()) 239 | } 240 | return c.pool.SetAddressByInstances(instances) 241 | } 242 | 243 | func (c *Client) formatURL(api string, querys []URLParameter, options *CallOptions) string { 244 | host := c.GetAddress() 245 | if options != nil && len(options.Address) != 0 { 246 | host = options.Address 247 | } 248 | builder := URLBuilder{ 249 | Protocol: c.protocol, 250 | Host: host, 251 | Path: api, 252 | URLParameters: querys, 253 | CallOptions: options, 254 | } 255 | return builder.String() 256 | } 257 | 258 | // GetDefaultHeaders gets the default headers for each request to be made to Service-Center 259 | func (c *Client) GetDefaultHeaders() http.Header { 260 | headers := http.Header{ 261 | HeaderContentType: []string{"application/json"}, 262 | HeaderUserAgent: []string{"go-client"}, 263 | TenantHeader: []string{"default"}, 264 | } 265 | 266 | return headers 267 | } 268 | 269 | // httpDo makes the http request to Service-center with proper header, body and method 270 | func (c *Client) httpDo(method string, rawURL string, headers http.Header, body []byte) (resp *http.Response, err error) { 271 | if len(headers) == 0 { 272 | headers = make(http.Header) 273 | } 274 | for k, v := range c.GetDefaultHeaders() { 275 | headers[k] = v 276 | } 277 | return c.client.Do(context.Background(), method, rawURL, headers, body) 278 | } 279 | 280 | // RegisterService registers the micro-services to Service-Center 281 | func (c *Client) RegisterService(microService *discovery.MicroService) (string, error) { 282 | if microService == nil { 283 | return "", ErrNil 284 | } 285 | request := discovery.CreateServiceRequest{ 286 | Service: microService, 287 | } 288 | 289 | registerURL := c.formatURL(MSAPIPath+MicroservicePath, nil, nil) 290 | body, err := json.Marshal(request) 291 | if err != nil { 292 | return "", NewJSONException(err, string(body)) 293 | } 294 | 295 | resp, err := c.httpDo("POST", registerURL, nil, body) 296 | if err != nil { 297 | return "", err 298 | } 299 | if resp == nil { 300 | return "", fmt.Errorf("RegisterService failed, response is empty, MicroServiceName: %s", microService.ServiceName) 301 | } 302 | body, err = ioutil.ReadAll(resp.Body) 303 | if err != nil { 304 | return "", NewIOException(err) 305 | } 306 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 307 | var response discovery.GetExistenceResponse 308 | err = json.Unmarshal(body, &response) 309 | if err != nil { 310 | return "", NewJSONException(err, string(body)) 311 | } 312 | microService.ServiceId = response.ServiceId 313 | return response.ServiceId, nil 314 | } 315 | if resp.StatusCode == 400 { 316 | return "", fmt.Errorf("client seems to have erred, error: %s", body) 317 | } 318 | return "", fmt.Errorf("register service failed, ServiceName/responseStatusCode/responsebody: %s/%d/%s", 319 | microService.ServiceName, resp.StatusCode, string(body)) 320 | } 321 | 322 | // GetProviders gets a list of provider for a particular consumer 323 | func (c *Client) GetProviders(consumer string, opts ...CallOption) (*MicroServiceProvideResponse, error) { 324 | copts := &CallOptions{} 325 | for _, opt := range opts { 326 | opt(copts) 327 | } 328 | providersURL := c.formatURL(fmt.Sprintf("%s%s/%s/providers", MSAPIPath, MicroservicePath, consumer), nil, copts) 329 | resp, err := c.httpDo("GET", providersURL, nil, nil) 330 | if err != nil { 331 | return nil, fmt.Errorf("get Providers failed, error: %s, MicroServiceid: %s", err, consumer) 332 | } 333 | if resp == nil { 334 | return nil, fmt.Errorf("get Providers failed, response is empty, MicroServiceid: %s", consumer) 335 | } 336 | var body []byte 337 | body, err = ioutil.ReadAll(resp.Body) 338 | if err != nil { 339 | return nil, fmt.Errorf("Get Providers failed, body is empty, error: %s, MicroServiceid: %s", err, consumer) 340 | } 341 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 342 | p := &MicroServiceProvideResponse{} 343 | err = json.Unmarshal(body, p) 344 | if err != nil { 345 | return nil, err 346 | } 347 | return p, nil 348 | } 349 | return nil, fmt.Errorf("get Providers failed, MicroServiceid: %s, response StatusCode: %d, response body: %s", 350 | consumer, resp.StatusCode, string(body)) 351 | } 352 | 353 | // AddSchemas adds a schema contents to the services registered in service-center 354 | func (c *Client) AddSchemas(microServiceID, schemaName, schemaInfo string) error { 355 | if microServiceID == "" { 356 | return errors.New("invalid micro service ID") 357 | } 358 | 359 | schemaURL := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s", MSAPIPath, MicroservicePath, microServiceID, SchemaPath, schemaName), nil, nil) 360 | h := sha256.New() 361 | _, err := h.Write([]byte(schemaInfo)) 362 | if err != nil { 363 | return err 364 | } 365 | request := &discovery.ModifySchemaRequest{ 366 | ServiceId: microServiceID, 367 | SchemaId: schemaName, 368 | Schema: schemaInfo, 369 | Summary: fmt.Sprintf("%x", h.Sum(nil)), 370 | } 371 | body, err := json.Marshal(request) 372 | if err != nil { 373 | return NewJSONException(err, string(body)) 374 | } 375 | 376 | resp, err := c.httpDo("PUT", schemaURL, nil, body) 377 | if err != nil { 378 | return err 379 | } 380 | 381 | if resp == nil { 382 | return fmt.Errorf("add schemas failed, response is empty") 383 | } 384 | 385 | if resp.StatusCode != http.StatusOK { 386 | return NewCommonException("add micro service schema failed. response StatusCode: %d, response body: %s", 387 | resp.StatusCode, string(httputil.ReadBody(resp))) 388 | } 389 | 390 | return nil 391 | } 392 | 393 | // GetSchema gets Schema list for the microservice from service-center 394 | func (c *Client) GetSchema(microServiceID, schemaName string, opts ...CallOption) ([]byte, error) { 395 | if microServiceID == "" { 396 | return []byte(""), errors.New("invalid micro service ID") 397 | } 398 | copts := &CallOptions{} 399 | for _, opt := range opts { 400 | opt(copts) 401 | } 402 | url := c.formatURL(fmt.Sprintf("%s%s/%s/%s/%s", MSAPIPath, MicroservicePath, microServiceID, "schemas", schemaName), nil, copts) 403 | resp, err := c.httpDo("GET", url, nil, nil) 404 | if err != nil { 405 | return []byte(""), err 406 | } 407 | if resp == nil { 408 | return []byte(""), fmt.Errorf("GetSchema failed, response is empty") 409 | } 410 | var body []byte 411 | body, err = ioutil.ReadAll(resp.Body) 412 | if err != nil { 413 | return []byte(""), NewIOException(err) 414 | } 415 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 416 | return body, nil 417 | } 418 | 419 | return []byte(""), err 420 | } 421 | 422 | // GetMicroServiceID gets the microserviceid by appID, serviceName and version 423 | func (c *Client) GetMicroServiceID(appID, microServiceName, version, env string, opts ...CallOption) (string, error) { 424 | copts := &CallOptions{} 425 | for _, opt := range opts { 426 | opt(copts) 427 | } 428 | url := c.formatURL(MSAPIPath+ExistencePath, []URLParameter{ 429 | {"type": "microservice"}, 430 | {"appId": appID}, 431 | {"serviceName": microServiceName}, 432 | {"version": version}, 433 | {"env": env}, 434 | }, copts) 435 | resp, err := c.httpDo("GET", url, nil, nil) 436 | if err != nil { 437 | return "", err 438 | } 439 | if resp == nil { 440 | return "", fmt.Errorf("GetMicroServiceID failed, response is empty, MicroServiceName: %s", microServiceName) 441 | } 442 | var body []byte 443 | body, err = ioutil.ReadAll(resp.Body) 444 | if err != nil { 445 | return "", NewIOException(err) 446 | } 447 | if resp.StatusCode >= 200 && resp.StatusCode < 500 { 448 | var response discovery.GetExistenceResponse 449 | err = json.Unmarshal(body, &response) 450 | if err != nil { 451 | return "", NewJSONException(err, string(body)) 452 | } 453 | return response.ServiceId, nil 454 | } 455 | return "", fmt.Errorf("GetMicroServiceID failed, MicroService: %s@%s#%s, response StatusCode: %d, response body: %s, URL: %s", 456 | microServiceName, appID, version, resp.StatusCode, string(body), url) 457 | } 458 | 459 | // GetAllMicroServices gets list of all the microservices registered with Service-Center 460 | func (c *Client) GetAllMicroServices(opts ...CallOption) ([]*discovery.MicroService, error) { 461 | copts := &CallOptions{} 462 | for _, opt := range opts { 463 | opt(copts) 464 | } 465 | url := c.formatURL(MSAPIPath+MicroservicePath, nil, copts) 466 | resp, err := c.httpDo("GET", url, nil, nil) 467 | if err != nil { 468 | return nil, err 469 | } 470 | if resp == nil { 471 | return nil, fmt.Errorf("GetAllMicroServices failed, response is empty") 472 | } 473 | var body []byte 474 | body, err = ioutil.ReadAll(resp.Body) 475 | if err != nil { 476 | return nil, NewIOException(err) 477 | } 478 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 479 | var response discovery.GetServicesResponse 480 | err = json.Unmarshal(body, &response) 481 | if err != nil { 482 | return nil, NewJSONException(err, string(body)) 483 | } 484 | return response.Services, nil 485 | } 486 | return nil, fmt.Errorf("GetAllMicroServices failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 487 | } 488 | 489 | // GetAllApplications returns the list of all the applications which is registered in governance-center 490 | func (c *Client) GetAllApplications(opts ...CallOption) ([]string, error) { 491 | copts := &CallOptions{} 492 | for _, opt := range opts { 493 | opt(copts) 494 | } 495 | governanceURL := c.formatURL(GovernAPIPATH+AppsPath, nil, copts) 496 | resp, err := c.httpDo("GET", governanceURL, nil, nil) 497 | if err != nil { 498 | return nil, err 499 | } 500 | if resp == nil { 501 | return nil, fmt.Errorf("GetAllApplications failed, response is empty") 502 | } 503 | var body []byte 504 | body, err = ioutil.ReadAll(resp.Body) 505 | if err != nil { 506 | return nil, NewIOException(err) 507 | } 508 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 509 | var response discovery.GetAppsResponse 510 | err = json.Unmarshal(body, &response) 511 | if err != nil { 512 | return nil, NewJSONException(err, string(body)) 513 | } 514 | return response.AppIds, nil 515 | } 516 | return nil, fmt.Errorf("GetAllApplications failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 517 | } 518 | 519 | // GetMicroService returns the microservices by ID 520 | func (c *Client) GetMicroService(microServiceID string, opts ...CallOption) (*discovery.MicroService, error) { 521 | copts := &CallOptions{} 522 | for _, opt := range opts { 523 | opt(copts) 524 | } 525 | microserviceURL := c.formatURL(fmt.Sprintf("%s%s/%s", MSAPIPath, MicroservicePath, microServiceID), nil, copts) 526 | resp, err := c.httpDo("GET", microserviceURL, nil, nil) 527 | if err != nil { 528 | return nil, err 529 | } 530 | if resp == nil { 531 | return nil, fmt.Errorf("GetMicroService failed, response is empty, MicroServiceId: %s", microServiceID) 532 | } 533 | var body []byte 534 | body, err = ioutil.ReadAll(resp.Body) 535 | if err != nil { 536 | return nil, NewIOException(err) 537 | } 538 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 539 | var response discovery.GetServiceResponse 540 | err = json.Unmarshal(body, &response) 541 | if err != nil { 542 | return nil, NewJSONException(err, string(body)) 543 | } 544 | return response.Service, nil 545 | } 546 | return nil, fmt.Errorf("GetMicroService failed, MicroServiceId: %s, response StatusCode: %d, response body: %s\n, microserviceURL: %s", microServiceID, resp.StatusCode, string(body), microserviceURL) 547 | } 548 | 549 | // BatchFindInstances fetch instances based on service name, env, app and version 550 | // finally it return instances grouped by service name 551 | func (c *Client) BatchFindInstances(consumerID string, keys []*discovery.FindService, opts ...CallOption) (*discovery.BatchFindInstancesResponse, error) { 552 | copts := &CallOptions{} 553 | for _, opt := range opts { 554 | opt(copts) 555 | } 556 | if len(keys) == 0 { 557 | return nil, ErrEmptyCriteria 558 | } 559 | url := c.formatURL(MSAPIPath+BatchInstancePath, []URLParameter{ 560 | {"type": "query"}, 561 | }, copts) 562 | r := &discovery.BatchFindInstancesRequest{ 563 | ConsumerServiceId: consumerID, 564 | Services: keys, 565 | } 566 | rBody, err := json.Marshal(r) 567 | if err != nil { 568 | return nil, NewJSONException(err, string(rBody)) 569 | } 570 | resp, err := c.httpDo("POST", url, http.Header{"X-ConsumerId": []string{consumerID}}, rBody) 571 | if err != nil { 572 | return nil, err 573 | } 574 | if resp == nil { 575 | return nil, fmt.Errorf("BatchFindInstances failed, response is empty") 576 | } 577 | body := httputil.ReadBody(resp) 578 | if resp.StatusCode == http.StatusOK { 579 | var response *discovery.BatchFindInstancesResponse 580 | err = json.Unmarshal(body, &response) 581 | if err != nil { 582 | return nil, NewJSONException(err, string(body)) 583 | } 584 | 585 | return response, nil 586 | } 587 | return nil, fmt.Errorf("batch find failed, status %d, body %s", resp.StatusCode, body) 588 | } 589 | 590 | // FindMicroServiceInstances find microservice instance using consumerID, appID, name and version rule 591 | // 592 | // Deprecated: use FindInstances instead 593 | func (c *Client) FindMicroServiceInstances(consumerID, appID, microServiceName, 594 | versionRule string, opts ...CallOption) ([]*discovery.MicroServiceInstance, error) { 595 | rst, err := c.findInstances(consumerID, appID, microServiceName, versionRule, opts...) 596 | if err != nil { 597 | return nil, err 598 | } 599 | return rst.Instances, nil 600 | } 601 | 602 | // FindInstances find microservice instance 603 | func (c *Client) FindInstances(consumerID, appID, microServiceName string, 604 | opts ...CallOption) (*FindMicroServiceInstancesResult, error) { 605 | return c.findInstances(consumerID, appID, microServiceName, "0%2B", opts...) // 0+, all version 606 | } 607 | 608 | // FindInstances find microservice instance using consumerID, appID, name 609 | func (c *Client) findInstances(consumerID, appID, microServiceName, 610 | versionRule string, opts ...CallOption) (*FindMicroServiceInstancesResult, error) { 611 | copts := &CallOptions{} 612 | for _, opt := range opts { 613 | opt(copts) 614 | } 615 | microserviceInstanceURL := c.formatURL(MSAPIPath+InstancePath, []URLParameter{ 616 | {"appId": appID}, 617 | {"serviceName": microServiceName}, 618 | {"version": versionRule}, 619 | }, copts) 620 | 621 | resp, err := c.httpDo("GET", microserviceInstanceURL, http.Header{"X-ConsumerId": []string{consumerID}}, nil) 622 | if err != nil { 623 | return nil, err 624 | } 625 | if resp == nil { 626 | return nil, fmt.Errorf("FindMicroServiceInstances failed, response is empty, appID/MicroServiceName/version: %s/%s/%s", appID, microServiceName, versionRule) 627 | } 628 | var body []byte 629 | body, err = ioutil.ReadAll(resp.Body) 630 | if err != nil { 631 | return nil, NewIOException(err) 632 | } 633 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 634 | var response discovery.GetInstancesResponse 635 | err = json.Unmarshal(body, &response) 636 | if err != nil { 637 | return nil, NewJSONException(err, string(body)) 638 | } 639 | return &FindMicroServiceInstancesResult{ 640 | Instances: response.Instances, 641 | Revision: resp.Header.Get(HeaderRevision), 642 | }, nil 643 | } 644 | if resp.StatusCode == http.StatusNotModified { 645 | return nil, ErrNotModified 646 | } 647 | if resp.StatusCode == http.StatusBadRequest { 648 | if strings.Contains(string(body), "\"errorCode\":\"400012\"") { 649 | return nil, ErrMicroServiceNotExists 650 | } 651 | } 652 | return nil, fmt.Errorf("FindMicroServiceInstances failed, appID/MicroServiceName/version: %s/%s/%s, response StatusCode: %d, response body: %s", 653 | appID, microServiceName, versionRule, resp.StatusCode, string(body)) 654 | } 655 | 656 | // RegisterMicroServiceInstance registers the microservice instance to Servive-Center 657 | func (c *Client) RegisterMicroServiceInstance(microServiceInstance *discovery.MicroServiceInstance) (string, error) { 658 | if microServiceInstance == nil { 659 | return "", errors.New("invalid request parameter") 660 | } 661 | request := &discovery.RegisterInstanceRequest{ 662 | Instance: microServiceInstance, 663 | } 664 | microserviceInstanceURL := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceInstance.ServiceId, InstancePath), nil, nil) 665 | body, err := json.Marshal(request) 666 | if err != nil { 667 | return "", NewJSONException(err, string(body)) 668 | } 669 | resp, err := c.httpDo("POST", microserviceInstanceURL, nil, body) 670 | if err != nil { 671 | return "", err 672 | } 673 | if resp == nil { 674 | return "", fmt.Errorf("register instance failed, response is empty, MicroServiceId = %s", microServiceInstance.ServiceId) 675 | } 676 | body, err = ioutil.ReadAll(resp.Body) 677 | if err != nil { 678 | return "", NewIOException(err) 679 | } 680 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 681 | var response *discovery.RegisterInstanceResponse 682 | err = json.Unmarshal(body, &response) 683 | if err != nil { 684 | return "", NewJSONException(err, string(body)) 685 | } 686 | return response.InstanceId, nil 687 | } 688 | return "", fmt.Errorf("register instance failed, MicroServiceId: %s, response StatusCode: %d, response body: %s", 689 | microServiceInstance.ServiceId, resp.StatusCode, string(body)) 690 | } 691 | 692 | // GetMicroServiceInstances queries the service-center with provider and consumer ID and returns the microservice-instance 693 | func (c *Client) GetMicroServiceInstances(consumerID, providerID string, opts ...CallOption) ([]*discovery.MicroServiceInstance, error) { 694 | copts := &CallOptions{} 695 | for _, opt := range opts { 696 | opt(copts) 697 | } 698 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, providerID, InstancePath), nil, copts) 699 | resp, err := c.httpDo("GET", url, http.Header{ 700 | "X-ConsumerId": []string{consumerID}, 701 | }, nil) 702 | if err != nil { 703 | return nil, err 704 | } 705 | if resp == nil { 706 | return nil, fmt.Errorf("GetMicroServiceInstances failed, response is empty, ConsumerId/ProviderId = %s%s", consumerID, providerID) 707 | } 708 | var body []byte 709 | body, err = ioutil.ReadAll(resp.Body) 710 | if err != nil { 711 | return nil, NewIOException(err) 712 | } 713 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 714 | var response discovery.GetInstancesResponse 715 | err = json.Unmarshal(body, &response) 716 | if err != nil { 717 | return nil, NewJSONException(err, string(body)) 718 | } 719 | return response.Instances, nil 720 | } 721 | return nil, fmt.Errorf("GetMicroServiceInstances failed, ConsumerId/ProviderId: %s%s, response StatusCode: %d, response body: %s", 722 | consumerID, providerID, resp.StatusCode, string(body)) 723 | } 724 | 725 | // GetAllResources retruns all the list of services, instances, providers, consumers in the service-center 726 | func (c *Client) GetAllResources(resource string, opts ...CallOption) ([]*discovery.ServiceDetail, error) { 727 | copts := &CallOptions{} 728 | for _, opt := range opts { 729 | opt(copts) 730 | } 731 | url := c.formatURL(GovernAPIPATH+MicroservicePath, []URLParameter{ 732 | {"options": resource}, 733 | }, copts) 734 | resp, err := c.httpDo("GET", url, nil, nil) 735 | if err != nil { 736 | return nil, err 737 | } 738 | if resp == nil { 739 | return nil, errors.New("GetAllResources failed, response is empty") 740 | } 741 | var body []byte 742 | body, err = ioutil.ReadAll(resp.Body) 743 | if err != nil { 744 | return nil, NewIOException(err) 745 | } 746 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 747 | var response discovery.GetServicesInfoResponse 748 | err = json.Unmarshal(body, &response) 749 | if err != nil { 750 | return nil, NewJSONException(err, string(body)) 751 | } 752 | return response.AllServicesDetail, nil 753 | } 754 | return nil, fmt.Errorf("GetAllResources failed, response StatusCode: %d, response body: %s", resp.StatusCode, string(body)) 755 | } 756 | 757 | // Health returns the list of all the endpoints of SC with their status 758 | func (c *Client) Health() ([]*discovery.MicroServiceInstance, error) { 759 | url := c.formatURL(MSAPIPath+"/health", nil, nil) 760 | resp, err := c.httpDo("GET", url, nil, nil) 761 | if err != nil { 762 | return nil, err 763 | } 764 | if resp == nil { 765 | return nil, errors.New("query cluster info failed, response is empty") 766 | } 767 | var body []byte 768 | body, err = ioutil.ReadAll(resp.Body) 769 | if err != nil { 770 | return nil, NewIOException(err) 771 | } 772 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 773 | var response discovery.GetInstancesResponse 774 | err = json.Unmarshal(body, &response) 775 | if err != nil { 776 | return nil, NewJSONException(err, string(body)) 777 | } 778 | return response.Instances, nil 779 | } 780 | return nil, fmt.Errorf("query cluster info failed, response StatusCode: %d, response body: %s", 781 | resp.StatusCode, string(body)) 782 | } 783 | 784 | // Heartbeat sends the heartbeat to service-center for particular service-instance 785 | func (c *Client) Heartbeat(microServiceID, microServiceInstanceID string) (bool, error) { 786 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 787 | InstancePath, microServiceInstanceID, HeartbeatPath), nil, nil) 788 | resp, err := c.httpDo("PUT", url, nil, nil) 789 | if err != nil { 790 | return false, err 791 | } 792 | if resp == nil { 793 | return false, fmt.Errorf("heartbeat failed, response is empty, MicroServiceId/MicroServiceInstanceId: %s%s", microServiceID, microServiceInstanceID) 794 | } 795 | if resp.StatusCode != http.StatusOK { 796 | body, err := ioutil.ReadAll(resp.Body) 797 | if err != nil { 798 | return false, NewIOException(err) 799 | } 800 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 801 | } 802 | return true, nil 803 | } 804 | 805 | // WSHeartbeat creates a web socket connection to service-center to send heartbeat. 806 | // It relies on the ping pong mechanism of websocket to ensure the heartbeat, which is maintained by goroutines. 807 | // After the connection is established, the communication fails and will be retried continuously. The retrial time increases exponentially. 808 | // The callback function is used to re-register the instance. 809 | func (c *Client) WSHeartbeat(microServiceID, microServiceInstanceID string, callback func()) error { 810 | err := c.setupWSConnection(microServiceID, microServiceInstanceID) 811 | if err != nil { 812 | return err 813 | } 814 | go func() { 815 | resetConn := func() error { 816 | return c.setupWSConnection(microServiceID, microServiceInstanceID) 817 | } 818 | for { 819 | conn := c.conns[microServiceInstanceID] 820 | _, _, err = conn.ReadMessage() 821 | if err != nil { 822 | openlog.Error(err.Error()) 823 | closeErr := conn.Close() 824 | if closeErr != nil { 825 | openlog.Error(fmt.Sprintf("failed to close websocket connection %s", closeErr.Error())) 826 | } 827 | if websocket.IsCloseError(err, discovery.ErrWebsocketInstanceNotExists) { 828 | // If the instance does not exist, it is closed normally and should be re-registered 829 | callback() 830 | } 831 | // reconnection 832 | err = backoff.RetryNotify( 833 | resetConn, 834 | backoff.NewExponentialBackOff(), 835 | func(err error, duration time.Duration) { 836 | openlog.Error(fmt.Sprintf("failed err: %s,and it will be executed again in %v", err.Error(), duration)) 837 | }) 838 | } 839 | } 840 | }() 841 | return nil 842 | } 843 | 844 | // setupWSConnection create websocket connection and assign it to the map of the connection 845 | func (c *Client) setupWSConnection(microServiceID, microServiceInstanceID string) error { 846 | scheme := "wss" 847 | if !c.opt.EnableSSL { 848 | scheme = "ws" 849 | } 850 | 851 | u := url.URL{ 852 | Scheme: scheme, 853 | Host: c.GetAddress(), 854 | Path: fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 855 | InstancePath, microServiceInstanceID, "/heartbeat"), 856 | } 857 | 858 | conn, _, err := c.dialWebsocket(&u) 859 | if err != nil { 860 | openlog.Error(fmt.Sprintf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error())) 861 | return err 862 | } 863 | c.conns[microServiceInstanceID] = conn 864 | openlog.Info(fmt.Sprintf("%s's websocket connection established successfully", microServiceInstanceID)) 865 | return nil 866 | } 867 | 868 | // UnregisterMicroServiceInstance un-registers the microservice instance from the service-center 869 | func (c *Client) UnregisterMicroServiceInstance(microServiceID, microServiceInstanceID string) (bool, error) { 870 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s", MSAPIPath, MicroservicePath, microServiceID, 871 | InstancePath, microServiceInstanceID), nil, nil) 872 | resp, err := c.httpDo("DELETE", url, nil, nil) 873 | if err != nil { 874 | return false, err 875 | } 876 | if resp == nil { 877 | return false, fmt.Errorf("unregister instance failed, response is empty, MicroServiceId/MicroServiceInstanceId: %s/%s", microServiceID, microServiceInstanceID) 878 | } 879 | if resp.StatusCode != http.StatusOK { 880 | body, err := ioutil.ReadAll(resp.Body) 881 | if err != nil { 882 | return false, NewIOException(err) 883 | } 884 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 885 | } 886 | return true, nil 887 | } 888 | 889 | // UnregisterMicroService un-registers the microservice from the service-center 890 | func (c *Client) UnregisterMicroService(microServiceID string) (bool, error) { 891 | url := c.formatURL(fmt.Sprintf("%s%s/%s", MSAPIPath, MicroservicePath, microServiceID), []URLParameter{ 892 | {"force": "1"}, 893 | }, nil) 894 | resp, err := c.httpDo("DELETE", url, nil, nil) 895 | if err != nil { 896 | return false, err 897 | } 898 | if resp == nil { 899 | return false, fmt.Errorf("UnregisterMicroService failed, response is empty, MicroServiceId: %s", microServiceID) 900 | } 901 | if resp.StatusCode != http.StatusOK { 902 | body, err := ioutil.ReadAll(resp.Body) 903 | if err != nil { 904 | return false, NewIOException(err) 905 | } 906 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 907 | } 908 | return true, nil 909 | } 910 | 911 | // UpdateMicroServiceInstanceStatus updates the microservicve instance status in service-center 912 | func (c *Client) UpdateMicroServiceInstanceStatus(microServiceID, microServiceInstanceID, status string) (bool, error) { 913 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, 914 | InstancePath, microServiceInstanceID, StatusPath), []URLParameter{ 915 | {"value": status}, 916 | }, nil) 917 | resp, err := c.httpDo("PUT", url, nil, nil) 918 | if err != nil { 919 | return false, err 920 | } 921 | if resp == nil { 922 | return false, fmt.Errorf("UpdateMicroServiceInstanceStatus failed, response is empty, MicroServiceId/MicroServiceInstanceId/status: %s%s%s", 923 | microServiceID, microServiceInstanceID, status) 924 | } 925 | if resp.StatusCode != http.StatusOK { 926 | body, err := ioutil.ReadAll(resp.Body) 927 | if err != nil { 928 | return false, NewIOException(err) 929 | } 930 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 931 | } 932 | return true, nil 933 | } 934 | 935 | // UpdateMicroServiceInstanceProperties updates the microserviceinstance prooperties in the service-center 936 | func (c *Client) UpdateMicroServiceInstanceProperties(microServiceID, microServiceInstanceID string, 937 | microServiceInstance *discovery.MicroServiceInstance) (bool, error) { 938 | if microServiceInstance.Properties == nil { 939 | return false, errors.New("invalid request parameter") 940 | } 941 | request := discovery.RegisterInstanceRequest{ 942 | Instance: microServiceInstance, 943 | } 944 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, InstancePath, microServiceInstanceID, PropertiesPath), nil, nil) 945 | body, err := json.Marshal(request.Instance) 946 | if err != nil { 947 | return false, NewJSONException(err, string(body)) 948 | } 949 | 950 | resp, err := c.httpDo("PUT", url, nil, body) 951 | 952 | if err != nil { 953 | return false, err 954 | } 955 | if resp == nil { 956 | return false, fmt.Errorf("UpdateMicroServiceInstanceProperties failed, response is empty, MicroServiceId/microServiceInstanceID: %s/%s", 957 | microServiceID, microServiceInstanceID) 958 | } 959 | if resp.StatusCode != http.StatusOK { 960 | body, err := ioutil.ReadAll(resp.Body) 961 | if err != nil { 962 | return false, NewIOException(err) 963 | } 964 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 965 | } 966 | return true, nil 967 | } 968 | 969 | // UpdateMicroServiceProperties updates the microservice properties in the servive-center 970 | func (c *Client) UpdateMicroServiceProperties(microServiceID string, microService *discovery.MicroService) (bool, error) { 971 | if microService.Properties == nil { 972 | return false, errors.New("invalid request parameter") 973 | } 974 | request := &discovery.CreateServiceRequest{ 975 | Service: microService, 976 | } 977 | url := c.formatURL(fmt.Sprintf("%s%s/%s%s", MSAPIPath, MicroservicePath, microServiceID, PropertiesPath), nil, nil) 978 | body, err := json.Marshal(request.Service) 979 | if err != nil { 980 | return false, NewJSONException(err, string(body)) 981 | } 982 | 983 | resp, err := c.httpDo("PUT", url, nil, body) 984 | 985 | if err != nil { 986 | return false, err 987 | } 988 | if resp == nil { 989 | return false, fmt.Errorf("UpdateMicroServiceProperties failed, response is empty, MicroServiceId: %s", microServiceID) 990 | } 991 | if resp.StatusCode != http.StatusOK { 992 | body, err := ioutil.ReadAll(resp.Body) 993 | if err != nil { 994 | return false, NewIOException(err) 995 | } 996 | return false, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 997 | } 998 | return true, nil 999 | } 1000 | 1001 | // Close closes the connection with Service-Center 1002 | func (c *Client) Close() error { 1003 | c.mutex.Lock() 1004 | defer c.mutex.Unlock() 1005 | for k, v := range c.conns { 1006 | err := v.Close() 1007 | if err != nil { 1008 | return fmt.Errorf("error:%s, microServiceID = %s", err.Error(), k) 1009 | } 1010 | delete(c.conns, k) 1011 | } 1012 | c.pool.Close() 1013 | return nil 1014 | } 1015 | 1016 | func (c *Client) WatchMicroServiceWithExtraHandle(microServiceID string, callback func(e *MicroServiceInstanceChangedEvent), 1017 | extraHandle func(action string, opts ...CallOption)) error { 1018 | openlog.Info(fmt.Sprintf("WatchMicroServiceWithExtraHandle, microServiceID:%s", microServiceID)) 1019 | c.mutex.Lock() 1020 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1021 | openlog.Info(fmt.Sprintf("WatchMicroServiceWithExtraHandle watch, microServiceID:%s", microServiceID)) 1022 | c.watchers[microServiceID] = true 1023 | scheme := "wss" 1024 | if !c.opt.EnableSSL { 1025 | scheme = "ws" 1026 | } 1027 | host := c.GetAddress() 1028 | u := url.URL{ 1029 | Scheme: scheme, 1030 | Host: host, 1031 | Path: fmt.Sprintf("%s%s/%s%s", MSAPIPath, 1032 | MicroservicePath, microServiceID, WatchPath), 1033 | } 1034 | conn, _, err := c.dialWebsocket(&u) 1035 | if err != nil { 1036 | c.watchers[microServiceID] = false 1037 | c.mutex.Unlock() 1038 | return fmt.Errorf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error()) 1039 | } 1040 | 1041 | c.conns[microServiceID] = conn 1042 | // After successfully subscribing to the service, pull the dependency again. 1043 | // This prevents the event from not being notified after one of the dual engines fails and the other has no dependencies. 1044 | extraHandle("watchSucceed", WithAddress(host)) 1045 | go func() { 1046 | for { 1047 | messageType, message, err := conn.ReadMessage() 1048 | if err != nil { 1049 | openlog.Error(fmt.Sprintf("%s:%s", "conn.ReadMessage()", err.Error())) 1050 | break 1051 | } 1052 | if messageType == websocket.TextMessage { 1053 | var response MicroServiceInstanceChangedEvent 1054 | err := json.Unmarshal(message, &response) 1055 | if err != nil { 1056 | if strings.Contains(string(message), "service does not exist") { 1057 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response), message", string(message))) 1058 | c.mutex.Lock() 1059 | delete(c.conns, microServiceID) 1060 | delete(c.watchers, microServiceID) 1061 | c.mutex.Unlock() 1062 | openlog.Info(fmt.Sprintf("delete conn, microServiceID:%s", microServiceID)) 1063 | extraHandle("serviceNotExist") 1064 | return 1065 | } 1066 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response), message", string(message))) 1067 | openlog.Error(fmt.Sprintf("%s:%s", "json.Unmarshal(message, &response)", err.Error())) 1068 | break 1069 | } 1070 | callback(&response) 1071 | } 1072 | } 1073 | err = conn.Close() 1074 | if err != nil { 1075 | openlog.Error(fmt.Sprintf("%s:%s", "conn.Close()", err.Error())) 1076 | } 1077 | c.mutex.Lock() 1078 | delete(c.conns, microServiceID) 1079 | delete(c.watchers, microServiceID) 1080 | c.mutex.Unlock() 1081 | openlog.Info(fmt.Sprintf("conn stop, microServiceID:%s", microServiceID)) 1082 | c.startBackOffWithExtraHandle(microServiceID, callback, extraHandle) 1083 | }() 1084 | } 1085 | c.mutex.Unlock() 1086 | return nil 1087 | } 1088 | 1089 | func (c *Client) startBackOffWithExtraHandle(microServiceID string, callback func(*MicroServiceInstanceChangedEvent), 1090 | extraHandle func(action string, opts ...CallOption)) { 1091 | boff := &backoff.ExponentialBackOff{ 1092 | InitialInterval: 1000 * time.Millisecond, 1093 | RandomizationFactor: backoff.DefaultRandomizationFactor, 1094 | Multiplier: backoff.DefaultMultiplier, 1095 | MaxInterval: 30000 * time.Millisecond, 1096 | MaxElapsedTime: 0, 1097 | Clock: backoff.SystemClock, 1098 | } 1099 | operation := func() error { 1100 | c.mutex.Lock() 1101 | c.watchers[microServiceID] = false 1102 | c.GetAddress() 1103 | c.mutex.Unlock() 1104 | err := c.WatchMicroServiceWithExtraHandle(microServiceID, callback, extraHandle) 1105 | if err != nil { 1106 | openlog.Error(fmt.Sprintf("%s:%s", "startBackOffWithExtraHandle:WatchMicroServiceWithExtraHandle error", err.Error())) 1107 | return err 1108 | } 1109 | return nil 1110 | } 1111 | 1112 | err := backoff.Retry(operation, boff) 1113 | if err == nil { 1114 | return 1115 | } 1116 | openlog.Error(fmt.Sprintf("%s:%s", "backoff.Retry", err.Error())) 1117 | } 1118 | 1119 | // WatchMicroService creates a web socket connection to service-center to keep a watch on the providers for a micro-service 1120 | func (c *Client) WatchMicroService(microServiceID string, callback func(*MicroServiceInstanceChangedEvent)) error { 1121 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1122 | c.mutex.Lock() 1123 | if ready, ok := c.watchers[microServiceID]; !ok || !ready { 1124 | c.watchers[microServiceID] = true 1125 | scheme := "wss" 1126 | if !c.opt.EnableSSL { 1127 | scheme = "ws" 1128 | } 1129 | u := url.URL{ 1130 | Scheme: scheme, 1131 | Host: c.GetAddress(), 1132 | Path: fmt.Sprintf("%s%s/%s%s", MSAPIPath, 1133 | MicroservicePath, microServiceID, WatchPath), 1134 | } 1135 | conn, _, err := c.dialWebsocket(&u) 1136 | if err != nil { 1137 | c.watchers[microServiceID] = false 1138 | c.mutex.Unlock() 1139 | return fmt.Errorf("watching microservice dial catch an exception,microServiceID: %s, error:%s", microServiceID, err.Error()) 1140 | } 1141 | 1142 | c.conns[microServiceID] = conn 1143 | go func() { 1144 | for { 1145 | messageType, message, err := conn.ReadMessage() 1146 | if err != nil { 1147 | break 1148 | } 1149 | if messageType == websocket.TextMessage { 1150 | var response MicroServiceInstanceChangedEvent 1151 | err := json.Unmarshal(message, &response) 1152 | if err != nil { 1153 | break 1154 | } 1155 | callback(&response) 1156 | } 1157 | } 1158 | err = conn.Close() 1159 | if err != nil { 1160 | openlog.Error(err.Error()) 1161 | } 1162 | c.mutex.Lock() 1163 | delete(c.conns, microServiceID) 1164 | c.mutex.Unlock() 1165 | c.startBackOff(microServiceID, callback) 1166 | }() 1167 | } 1168 | c.mutex.Unlock() 1169 | } 1170 | return nil 1171 | } 1172 | 1173 | func (c *Client) GetAddress() string { 1174 | return c.pool.GetAvailableAddress() 1175 | } 1176 | 1177 | func (c *Client) startBackOff(microServiceID string, callback func(*MicroServiceInstanceChangedEvent)) { 1178 | boff := &backoff.ExponentialBackOff{ 1179 | InitialInterval: 1000 * time.Millisecond, 1180 | RandomizationFactor: backoff.DefaultRandomizationFactor, 1181 | Multiplier: backoff.DefaultMultiplier, 1182 | MaxInterval: 30000 * time.Millisecond, 1183 | MaxElapsedTime: 0, 1184 | Clock: backoff.SystemClock, 1185 | } 1186 | operation := func() error { 1187 | c.mutex.Lock() 1188 | c.watchers[microServiceID] = false 1189 | c.GetAddress() 1190 | c.mutex.Unlock() 1191 | err := c.WatchMicroService(microServiceID, callback) 1192 | if err != nil { 1193 | return err 1194 | } 1195 | return nil 1196 | } 1197 | 1198 | err := backoff.Retry(operation, boff) 1199 | if err == nil { 1200 | return 1201 | } 1202 | } 1203 | 1204 | // GetToken generate token according to user-password 1205 | func (c *Client) GetToken(a *rbac.AuthUser) (string, error) { 1206 | return c.GetTokenWithExpiration(a, "") 1207 | } 1208 | 1209 | // GetTokenWithExpiration expiration: 15m~24h, default 12h 1210 | func (c *Client) GetTokenWithExpiration(a *rbac.AuthUser, expiration string) (string, error) { 1211 | request := rbac.Account{ 1212 | Name: a.Username, 1213 | Password: a.Password, 1214 | TokenExpirationTime: expiration, 1215 | } 1216 | body, err := json.Marshal(request) 1217 | if err != nil { 1218 | return "", NewJSONException(err, "parse the username or password failed") 1219 | } 1220 | 1221 | tokenUrl := c.formatURL(TokenPath, nil, nil) 1222 | resp, err := c.httpDo(http.MethodPost, tokenUrl, nil, body) 1223 | if err != nil { 1224 | return "", err 1225 | } 1226 | if resp == nil { 1227 | return "", fmt.Errorf("user %s generate token failed: ", a.Username) 1228 | } 1229 | body, err = ioutil.ReadAll(resp.Body) 1230 | if err != nil { 1231 | return "", NewIOException(err) 1232 | } 1233 | 1234 | if resp.StatusCode == http.StatusOK { 1235 | var response rbac.Token 1236 | err = json.Unmarshal(body, &response) 1237 | if err != nil { 1238 | return "", NewJSONException(err, string(body)) 1239 | } 1240 | return response.TokenStr, nil 1241 | } 1242 | return "", fmt.Errorf("user %s generate token failed, response status code: %d", a.Username, resp.StatusCode) 1243 | } 1244 | 1245 | func (c *Client) CheckPeerStatus() (*PeerStatusResp, error) { 1246 | url := c.formatURL(fmt.Sprintf("%s", PeerHealthPath), nil, nil) 1247 | resp, err := c.httpDo(http.MethodGet, url, nil, nil) 1248 | if err != nil { 1249 | return nil, err 1250 | } 1251 | if resp == nil { 1252 | return nil, fmt.Errorf("check the status of peer engine fail") 1253 | } 1254 | body, err := ioutil.ReadAll(resp.Body) 1255 | if err != nil { 1256 | return nil, NewIOException(err) 1257 | } 1258 | if resp.StatusCode != http.StatusOK { 1259 | return nil, NewCommonException("result: %d %s", resp.StatusCode, string(body)) 1260 | } 1261 | 1262 | var response *PeerStatusResp 1263 | err = json.Unmarshal(body, &response) 1264 | if err != nil { 1265 | return nil, NewJSONException(err, string(body)) 1266 | } 1267 | return response, nil 1268 | } 1269 | -------------------------------------------------------------------------------- /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.20250322063348-41f1988548c4 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.20250322063348-41f1988548c4 h1:aU4lCp/RAk3CtR7CeMhJW2kkqJgRP1x9m/tsrRTQdgQ= 10 | github.com/go-chassis/cari v0.9.1-0.20250322063348-41f1988548c4/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 | Endpoints []string 15 | EnableSSL bool 16 | Timeout time.Duration 17 | TLSConfig *tls.Config 18 | // Other options can be stored in a context 19 | Context context.Context 20 | Compressed bool 21 | Verbose bool 22 | EnableAuth bool 23 | AuthUser *rbac.AuthUser 24 | AuthToken string 25 | TokenExpiration time.Duration 26 | SignRequest func(*http.Request) error 27 | } 28 | 29 | // CallOptions is options when you call a API 30 | type CallOptions struct { 31 | WithoutRevision bool 32 | Revision string 33 | WithGlobal bool 34 | Address string 35 | } 36 | 37 | // WithoutRevision ignore current revision number 38 | func WithoutRevision() CallOption { 39 | return func(o *CallOptions) { 40 | o.WithoutRevision = true 41 | } 42 | } 43 | 44 | // WithRevision query resources with the revision 45 | func WithRevision(revision string) CallOption { 46 | return func(o *CallOptions) { 47 | o.Revision = revision 48 | } 49 | } 50 | 51 | // WithGlobal query resources include other aggregated SC 52 | func WithGlobal() CallOption { 53 | return func(o *CallOptions) { 54 | o.WithGlobal = true 55 | } 56 | } 57 | 58 | // WithAddress query resources with the sc address 59 | func WithAddress(address string) CallOption { 60 | return func(o *CallOptions) { 61 | o.Address = address 62 | } 63 | } 64 | 65 | // CallOption is receiver for options and chang the attribute of it 66 | type CallOption func(*CallOptions) 67 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------