├── .gitattributes ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── client.go ├── client_deprecated.go ├── client_test.go └── schema-registry-cli ├── cmd ├── add.go ├── compatible.go ├── exists.go ├── get.go ├── get_config.go ├── helpers.go ├── root.go ├── subjects.go └── versions.go └── main.go /.gitattributes: -------------------------------------------------------------------------------- 1 | *.go linguist-language=Go 2 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | .idea 3 | .vscode 4 | .directory -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | os: 3 | - linux 4 | - osx 5 | go: 6 | - "go1.10" 7 | go_import_path: github.com/landoop/schema-registry 8 | 9 | env: 10 | global: 11 | - GOCACHE=off 12 | install: 13 | - go get ./... 14 | script: 15 | - go test -v -cover ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Schema Registry CLI and client 2 | ============================================== 3 | 4 | This repository contains a Command Line Interface (CLI) and a Go client for the REST API of Confluent's Kafka Schema Registry. 5 | 6 | [![Build Status](https://travis-ci.org/Landoop/schema-registry.svg?branch=master)](https://travis-ci.org/Landoop/schema-registry) 7 | [![GoDoc](https://godoc.org/github.com/Landoop/schema-registry?status.svg)](https://godoc.org/github.com/Landoop/schema-registry) 8 | [![Chat](https://img.shields.io/badge/join-%20chat-00BCD4.svg?style=flat-square)](https://slackpass.io/landoop-community) 9 | 10 | CLI 11 | --- 12 | 13 | To install the CLI, assuming a properly setup Go installation, do: 14 | 15 | `go get -u github.com/landoop/schema-registry/schema-registry-cli` 16 | 17 | After that, the CLI is found in `$GOPATH/bin/schema-registry-cli`. Running `schema-registry-cli` without arguments gives: 18 | 19 | ``` 20 | A command line interface for the Confluent schema registry 21 | 22 | Usage: 23 | schema-registry-cli [command] 24 | 25 | Available Commands: 26 | add registers the schema provided through stdin 27 | compatible tests compatibility between a schema from stdin and a given subject 28 | exists checks if the schema provided through stdin exists for the subject 29 | get retrieves a schema specified by id or subject 30 | get-config retrieves global or suject specific configuration 31 | subjects lists all registered subjects 32 | versions lists all available versions 33 | 34 | Flags: 35 | -h, --help help for schema-registry-cli 36 | -n, --no-color dont color output 37 | -e, --url string schema registry url, overrides SCHEMA_REGISTRY_URL (default "http://localhost:8081") 38 | -v, --verbose be verbose 39 | 40 | Use "schema-registry-cli [command] --help" for more information about a command. 41 | ``` 42 | 43 | The schema registry url can be configured through the `SCHEMA_REGISTRY_URL` environment variable, and overridden through `--url`. When none is provided, `http://localhost:8081` is used as default. 44 | 45 | Client 46 | ------ 47 | 48 | The client package provides a client to deal with the registry from code. It is used by the CLI internally. Usage looks like: 49 | 50 | ```go 51 | import "github.com/landoop/schema-registry" 52 | 53 | client, _ := schemaregistry.NewClient(schemaregistry.DefaultUrl) 54 | client.Subjects() 55 | ``` 56 | 57 | Or, to use with a Schema Registry endpoint listening on HTTPS: 58 | 59 | ```go 60 | import ( 61 | "crypto/tls" 62 | "crypto/x509" 63 | "io/ioutil" 64 | 65 | "github.com/landoop/schema-registry" 66 | ) 67 | 68 | // Create a TLS config to use to connect to Schema Registry. This config will permit TLS connections to an endpoint 69 | // whose TLS cert is signed by the given caFile. 70 | caCert, err := ioutil.ReadFile("/path/to/ca/file") 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | caCertPool := x509.NewCertPool() 76 | caCertPool.AppendCertsFromPEM(caCert) 77 | 78 | tlsConfig := &tls.Config{ 79 | RootCAs: caCertPool, 80 | InsecureSkipVerify: true, 81 | } 82 | 83 | httpsClientTransport := &http.Transport{ 84 | TLSClientConfig: tlsConfig, 85 | } 86 | 87 | httpsClient := &http.Client{ 88 | Transport: httpsClientTransport, 89 | } 90 | 91 | // Create the Schema Registry client 92 | client, _ := schemaregistry.NewClient(baseurl, UsingClient(httpsClient)) 93 | client.Subjects() 94 | ``` 95 | 96 | The documentation of the package can be found here: [![GoDoc](https://godoc.org/github.com/Landoop/schema-registry?status.svg)](https://godoc.org/github.com/Landoop/schema-registry) 97 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | // Package schemaregistry provides a client for Confluent's Kafka Schema Registry REST API. 2 | package schemaregistry 3 | 4 | import ( 5 | "bytes" 6 | "compress/gzip" 7 | "crypto/tls" 8 | "encoding/json" 9 | "fmt" 10 | "io" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "strings" 16 | "time" 17 | ) 18 | 19 | // DefaultURL is the address where a local schema registry listens by default. 20 | const DefaultURL = "http://localhost:8081" 21 | 22 | type ( 23 | httpDoer interface { 24 | Do(req *http.Request) (resp *http.Response, err error) 25 | } 26 | // Client is the registry schema REST API client. 27 | Client struct { 28 | baseURL string 29 | 30 | // the client is created on the `NewClient` function, it can be customized via options. 31 | client httpDoer 32 | } 33 | 34 | // Option describes an optional runtime configurator that can be passed on `NewClient`. 35 | // Custom `Option` can be used as well, it's just a type of `func(*schemaregistry.Client)`. 36 | // 37 | // Look `UsingClient`. 38 | Option func(*Client) 39 | ) 40 | 41 | // UsingClient modifies the underline HTTP Client that schema registry is using for contact with the backend server. 42 | func UsingClient(httpClient *http.Client) Option { 43 | return func(c *Client) { 44 | if httpClient == nil { 45 | return 46 | } 47 | 48 | transport := getTransportLayer(httpClient, 0) 49 | httpClient.Transport = transport 50 | 51 | c.client = httpClient 52 | } 53 | } 54 | 55 | func getTransportLayer(httpClient *http.Client, timeout time.Duration) (t http.RoundTripper) { 56 | if t := httpClient.Transport; t != nil { 57 | return t 58 | } 59 | 60 | httpTransport := &http.Transport{ 61 | TLSNextProto: make(map[string]func(authority string, c *tls.Conn) http.RoundTripper), 62 | } 63 | 64 | if timeout > 0 { 65 | httpTransport.Dial = func(network string, addr string) (net.Conn, error) { 66 | return net.DialTimeout(network, addr, timeout) 67 | } 68 | } 69 | 70 | return httpTransport 71 | } 72 | 73 | // formatBaseURL will try to make sure that the schema:host:port pattern is followed on the `baseURL` field. 74 | func formatBaseURL(baseURL string) string { 75 | if baseURL == "" { 76 | return "" 77 | } 78 | 79 | // remove last slash, so the API can append the path with ease. 80 | if baseURL[len(baseURL)-1] == '/' { 81 | baseURL = baseURL[0 : len(baseURL)-1] 82 | } 83 | 84 | portIdx := strings.LastIndexByte(baseURL, ':') 85 | 86 | schemaIdx := strings.Index(baseURL, "://") 87 | hasSchema := schemaIdx >= 0 88 | hasPort := portIdx > schemaIdx+1 89 | 90 | var port = "80" 91 | if hasPort { 92 | port = baseURL[portIdx+1:] 93 | } 94 | 95 | // find the schema based on the port. 96 | if !hasSchema { 97 | if port == "443" { 98 | baseURL = "https://" + baseURL 99 | } else { 100 | baseURL = "http://" + baseURL 101 | } 102 | } else if !hasPort { 103 | // has schema but not port. 104 | if strings.HasPrefix(baseURL, "https://") { 105 | port = "443" 106 | } 107 | } 108 | 109 | // finally, append the port part if it wasn't there. 110 | if !hasPort { 111 | baseURL += ":" + port 112 | } 113 | 114 | return baseURL 115 | } 116 | 117 | // NewClient creates & returns a new Registry Schema Client 118 | // based on the passed url and the options. 119 | func NewClient(baseURL string, options ...Option) (*Client, error) { 120 | baseURL = formatBaseURL(baseURL) 121 | if _, err := url.Parse(baseURL); err != nil { 122 | return nil, err 123 | } 124 | 125 | c := &Client{baseURL: baseURL} 126 | for _, opt := range options { 127 | opt(c) 128 | } 129 | 130 | if c.client == nil { 131 | httpClient := &http.Client{} 132 | UsingClient(httpClient)(c) 133 | } 134 | 135 | return c, nil 136 | } 137 | 138 | const ( 139 | contentTypeHeaderKey = "Content-Type" 140 | contentTypeJSON = "application/json" 141 | 142 | acceptHeaderKey = "Accept" 143 | acceptEncodingHeaderKey = "Accept-Encoding" 144 | contentEncodingHeaderKey = "Content-Encoding" 145 | gzipEncodingHeaderValue = "gzip" 146 | ) 147 | 148 | // ResourceError is being fired from all API calls when an error code is received. 149 | type ResourceError struct { 150 | ErrorCode int `json:"error_code"` 151 | Method string `json:"method,omitempty"` 152 | URI string `json:"uri,omitempty"` 153 | Message string `json:"message,omitempty"` 154 | } 155 | 156 | func (err ResourceError) Error() string { 157 | return fmt.Sprintf("client: (%s: %s) failed with error code %d%s", 158 | err.Method, err.URI, err.ErrorCode, err.Message) 159 | } 160 | 161 | func newResourceError(errCode int, uri, method, body string) ResourceError { 162 | unescapedURI, _ := url.QueryUnescape(uri) 163 | 164 | return ResourceError{ 165 | ErrorCode: errCode, 166 | URI: unescapedURI, 167 | Method: method, 168 | Message: body, 169 | } 170 | } 171 | 172 | // These numbers are used by the schema registry to communicate errors. 173 | const ( 174 | subjectNotFoundCode = 40401 175 | schemaNotFoundCode = 40403 176 | ) 177 | 178 | // IsSubjectNotFound checks the returned error to see if it is kind of a subject not found error code. 179 | func IsSubjectNotFound(err error) bool { 180 | if err == nil { 181 | return false 182 | } 183 | 184 | if resErr, ok := err.(ResourceError); ok { 185 | return resErr.ErrorCode == subjectNotFoundCode 186 | } 187 | 188 | return false 189 | } 190 | 191 | // IsSchemaNotFound checks the returned error to see if it is kind of a schema not found error code. 192 | func IsSchemaNotFound(err error) bool { 193 | if err == nil { 194 | return false 195 | } 196 | 197 | if resErr, ok := err.(ResourceError); ok { 198 | return resErr.ErrorCode == schemaNotFoundCode 199 | } 200 | 201 | return false 202 | } 203 | 204 | // isOK is called inside the `Client#do` and it closes the body reader if no accessible. 205 | func isOK(resp *http.Response) bool { 206 | return !(resp.StatusCode < 200 || resp.StatusCode >= 300) 207 | } 208 | 209 | var noOpBuffer = new(bytes.Buffer) 210 | 211 | func acquireBuffer(b []byte) *bytes.Buffer { 212 | if len(b) > 0 { 213 | return bytes.NewBuffer(b) 214 | } 215 | 216 | return noOpBuffer 217 | } 218 | 219 | const schemaAPIVersion = "v1" 220 | const contentTypeSchemaJSON = "application/vnd.schemaregistry." + schemaAPIVersion + "+json" 221 | 222 | func (c *Client) do(method, path, contentType string, send []byte) (*http.Response, error) { 223 | if path[0] == '/' { 224 | path = path[1:] 225 | } 226 | 227 | uri := c.baseURL + "/" + path 228 | 229 | req, err := http.NewRequest(method, uri, acquireBuffer(send)) 230 | if err != nil { 231 | return nil, err 232 | } 233 | 234 | // set the content type if any. 235 | if contentType != "" { 236 | req.Header.Set(contentTypeHeaderKey, contentType) 237 | } 238 | 239 | // response accept gziped content. 240 | req.Header.Add(acceptEncodingHeaderKey, gzipEncodingHeaderValue) 241 | req.Header.Add(acceptHeaderKey, contentTypeSchemaJSON+", application/vnd.schemaregistry+json, application/json") 242 | 243 | // send the request and check the response for any connection & authorization errors here. 244 | resp, err := c.client.Do(req) 245 | if err != nil { 246 | return nil, err 247 | } 248 | 249 | if !isOK(resp) { 250 | defer resp.Body.Close() 251 | var errBody string 252 | respContentType := resp.Header.Get(contentTypeHeaderKey) 253 | 254 | if strings.Contains(respContentType, "text/html") { 255 | // if the body is html, then don't read it, it doesn't contain the raw info we need. 256 | } else if strings.Contains(respContentType, "json") { 257 | // if it's json try to read it as confluent's specific error json. 258 | var resErr ResourceError 259 | c.readJSON(resp, &resErr) 260 | return nil, resErr 261 | } else { 262 | // else give the whole body to the error context. 263 | b, err := c.readResponseBody(resp) 264 | if err != nil { 265 | errBody = " unable to read body: " + err.Error() 266 | } else { 267 | errBody = "\n" + string(b) 268 | } 269 | } 270 | 271 | return nil, newResourceError(resp.StatusCode, uri, method, errBody) 272 | } 273 | 274 | return resp, nil 275 | } 276 | 277 | type gzipReadCloser struct { 278 | respReader io.ReadCloser 279 | gzipReader io.ReadCloser 280 | } 281 | 282 | func (rc *gzipReadCloser) Close() error { 283 | if rc.gzipReader != nil { 284 | defer rc.gzipReader.Close() 285 | } 286 | 287 | return rc.respReader.Close() 288 | } 289 | 290 | func (rc *gzipReadCloser) Read(p []byte) (n int, err error) { 291 | if rc.gzipReader != nil { 292 | return rc.gzipReader.Read(p) 293 | } 294 | 295 | return rc.respReader.Read(p) 296 | } 297 | 298 | func (c *Client) acquireResponseBodyStream(resp *http.Response) (io.ReadCloser, error) { 299 | // check for gzip and read it, the right way. 300 | var ( 301 | reader = resp.Body 302 | err error 303 | ) 304 | 305 | if encoding := resp.Header.Get(contentEncodingHeaderKey); encoding == gzipEncodingHeaderValue { 306 | reader, err = gzip.NewReader(resp.Body) 307 | if err != nil { 308 | return nil, fmt.Errorf("client: failed to read gzip compressed content, trace: %v", err) 309 | } 310 | // we wrap the gzipReader and the underline response reader 311 | // so a call of .Close() can close both of them with the correct order when finish reading, the caller decides. 312 | // Must close manually using a defer on the callers before the `readResponseBody` call, 313 | // note that the `readJSON` can decide correctly by itself. 314 | return &gzipReadCloser{ 315 | respReader: resp.Body, 316 | gzipReader: reader, 317 | }, nil 318 | } 319 | 320 | // return the stream reader. 321 | return reader, err 322 | } 323 | 324 | func (c *Client) readResponseBody(resp *http.Response) ([]byte, error) { 325 | reader, err := c.acquireResponseBodyStream(resp) 326 | if err != nil { 327 | return nil, err 328 | } 329 | 330 | body, err := ioutil.ReadAll(reader) 331 | if err = reader.Close(); err != nil { 332 | return nil, err 333 | } 334 | 335 | // return the body. 336 | return body, err 337 | } 338 | 339 | func (c *Client) readJSON(resp *http.Response, valuePtr interface{}) error { 340 | b, err := c.readResponseBody(resp) 341 | if err != nil { 342 | return err 343 | } 344 | 345 | return json.Unmarshal(b, valuePtr) 346 | } 347 | 348 | var errRequired = func(field string) error { 349 | return fmt.Errorf("client: %s is required", field) 350 | } 351 | 352 | const ( 353 | subjectsPath = "subjects" 354 | subjectPath = subjectsPath + "/%s" 355 | schemaPath = "schemas/ids/%d" 356 | ) 357 | 358 | // Subjects returns a list of the available subjects(schemas). 359 | // https://docs.confluent.io/current/schema-registry/docs/api.html#subjects 360 | func (c *Client) Subjects() (subjects []string, err error) { 361 | // # List all available subjects 362 | // GET /subjects 363 | resp, respErr := c.do(http.MethodGet, subjectsPath, "", nil) 364 | if respErr != nil { 365 | err = respErr 366 | return 367 | } 368 | 369 | err = c.readJSON(resp, &subjects) 370 | return 371 | } 372 | 373 | // Versions returns all schema version numbers registered for this subject. 374 | func (c *Client) Versions(subject string) (versions []int, err error) { 375 | if subject == "" { 376 | err = errRequired("subject") 377 | return 378 | } 379 | 380 | // # List all versions of a particular subject 381 | // GET /subjects/(string: subject)/versions 382 | path := fmt.Sprintf(subjectPath, subject+"/versions") 383 | resp, respErr := c.do(http.MethodGet, path, "", nil) 384 | if respErr != nil { 385 | err = respErr 386 | return 387 | } 388 | 389 | err = c.readJSON(resp, &versions) 390 | return 391 | } 392 | 393 | // DeleteSubject deletes the specified subject and its associated compatibility level if registered. 394 | // It is recommended to use this API only when a topic needs to be recycled or in development environment. 395 | // Returns the versions of the schema deleted under this subject. 396 | func (c *Client) DeleteSubject(subject string) (versions []int, err error) { 397 | if subject == "" { 398 | err = errRequired("subject") 399 | return 400 | } 401 | 402 | // DELETE /subjects/(string: subject) 403 | path := fmt.Sprintf(subjectPath, subject) 404 | resp, respErr := c.do(http.MethodDelete, path, "", nil) 405 | if respErr != nil { 406 | err = respErr 407 | return 408 | } 409 | 410 | err = c.readJSON(resp, &versions) 411 | return 412 | } 413 | 414 | // IsRegistered tells if the given "schema" is registered for this "subject". 415 | func (c *Client) IsRegistered(subject, schema string) (bool, Schema, error) { 416 | var fs Schema 417 | 418 | sc := schemaOnlyJSON{schema} 419 | send, err := json.Marshal(sc) 420 | if err != nil { 421 | return false, fs, err 422 | } 423 | 424 | path := fmt.Sprintf(subjectPath, subject) 425 | resp, err := c.do(http.MethodPost, path, "", send) 426 | if err != nil { 427 | // schema not found? 428 | if IsSchemaNotFound(err) { 429 | return false, fs, nil 430 | } 431 | // error? 432 | return false, fs, err 433 | } 434 | 435 | if err = c.readJSON(resp, &fs); err != nil { 436 | return true, fs, err // found but error when unmarshal. 437 | } 438 | 439 | // so we have a schema. 440 | return true, fs, nil 441 | } 442 | 443 | type ( 444 | schemaOnlyJSON struct { 445 | Schema string `json:"schema"` 446 | } 447 | 448 | idOnlyJSON struct { 449 | ID int `json:"id"` 450 | } 451 | 452 | isCompatibleJSON struct { 453 | IsCompatible bool `json:"is_compatible"` 454 | } 455 | 456 | // Schema describes a schema, look `GetSchema` for more. 457 | Schema struct { 458 | // Schema is the Avro schema string. 459 | Schema string `json:"schema"` 460 | // Subject where the schema is registered for. 461 | Subject string `json:"subject"` 462 | // Version of the returned schema. 463 | Version int `json:"version"` 464 | ID int `json:"id,omitempty"` 465 | } 466 | 467 | // Config describes a subject or globa schema-registry configuration 468 | Config struct { 469 | // CompatibilityLevel mode of subject or global 470 | CompatibilityLevel string `json:"compatibilityLevel"` 471 | } 472 | ) 473 | 474 | // RegisterNewSchema registers a schema. 475 | // The returned identifier should be used to retrieve 476 | // this schema from the schemas resource and is different from 477 | // the schema’s version which is associated with that name. 478 | func (c *Client) RegisterNewSchema(subject string, avroSchema string) (int, error) { 479 | if subject == "" { 480 | return 0, errRequired("subject") 481 | } 482 | if avroSchema == "" { 483 | return 0, errRequired("avroSchema") 484 | } 485 | 486 | schema := schemaOnlyJSON{ 487 | Schema: avroSchema, 488 | } 489 | 490 | send, err := json.Marshal(schema) 491 | if err != nil { 492 | return 0, err 493 | } 494 | 495 | // # Register a new schema under a particular subject 496 | // POST /subjects/(string: subject)/versions 497 | 498 | path := fmt.Sprintf(subjectPath+"/versions", subject) 499 | resp, err := c.do(http.MethodPost, path, contentTypeSchemaJSON, send) 500 | if err != nil { 501 | return 0, err 502 | } 503 | 504 | var res idOnlyJSON 505 | err = c.readJSON(resp, &res) 506 | return res.ID, err 507 | } 508 | 509 | // JSONAvroSchema converts and returns the json form of the "avroSchema" as []byte. 510 | func JSONAvroSchema(avroSchema string) (json.RawMessage, error) { 511 | var raw json.RawMessage 512 | err := json.Unmarshal(json.RawMessage(avroSchema), &raw) 513 | if err != nil { 514 | return nil, err 515 | } 516 | return raw, err 517 | } 518 | 519 | // GetSchemaByID returns the Auro schema string identified by the id. 520 | // id (int) – the globally unique identifier of the schema. 521 | func (c *Client) GetSchemaByID(subjectID int) (string, error) { 522 | // # Get the schema for a particular subject id 523 | // GET /schemas/ids/{int: id} 524 | path := fmt.Sprintf(schemaPath, subjectID) 525 | resp, err := c.do(http.MethodGet, path, "", nil) 526 | if err != nil { 527 | return "", err 528 | } 529 | 530 | var res schemaOnlyJSON 531 | if err = c.readJSON(resp, &res); err != nil { 532 | return "", err 533 | } 534 | 535 | return res.Schema, nil 536 | } 537 | 538 | // SchemaLatestVersion is the only one valid string for the "versionID", it's the "latest" version string and it's used on `GetLatestSchema`. 539 | const SchemaLatestVersion = "latest" 540 | 541 | func checkSchemaVersionID(versionID interface{}) error { 542 | if versionID == nil { 543 | return errRequired("versionID (string \"latest\" or int)") 544 | } 545 | 546 | if verStr, ok := versionID.(string); ok { 547 | if verStr != SchemaLatestVersion { 548 | return fmt.Errorf("client: %v string is not a valid value for the versionID input parameter [versionID == \"latest\"]", versionID) 549 | } 550 | } 551 | 552 | if verInt, ok := versionID.(int); ok { 553 | if verInt <= 0 || verInt > 2^31-1 { // it's the max of int32, math.MaxInt32 already but do that check. 554 | return fmt.Errorf("client: %v integer is not a valid value for the versionID input parameter [ versionID > 0 && versionID <= 2^31-1]", versionID) 555 | } 556 | } 557 | 558 | return nil 559 | } 560 | 561 | // subject (string) – Name of the subject 562 | // version (versionId [string "latest" or 1,2^31-1]) – Version of the schema to be returned. 563 | // Valid values for versionId are between [1,2^31-1] or the string “latest”. 564 | // The string “latest” refers to the last registered schema under the specified subject. 565 | // Note that there may be a new latest schema that gets registered right after this request is served. 566 | // 567 | // It's not safe to use just an interface to the high-level API, therefore we split this method 568 | // to two, one which will retrieve the latest versioned schema and the other which will accept 569 | // the version as integer and it will retrieve by a specific version. 570 | // 571 | // See `GetLatestSchema` and `GetSchemaAtVersion` instead. 572 | func (c *Client) getSubjectSchemaAtVersion(subject string, versionID interface{}) (s Schema, err error) { 573 | if subject == "" { 574 | err = errRequired("subject") 575 | return 576 | } 577 | 578 | if err = checkSchemaVersionID(versionID); err != nil { 579 | return 580 | } 581 | 582 | // # Get the schema at a particular version 583 | // GET /subjects/(string: subject)/versions/(versionId: "latest" | int) 584 | path := fmt.Sprintf(subjectPath+"/versions/%v", subject, versionID) 585 | resp, respErr := c.do(http.MethodGet, path, "", nil) 586 | if respErr != nil { 587 | err = respErr 588 | return 589 | } 590 | 591 | err = c.readJSON(resp, &s) 592 | return 593 | } 594 | 595 | // GetSchemaBySubject returns the schema for a particular subject and version. 596 | func (c *Client) GetSchemaBySubject(subject string, versionID int) (Schema, error) { 597 | return c.getSubjectSchemaAtVersion(subject, versionID) 598 | } 599 | 600 | // GetLatestSchema returns the latest version of a schema. 601 | // See `GetSchemaAtVersion` to retrieve a subject schema by a specific version. 602 | func (c *Client) GetLatestSchema(subject string) (Schema, error) { 603 | return c.getSubjectSchemaAtVersion(subject, SchemaLatestVersion) 604 | } 605 | 606 | // getConfigSubject returns the Config of global or for a given subject. It handles 404 error in a 607 | // different way, since not-found for a subject configuration means it's using global. 608 | func (c *Client) getConfigSubject(subject string) (Config, error) { 609 | var err error 610 | var config = Config{} 611 | 612 | path := fmt.Sprintf("/config/%s", subject) 613 | resp, respErr := c.do(http.MethodGet, path, "", nil) 614 | if respErr != nil && respErr.(ResourceError).ErrorCode != 404 { 615 | return config, respErr 616 | } 617 | if resp != nil { 618 | err = c.readJSON(resp, &config) 619 | } 620 | 621 | return config, err 622 | } 623 | 624 | // GetConfig returns the configuration (Config type) for global Schema-Registry or a specific 625 | // subject. When Config returned has "compatibilityLevel" empty, it's using global settings. 626 | func (c *Client) GetConfig(subject string) (Config, error) { 627 | return c.getConfigSubject(subject) 628 | } 629 | 630 | // subject (string) – Name of the subject 631 | // version (versionId [string "latest" or 1,2^31-1]) – Version of the schema to be returned. 632 | // Valid values for versionId are between [1,2^31-1] or the string “latest”. 633 | // The string “latest” refers to the last registered schema under the specified subject. 634 | // Note that there may be a new latest schema that gets registered right after this request is served. 635 | // 636 | // It's not safe to use just an interface to the high-level API, therefore we split this method 637 | // to two, one which will retrieve the latest versioned schema and the other which will accept 638 | // the version as integer and it will retrieve by a specific version. 639 | // 640 | // See `IsSchemaCompatible` and `IsLatestSchemaCompatible` instead. 641 | func (c *Client) isSchemaCompatibleAtVersion(subject string, avroSchema string, versionID interface{}) (combatible bool, err error) { 642 | if subject == "" { 643 | err = errRequired("subject") 644 | return 645 | } 646 | if avroSchema == "" { 647 | err = errRequired("avroSchema") 648 | return 649 | } 650 | 651 | if err = checkSchemaVersionID(versionID); err != nil { 652 | return 653 | } 654 | 655 | schema := schemaOnlyJSON{ 656 | Schema: avroSchema, 657 | } 658 | 659 | send, err := json.Marshal(schema) 660 | if err != nil { 661 | return 662 | } 663 | 664 | // # Test input schema against a particular version of a subject’s schema for compatibility 665 | // POST /compatibility/subjects/(string: subject)/versions/(versionId: "latest" | int) 666 | path := fmt.Sprintf("compatibility/"+subjectPath+"/versions/%v", subject, versionID) 667 | resp, err := c.do(http.MethodPost, path, contentTypeSchemaJSON, send) 668 | if err != nil { 669 | return 670 | } 671 | 672 | var res isCompatibleJSON 673 | err = c.readJSON(resp, &res) 674 | return res.IsCompatible, err 675 | } 676 | 677 | // IsSchemaCompatible tests compatibility with a specific version of a subject's schema. 678 | func (c *Client) IsSchemaCompatible(subject string, avroSchema string, versionID int) (bool, error) { 679 | return c.isSchemaCompatibleAtVersion(subject, avroSchema, versionID) 680 | } 681 | 682 | // IsLatestSchemaCompatible tests compatibility with the latest version of a subject's schema. 683 | func (c *Client) IsLatestSchemaCompatible(subject string, avroSchema string) (bool, error) { 684 | return c.isSchemaCompatibleAtVersion(subject, avroSchema, SchemaLatestVersion) 685 | } 686 | -------------------------------------------------------------------------------- /client_deprecated.go: -------------------------------------------------------------------------------- 1 | package schemaregistry 2 | 3 | import ( 4 | "crypto/tls" 5 | "errors" 6 | "net/http" 7 | "net/url" 8 | ) 9 | 10 | // DefaultUrl is the address where a local schema registry listens by default. 11 | // 12 | // DEPRECATED: Use `schemaregistry.DefaultURL` instead. 13 | const DefaultUrl = DefaultURL 14 | 15 | // GetSchemaById returns the schema for some id. 16 | // The schema registry only provides the schema itself, not the id, subject or version. 17 | // 18 | // DEPRECATED: Use `Client#GetSchemaByID` instead. 19 | func (c *Client) GetSchemaById(id int) (string, error) { 20 | return c.GetSchemaByID(id) 21 | } 22 | 23 | // NewTlsClient returns a new Client that securely connects to baseurl. 24 | // 25 | // DEPRECATED: Use `schemaregistry.NewClient(baseURL, schemaregistry.UsingClient(customHTTPSClient))` instead. 26 | func NewTlsClient(baseurl string, tlsConfig *tls.Config) (*Client, error) { 27 | u, err := url.Parse(baseurl) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | if u.Scheme != "https" { 33 | return nil, errors.New("func NewTlsClient: This method only accepts HTTPS URLs") 34 | } 35 | 36 | // TODO: Consider using golang.org/x/net/http2 to enable HTTP/2 with HTTPS connections 37 | httpsClientTransport := &http.Transport{ 38 | TLSClientConfig: tlsConfig, 39 | } 40 | 41 | httpsClient := &http.Client{ 42 | Transport: httpsClientTransport, 43 | } 44 | 45 | return NewClient(baseurl, UsingClient(httpsClient)) 46 | } 47 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package schemaregistry 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "reflect" 9 | "strings" 10 | "testing" 11 | ) 12 | 13 | const testHost = "testhost:1337" 14 | const testURL = "http://" + testHost 15 | 16 | type D func(req *http.Request) (*http.Response, error) 17 | 18 | func (d D) Do(req *http.Request) (*http.Response, error) { 19 | return d(req) 20 | } 21 | 22 | // verifies the http.Request, creates an http.Response 23 | func dummyHTTPHandler(t *testing.T, method, path string, status int, reqBody, respBody interface{}) D { 24 | d := D(func(req *http.Request) (*http.Response, error) { 25 | if method != "" && req.Method != method { 26 | t.Errorf("method is wrong, expected `%s`, got `%s`", method, req.Method) 27 | } 28 | if req.URL.Host != testHost { 29 | t.Errorf("expected host `%s`, got `%s`", testHost, req.URL.Host) 30 | } 31 | if path != "" && req.URL.Path != path { 32 | t.Errorf("path is wrong, expected `%s`, got `%s`", path, req.URL.Path) 33 | } 34 | if reqBody != nil { 35 | expbs, err := json.Marshal(reqBody) 36 | if err != nil { 37 | t.Error(err) 38 | } 39 | bs, err := ioutil.ReadAll(req.Body) 40 | mustEqual(t, strings.Trim(string(bs), "\r\n"), strings.Trim(string(expbs), "\r\n")) 41 | } 42 | var resp http.Response 43 | resp.Header = http.Header{contentTypeHeaderKey: []string{contentTypeJSON}} 44 | resp.StatusCode = status 45 | if respBody != nil { 46 | bs, err := json.Marshal(respBody) 47 | if err != nil { 48 | t.Error(err) 49 | } 50 | resp.Body = ioutil.NopCloser(bytes.NewReader(bs)) 51 | } 52 | return &resp, nil 53 | }) 54 | return d 55 | } 56 | 57 | func httpSuccess(t *testing.T, method, path string, reqBody, respBody interface{}) *Client { 58 | return &Client{testURL, dummyHTTPHandler(t, method, path, 200, reqBody, respBody)} 59 | } 60 | 61 | func httpError(t *testing.T, status, errCode int, errMsg string) *Client { 62 | return &Client{testURL, dummyHTTPHandler(t, "", "", status, nil, ResourceError{ErrorCode: errCode, Message: errMsg})} 63 | } 64 | 65 | func mustEqual(t *testing.T, actual, expected interface{}) { 66 | if !reflect.DeepEqual(actual, expected) { 67 | t.Errorf("expected `%#v`, got `%#v`", expected, actual) 68 | } 69 | } 70 | 71 | func TestSubjects(t *testing.T) { 72 | subsIn := []string{"rollulus", "hello-subject"} 73 | c := httpSuccess(t, "GET", "/subjects", nil, subsIn) 74 | subs, err := c.Subjects() 75 | if err != nil { 76 | t.Error() 77 | } 78 | mustEqual(t, subs, subsIn) 79 | } 80 | 81 | func TestVersions(t *testing.T) { 82 | versIn := []int{1, 2, 3} 83 | c := httpSuccess(t, "GET", "/subjects/mysubject/versions", nil, versIn) 84 | vers, err := c.Versions("mysubject") 85 | if err != nil { 86 | t.Error() 87 | } 88 | mustEqual(t, vers, versIn) 89 | } 90 | 91 | func TestIsRegistered_yes(t *testing.T) { 92 | s := `{"x":"y"}` 93 | ss := schemaOnlyJSON{s} 94 | sIn := Schema{s, "mysubject", 4, 7} 95 | c := httpSuccess(t, "POST", "/subjects/mysubject", ss, sIn) 96 | isreg, sOut, err := c.IsRegistered("mysubject", s) 97 | if err != nil { 98 | t.Error() 99 | } 100 | if !isreg { 101 | t.Error() 102 | } 103 | mustEqual(t, sOut, sIn) 104 | } 105 | 106 | func TestIsRegistered_not(t *testing.T) { 107 | c := httpError(t, 404, schemaNotFoundCode, "too bad") 108 | isreg, _, err := c.IsRegistered("mysubject", "{}") 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | if isreg { 113 | t.Fatalf("is registered: %v", err) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/add.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var addCmd = &cobra.Command{ 11 | Use: "add ", 12 | Short: "registers the schema provided through stdin", 13 | Long: ``, 14 | SilenceUsage: true, 15 | RunE: func(cmd *cobra.Command, args []string) error { 16 | if len(args) != 1 { 17 | return fmt.Errorf("expected 1 argument") 18 | } 19 | id, err := assertClient().RegisterNewSchema(args[0], stdinToString()) 20 | if err != nil { 21 | return err 22 | } 23 | log.Printf("registered schema with id %d\n", id) 24 | return nil 25 | }, 26 | } 27 | 28 | func init() { 29 | RootCmd.AddCommand(addCmd) 30 | } 31 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/compatible.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // compatible can handle two argument styles: or 11 | var compatibleCmd = &cobra.Command{ 12 | Use: "compatible [version]", 13 | Short: "tests compatibility between a schema from stdin and a given subject", 14 | Long: `The compatibility level of the subject is used for this check. 15 | If it has never been changed, the global compatibility level applies. 16 | If no schema version is specified, the latest version is tested. 17 | `, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | if len(args) < 1 || len(args) > 2 { 20 | return fmt.Errorf("expected 1 to 2 arguments") 21 | } 22 | var iscompat bool 23 | var err error 24 | switch len(args) { 25 | case 1: 26 | iscompat, err = assertClient().IsLatestSchemaCompatible(args[0], stdinToString()) 27 | case 2: 28 | ver, err := strconv.Atoi(args[1]) 29 | if err != nil { 30 | return fmt.Errorf("2nd argument must be a version number") 31 | } 32 | iscompat, err = assertClient().IsSchemaCompatible(args[0], stdinToString(), ver) 33 | } 34 | if err != nil { 35 | return err 36 | } 37 | if iscompat { 38 | fmt.Println("the provided schema is compatible") 39 | } else { 40 | err = fmt.Errorf("the provided schema is not compatible") 41 | } 42 | return err 43 | }, 44 | } 45 | 46 | func init() { 47 | RootCmd.AddCommand(compatibleCmd) 48 | } 49 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/exists.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var existsCmd = &cobra.Command{ 10 | Use: "exists ", 11 | Short: "checks if the schema provided through stdin exists for the subject", 12 | Long: ``, 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | if len(args) != 1 { 15 | return fmt.Errorf("expected 1 argument") 16 | } 17 | isreg, sch, err := assertClient().IsRegistered(args[0], stdinToString()) 18 | if err != nil { 19 | return err 20 | } 21 | fmt.Printf("exists: %v\n", isreg) 22 | if isreg { 23 | fmt.Printf("id: %d\n", sch.ID) 24 | fmt.Printf("version: %d\n", sch.Version) 25 | } 26 | return nil 27 | }, 28 | } 29 | 30 | func init() { 31 | RootCmd.AddCommand(existsCmd) 32 | } 33 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/get.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | // get can handle three argument styles: , or 11 | var getCmd = &cobra.Command{ 12 | Use: "get | ( [])", 13 | Short: "retrieves a schema specified by id or subject", 14 | Long: `The schema can be requested by id or subject. 15 | When a subject is given, optionally one can provide a specific version. If no 16 | version is specified, the latest version is returned. 17 | `, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | if len(args) < 1 || len(args) > 2 { 20 | return fmt.Errorf("expected 1 to 2 arguments") 21 | } 22 | id, idParseErr := strconv.Atoi(args[0]) 23 | var err error 24 | switch { 25 | case len(args) == 1 && idParseErr == nil: 26 | err = getByID(id) 27 | case len(args) == 1 && idParseErr != nil: 28 | err = getLatestBySubject(args[0]) 29 | case len(args) == 2: 30 | ver, err := strconv.Atoi(args[1]) 31 | if err != nil { 32 | return fmt.Errorf("2nd argument must be a version number") 33 | } 34 | err = getBySubjectVersion(args[0], ver) 35 | default: 36 | return fmt.Errorf("?") 37 | } 38 | return err 39 | }, 40 | } 41 | 42 | func init() { 43 | RootCmd.AddCommand(getCmd) 44 | } 45 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/get_config.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var getConfigCmd = &cobra.Command{ 10 | Use: "get-config [subject]", 11 | Short: "retrieves global or suject specific configuration", 12 | Long: `Configuration can be requested for all or a specific subject. When "compatibility-level" 13 | is not defined for a specific subject, then it's using global compatibility level. To check global 14 | setting just call "get-config" without arguments. 15 | Compatibility levels in Schema-Registry may be: "NONE", "BACKWARD", "FORWARD" and "FULL". Please 16 | consider official documentation for more details. 17 | `, 18 | RunE: func(cmd *cobra.Command, args []string) error { 19 | switch { 20 | case len(args) > 1: 21 | return fmt.Errorf("only one subject allowed") 22 | case len(args) == 0: 23 | if err := getConfig(""); err != nil { 24 | return err 25 | } 26 | case len(args) == 1: 27 | if err := getConfig(args[0]); err != nil { 28 | return err 29 | } 30 | } 31 | 32 | return nil 33 | }, 34 | } 35 | 36 | func init() { 37 | RootCmd.AddCommand(getConfigCmd) 38 | } 39 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/helpers.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | 10 | "github.com/hokaccha/go-prettyjson" 11 | 12 | schemaregistry "github.com/landoop/schema-registry" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | func stdinToString() string { 17 | bs, err := ioutil.ReadAll(bufio.NewReader(os.Stdin)) 18 | if err != nil { 19 | panic(err) 20 | } 21 | return string(bs) 22 | } 23 | 24 | func printSchema(sch schemaregistry.Schema) { 25 | log.Printf("version: %d\n", sch.Version) 26 | log.Printf("id: %d\n", sch.ID) 27 | 28 | pretty, err := prettyjson.Format([]byte(sch.Schema)) 29 | if err != nil { 30 | fmt.Println(sch.Schema) //isn't a json object, which is legal 31 | return 32 | } 33 | os.Stdout.Write(pretty) 34 | os.Stdout.WriteString("\n") 35 | } 36 | 37 | func getByID(id int) error { 38 | cl := assertClient() 39 | sch, err := cl.GetSchemaByID(id) 40 | if err != nil { 41 | return err 42 | } 43 | fmt.Println(sch) 44 | return nil 45 | } 46 | 47 | func getLatestBySubject(subj string) error { 48 | cl := assertClient() 49 | sch, err := cl.GetLatestSchema(subj) 50 | if err != nil { 51 | return err 52 | } 53 | printSchema(sch) 54 | return nil 55 | } 56 | 57 | func getBySubjectVersion(subj string, ver int) error { 58 | cl := assertClient() 59 | sch, err := cl.GetSchemaBySubject(subj, ver) 60 | if err != nil { 61 | return err 62 | } 63 | printSchema(sch) 64 | return nil 65 | } 66 | 67 | func printConfig(cfg schemaregistry.Config, subj string) { 68 | if subj == "" { 69 | subj = "global" 70 | } 71 | if cfg.CompatibilityLevel == "" { 72 | cfg.CompatibilityLevel = "not defined, using global" 73 | } 74 | fmt.Printf("%s compatibility-level: %s\n", subj, cfg.CompatibilityLevel) 75 | } 76 | 77 | func getConfig(subj string) error { 78 | cl := assertClient() 79 | cfg, err := cl.GetConfig(subj) 80 | if err != nil { 81 | return err 82 | } 83 | printConfig(cfg, subj) 84 | return nil 85 | } 86 | 87 | func assertClient() *schemaregistry.Client { 88 | c, err := schemaregistry.NewClient(viper.GetString("url")) 89 | if err != nil { 90 | fmt.Println(err) 91 | os.Exit(-1) 92 | } 93 | return c 94 | } 95 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/root.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | 9 | "github.com/fatih/color" 10 | schemaregistry "github.com/landoop/schema-registry" 11 | 12 | "github.com/spf13/cobra" 13 | "github.com/spf13/viper" 14 | ) 15 | 16 | var ( 17 | cfgFile string 18 | registryURL string 19 | verbose bool 20 | nocolor bool 21 | ) 22 | 23 | // RootCmd represents the base command when called without any subcommands 24 | var RootCmd = &cobra.Command{ 25 | Use: "schema-registry-cli", 26 | Short: "A command line interface for the Confluent schema registry", 27 | Long: `A command line interface for the Confluent schema registry`, 28 | PersistentPreRun: func(cmd *cobra.Command, args []string) { 29 | if !verbose { 30 | log.SetOutput(ioutil.Discard) 31 | } 32 | if nocolor { 33 | color.NoColor = true 34 | } 35 | log.Printf("schema registry url: %s\n", viper.Get("url")) 36 | }, 37 | } 38 | 39 | // Execute adds all child commands to the root command sets flags appropriately. 40 | // This is called by main.main(). It only needs to happen once to the rootCmd. 41 | func Execute() { 42 | if err := RootCmd.Execute(); err != nil { 43 | fmt.Println(err) 44 | os.Exit(-1) 45 | } 46 | } 47 | 48 | func init() { 49 | RootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "be verbose") 50 | RootCmd.PersistentFlags().BoolVarP(&nocolor, "no-color", "n", false, "dont color output") 51 | RootCmd.PersistentFlags().StringVarP(®istryURL, "url", "e", schemaregistry.DefaultURL, "schema registry url, overrides SCHEMA_REGISTRY_URL") 52 | viper.SetEnvPrefix("schema_registry") 53 | viper.BindPFlag("url", RootCmd.PersistentFlags().Lookup("url")) 54 | viper.BindEnv("url") 55 | } 56 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/subjects.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var subjectsCmd = &cobra.Command{ 11 | Use: "subjects", 12 | Short: "lists all registered subjects", 13 | Long: ``, 14 | RunE: func(cmd *cobra.Command, args []string) error { 15 | subs, err := assertClient().Subjects() 16 | if err != nil { 17 | return err 18 | } 19 | log.Printf("there are %d subjects\n", len(subs)) 20 | for _, s := range subs { 21 | fmt.Println(s) 22 | } 23 | return nil 24 | }, 25 | } 26 | 27 | func init() { 28 | RootCmd.AddCommand(subjectsCmd) 29 | } 30 | -------------------------------------------------------------------------------- /schema-registry-cli/cmd/versions.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | var versionsCmd = &cobra.Command{ 10 | Use: "versions", 11 | Short: "lists all available versions", 12 | Long: ``, 13 | RunE: func(cmd *cobra.Command, args []string) error { 14 | if len(args) != 1 { 15 | return fmt.Errorf("expected 1 argument") 16 | } 17 | client := assertClient() 18 | vers, err := client.Versions(args[0]) 19 | if err != nil { 20 | return err 21 | } 22 | fmt.Printf("%v\n", vers) 23 | return nil 24 | }, 25 | } 26 | 27 | func init() { 28 | RootCmd.AddCommand(versionsCmd) 29 | } 30 | -------------------------------------------------------------------------------- /schema-registry-cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/landoop/schema-registry/schema-registry-cli/cmd" 4 | 5 | func main() { 6 | cmd.Execute() 7 | } 8 | --------------------------------------------------------------------------------