github.com/hamba/avro@v1.8.0/registry/client.go (about) 1 /* 2 Package registry implements a Confluent Schema Registry compliant client. 3 4 See the Confluent Schema Registry docs for an understanding of the 5 API: https://docs.confluent.io/current/schema-registry/docs/api.html 6 7 */ 8 package registry 9 10 import ( 11 "bytes" 12 "io" 13 "net" 14 "net/http" 15 "net/url" 16 "strconv" 17 "strings" 18 "time" 19 20 "github.com/hamba/avro" 21 jsoniter "github.com/json-iterator/go" 22 "github.com/modern-go/concurrent" 23 ) 24 25 const ( 26 contentType = "application/vnd.schemaregistry.v1+json" 27 ) 28 29 // Registry represents a schema registry. 30 type Registry interface { 31 // GetSchema returns the schema with the given id. 32 GetSchema(id int) (avro.Schema, error) 33 34 // GetSubjects gets the registry subjects. 35 GetSubjects() ([]string, error) 36 37 // GetVersions gets the schema versions for a subject. 38 GetVersions(subject string) ([]int, error) 39 40 // GetSchemaByVersion gets the schema by version. 41 GetSchemaByVersion(subject string, version int) (avro.Schema, error) 42 43 // GetLatestSchema gets the latest schema for a subject. 44 GetLatestSchema(subject string) (avro.Schema, error) 45 46 // GetLatestSchemaInfo gets the latest schema and schema metadata for a subject. 47 GetLatestSchemaInfo(subject string) (SchemaInfo, error) 48 49 // CreateSchema creates a schema in the registry, returning the schema id. 50 CreateSchema(subject, schema string, references ...SchemaReference) (int, avro.Schema, error) 51 52 // IsRegistered determines of the schema is registered. 53 IsRegistered(subject, schema string) (int, avro.Schema, error) 54 } 55 56 type schemaPayload struct { 57 Schema string `json:"schema"` 58 References []SchemaReference `json:"references,omitempty"` 59 } 60 61 // SchemaReference represents a schema reference. 62 type SchemaReference struct { 63 Name string `json:"name"` 64 Subject string `json:"subject"` 65 Version int `json:"version"` 66 } 67 68 type idPayload struct { 69 ID int `json:"id"` 70 } 71 72 type credentials struct { 73 username string 74 password string 75 } 76 77 type schemaInfoPayload struct { 78 Schema string `json:"schema"` 79 ID int `json:"id"` 80 Version int `json:"version"` 81 } 82 83 // Parse converts the string schema registry response into a 84 // SchemaInfo object with an avro.Schema schema. 85 func (s *schemaInfoPayload) Parse() (info SchemaInfo, err error) { 86 info = SchemaInfo{ 87 ID: s.ID, 88 Version: s.Version, 89 } 90 info.Schema, err = avro.Parse(s.Schema) 91 return info, err 92 } 93 94 // SchemaInfo represents a schema and metadata information. 95 type SchemaInfo struct { 96 Schema avro.Schema 97 ID int 98 Version int 99 } 100 101 var defaultClient = &http.Client{ 102 Transport: &http.Transport{ 103 Proxy: http.ProxyFromEnvironment, 104 DialContext: (&net.Dialer{ 105 Timeout: 15 * time.Second, 106 KeepAlive: 90 * time.Second, 107 }).DialContext, 108 TLSHandshakeTimeout: 3 * time.Second, 109 }, 110 } 111 112 // ClientFunc is a function used to customize the Client. 113 type ClientFunc func(*Client) 114 115 // WithHTTPClient sets the http client to make requests with. 116 func WithHTTPClient(client *http.Client) ClientFunc { 117 return func(c *Client) { 118 c.client = client 119 } 120 } 121 122 // WithBasicAuth sets the credentials to perform http basic auth. 123 func WithBasicAuth(username, password string) ClientFunc { 124 return func(c *Client) { 125 c.creds = credentials{username: username, password: password} 126 } 127 } 128 129 // Client is an HTTP registry client. 130 type Client struct { 131 client *http.Client 132 base string 133 134 creds credentials 135 136 cache *concurrent.Map // map[int]avro.Schema 137 } 138 139 // NewClient creates a schema registry Client with the given base url. 140 func NewClient(baseURL string, opts ...ClientFunc) (*Client, error) { 141 if _, err := url.Parse(baseURL); err != nil { 142 return nil, err 143 } 144 baseURL = strings.TrimSuffix(baseURL, "/") 145 146 c := &Client{ 147 client: defaultClient, 148 base: baseURL, 149 cache: concurrent.NewMap(), 150 } 151 152 for _, opt := range opts { 153 opt(c) 154 } 155 156 return c, nil 157 } 158 159 // GetSchema returns the schema with the given id. 160 // 161 // GetSchema will cache the schema in memory after it is successfully returned, 162 // allowing it to be used efficiently in a high load situation. 163 func (c *Client) GetSchema(id int) (avro.Schema, error) { 164 if schema, ok := c.cache.Load(id); ok { 165 return schema.(avro.Schema), nil 166 } 167 168 var payload schemaPayload 169 if err := c.request(http.MethodGet, "/schemas/ids/"+strconv.Itoa(id), nil, &payload); err != nil { 170 return nil, err 171 } 172 173 schema, err := avro.Parse(payload.Schema) 174 if err != nil { 175 return nil, err 176 } 177 178 c.cache.Store(id, schema) 179 180 return schema, nil 181 } 182 183 // GetSubjects gets the registry subjects. 184 func (c *Client) GetSubjects() ([]string, error) { 185 var subjects []string 186 err := c.request(http.MethodGet, "/subjects", nil, &subjects) 187 if err != nil { 188 return nil, err 189 } 190 191 return subjects, err 192 } 193 194 // GetVersions gets the schema versions for a subject. 195 func (c *Client) GetVersions(subject string) ([]int, error) { 196 var versions []int 197 err := c.request(http.MethodGet, "/subjects/"+subject+"/versions", nil, &versions) 198 if err != nil { 199 return nil, err 200 } 201 202 return versions, err 203 } 204 205 // GetSchemaByVersion gets the schema by version. 206 func (c *Client) GetSchemaByVersion(subject string, version int) (avro.Schema, error) { 207 var payload schemaPayload 208 err := c.request(http.MethodGet, "/subjects/"+subject+"/versions/"+strconv.Itoa(version), nil, &payload) 209 if err != nil { 210 return nil, err 211 } 212 213 return avro.Parse(payload.Schema) 214 } 215 216 // GetLatestSchema gets the latest schema for a subject. 217 func (c *Client) GetLatestSchema(subject string) (avro.Schema, error) { 218 var payload schemaPayload 219 err := c.request(http.MethodGet, "/subjects/"+subject+"/versions/latest", nil, &payload) 220 if err != nil { 221 return nil, err 222 } 223 224 return avro.Parse(payload.Schema) 225 } 226 227 // GetLatestSchemaInfo gets the latest schema and schema metadata for a subject. 228 func (c *Client) GetLatestSchemaInfo(subject string) (SchemaInfo, error) { 229 var payload schemaInfoPayload 230 err := c.request(http.MethodGet, "/subjects/"+subject+"/versions/latest", nil, &payload) 231 if err != nil { 232 return SchemaInfo{}, err 233 } 234 235 return payload.Parse() 236 } 237 238 // CreateSchema creates a schema in the registry, returning the schema id. 239 func (c *Client) CreateSchema(subject, schema string, references ...SchemaReference) (int, avro.Schema, error) { 240 var payload idPayload 241 inPayload := schemaPayload{Schema: schema, References: references} 242 err := c.request(http.MethodPost, "/subjects/"+subject+"/versions", inPayload, &payload) 243 if err != nil { 244 return 0, nil, err 245 } 246 247 sch, err := avro.Parse(schema) 248 return payload.ID, sch, err 249 } 250 251 // IsRegistered determines of the schema is registered. 252 func (c *Client) IsRegistered(subject, schema string) (int, avro.Schema, error) { 253 var payload idPayload 254 err := c.request(http.MethodPost, "/subjects/"+subject, schemaPayload{Schema: schema}, &payload) 255 if err != nil { 256 return 0, nil, err 257 } 258 259 sch, err := avro.Parse(schema) 260 return payload.ID, sch, err 261 } 262 263 func (c *Client) request(method, uri string, in, out interface{}) error { 264 var body io.Reader 265 if in != nil { 266 b, _ := jsoniter.Marshal(in) 267 body = bytes.NewReader(b) 268 } 269 270 req, _ := http.NewRequest(method, c.base+uri, body) // This error is not possible as we already parsed the url 271 req.Header.Set("Content-Type", contentType) 272 273 if len(c.creds.username) > 0 || len(c.creds.password) > 0 { 274 req.SetBasicAuth(c.creds.username, c.creds.password) 275 } 276 277 resp, err := c.client.Do(req) 278 if err != nil { 279 return err 280 } 281 defer func() { 282 _, _ = io.Copy(io.Discard, resp.Body) 283 _ = resp.Body.Close() 284 }() 285 286 if resp.StatusCode >= 400 { 287 err := Error{StatusCode: resp.StatusCode} 288 _ = jsoniter.NewDecoder(resp.Body).Decode(&err) 289 return err 290 } 291 292 return jsoniter.NewDecoder(resp.Body).Decode(out) 293 } 294 295 // Error is returned by the registry when there is an error. 296 type Error struct { 297 StatusCode int `json:"-"` 298 299 Code int `json:"error_code"` 300 Message string `json:"message"` 301 } 302 303 // Error returns the error message. 304 func (e Error) Error() string { 305 if e.Message != "" { 306 return e.Message 307 } 308 309 return "registry error: " + strconv.Itoa(e.StatusCode) 310 }