github.com/confluentinc/confluent-kafka-go@v1.9.2/schemaregistry/schemaregistry_client.go (about) 1 /** 2 * Copyright 2022 Confluent Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * 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 17 package schemaregistry 18 19 import ( 20 "encoding/json" 21 "fmt" 22 "net/url" 23 "strings" 24 "sync" 25 26 "github.com/confluentinc/confluent-kafka-go/schemaregistry/cache" 27 ) 28 29 /* Schema Registry API endpoints 30 * 31 * ====Schemas==== 32 * Fetch string: schema(escaped) identified by the input id. 33 * -GET /schemas/ids/{int: id} returns: JSON blob: schema; raises: 404[03], 500[01] 34 * 35 * ====Subjects==== 36 * Fetch JSON array str:subject of all registered subjects 37 * -GET /subjects returns: JSON array string: subjects; raises: 500[01] 38 * Fetch JSON array int:versions 39 * GET /subjects/{string: subject}/versions returns: JSON array of int: versions; raises: 404[01], 500[01] 40 * 41 * GET /subjects/{string: subject}/versions/{int|string('latest'): version} returns: JSON blob *schemaMetadata*; raises: 404[01, 02], 422[02], 500[01] 42 * GET /subjects/{string: subject}/versions/{int|string('latest'): version}/schema returns : JSON blob: schema(unescaped); raises: 404, 422, 500[01, 02, 03] 43 * 44 * Delete subject and it's associated subject configuration subjectConfig 45 * -DELETE /subjects/{string: subject}) returns: JSON array int: version; raises: 404[01], 500[01] 46 * Delete subject version 47 * -DELETE /subjects/{string: subject}/versions/{int|str('latest'): version} returns int: deleted version id; raises: 404[01, 02] 48 * 49 * Register new schema under subject 50 * -POST /subjects/{string: subject}/versions returns JSON blob ; raises: 409, 422[01], 500[01, 02, 03] 51 * Return SchemaMetadata for the subject version (if any) associated with the schema in the request body 52 * -POST /subjects/{string: subject} returns JSON *schemaMetadata*; raises: 404[01, 03] 53 * 54 * ====Compatibility==== 55 * Test schema (http body) against configured comparability for subject version 56 * -POST /compatibility/subjects/{string: subject}/versions/{int:string('latest'): version} returns: JSON bool:is_compatible; raises: 404[01,02], 422[01,02], 500[01] 57 * 58 * ====SerializerConfig==== 59 * Returns global configuration 60 * -GET /config returns: JSON string:comparability; raises: 500[01] 61 * Update global SR config 62 * -PUT /config returns: JSON string:compatibility; raises: 422[03], 500[01, 03] 63 * Update subject level subjectConfig 64 * -PUT /config/{string: subject} returns: JSON string:compatibility; raises: 422[03], 500[01,03] 65 * Returns compatibility level of subject 66 * GET /config/(string: subject) returns: JSON string:compatibility; raises: 404, 500[01] 67 */ 68 69 // Reference represents a schema reference 70 type Reference struct { 71 Name string `json:"name"` 72 Subject string `json:"subject"` 73 Version int `json:"version"` 74 } 75 76 // SchemaInfo represents basic schema information 77 type SchemaInfo struct { 78 Schema string `json:"schema,omitempty"` 79 SchemaType string `json:"schemaType,omitempty"` 80 References []Reference `json:"references,omitempty"` 81 } 82 83 // MarshalJSON implements the json.Marshaler interface 84 func (sd *SchemaInfo) MarshalJSON() ([]byte, error) { 85 return json.Marshal(&struct { 86 Schema string `json:"schema,omitempty"` 87 SchemaType string `json:"schemaType,omitempty"` 88 References []Reference `json:"references,omitempty"` 89 }{ 90 sd.Schema, 91 sd.SchemaType, 92 sd.References, 93 }) 94 } 95 96 // UnmarshalJSON implements the json.Unmarshaller interface 97 func (sd *SchemaInfo) UnmarshalJSON(b []byte) error { 98 var err error 99 var tmp struct { 100 Schema string `json:"schema,omitempty"` 101 SchemaType string `json:"schemaType,omitempty"` 102 References []Reference `json:"references,omitempty"` 103 } 104 105 err = json.Unmarshal(b, &tmp) 106 107 sd.Schema = tmp.Schema 108 sd.SchemaType = tmp.SchemaType 109 sd.References = tmp.References 110 111 return err 112 } 113 114 // SchemaMetadata represents schema metadata 115 type SchemaMetadata struct { 116 SchemaInfo 117 ID int `json:"id,omitempty"` 118 Subject string `json:"subject,omitempty"` 119 Version int `json:"version,omitempty"` 120 } 121 122 // MarshalJSON implements the json.Marshaler interface 123 func (sd *SchemaMetadata) MarshalJSON() ([]byte, error) { 124 return json.Marshal(&struct { 125 Schema string `json:"schema,omitempty"` 126 SchemaType string `json:"schemaType,omitempty"` 127 References []Reference `json:"references,omitempty"` 128 ID int `json:"id,omitempty"` 129 Subject string `json:"subject,omitempty"` 130 Version int `json:"version,omitempty"` 131 }{ 132 sd.Schema, 133 sd.SchemaType, 134 sd.References, 135 sd.ID, 136 sd.Subject, 137 sd.Version, 138 }) 139 } 140 141 // UnmarshalJSON implements the json.Unmarshaller interface 142 func (sd *SchemaMetadata) UnmarshalJSON(b []byte) error { 143 var err error 144 var tmp struct { 145 Schema string `json:"schema,omitempty"` 146 SchemaType string `json:"schemaType,omitempty"` 147 References []Reference `json:"references,omitempty"` 148 ID int `json:"id,omitempty"` 149 Subject string `json:"subject,omitempty"` 150 Version int `json:"version,omitempty"` 151 } 152 153 err = json.Unmarshal(b, &tmp) 154 155 sd.Schema = tmp.Schema 156 sd.SchemaType = tmp.SchemaType 157 sd.References = tmp.References 158 sd.ID = tmp.ID 159 sd.Subject = tmp.Subject 160 sd.Version = tmp.Version 161 162 return err 163 } 164 165 type subjectJSON struct { 166 subject string 167 json string 168 } 169 170 type subjectID struct { 171 subject string 172 id int 173 } 174 175 /* HTTP(S) Schema Registry Client and schema caches */ 176 type client struct { 177 sync.Mutex 178 restService *restService 179 schemaCache cache.Cache 180 schemaCacheLock sync.RWMutex 181 idCache cache.Cache 182 idCacheLock sync.RWMutex 183 versionCache cache.Cache 184 versionCacheLock sync.RWMutex 185 } 186 187 var _ Client = new(client) 188 189 // Client is an interface for clients interacting with the Confluent Schema Registry. 190 // The Schema Registry's REST interface is further explained in Confluent's Schema Registry API documentation 191 // https://github.com/confluentinc/schema-registry/blob/master/client/src/main/java/io/confluent/kafka/schemaregistry/client/SchemaRegistryClient.java 192 type Client interface { 193 Register(subject string, schema SchemaInfo, normalize bool) (id int, err error) 194 GetBySubjectAndID(subject string, id int) (schema SchemaInfo, err error) 195 GetID(subject string, schema SchemaInfo, normalize bool) (id int, err error) 196 GetLatestSchemaMetadata(subject string) (SchemaMetadata, error) 197 GetSchemaMetadata(subject string, version int) (SchemaMetadata, error) 198 GetAllVersions(subject string) ([]int, error) 199 GetVersion(subject string, schema SchemaInfo, normalize bool) (version int, err error) 200 GetAllSubjects() ([]string, error) 201 DeleteSubject(subject string, permanent bool) ([]int, error) 202 DeleteSubjectVersion(subject string, version int, permanent bool) (deletes int, err error) 203 GetCompatibility(subject string) (compatibility Compatibility, err error) 204 UpdateCompatibility(subject string, update Compatibility) (compatibility Compatibility, err error) 205 TestCompatibility(subject string, version int, schema SchemaInfo) (compatible bool, err error) 206 GetDefaultCompatibility() (compatibility Compatibility, err error) 207 UpdateDefaultCompatibility(update Compatibility) (compatibility Compatibility, err error) 208 } 209 210 // NewClient returns a Client implementation 211 func NewClient(conf *Config) (Client, error) { 212 213 urlConf := conf.SchemaRegistryURL 214 if strings.HasPrefix(urlConf, "mock://") { 215 url, err := url.Parse(urlConf) 216 if err != nil { 217 return nil, err 218 } 219 mock := &mockclient{ 220 url: url, 221 schemaCache: make(map[subjectJSON]idCacheEntry), 222 idCache: make(map[subjectID]*SchemaInfo), 223 versionCache: make(map[subjectJSON]versionCacheEntry), 224 compatibilityCache: make(map[string]Compatibility), 225 } 226 return mock, nil 227 } 228 229 restService, err := newRestService(conf) 230 if err != nil { 231 return nil, err 232 } 233 234 var schemaCache cache.Cache 235 var idCache cache.Cache 236 var versionCache cache.Cache 237 if conf.CacheCapacity != 0 { 238 schemaCache, err = cache.NewLRUCache(conf.CacheCapacity) 239 if err != nil { 240 return nil, err 241 } 242 idCache, err = cache.NewLRUCache(conf.CacheCapacity) 243 if err != nil { 244 return nil, err 245 } 246 versionCache, err = cache.NewLRUCache(conf.CacheCapacity) 247 if err != nil { 248 return nil, err 249 } 250 } else { 251 schemaCache = cache.NewMapCache() 252 idCache = cache.NewMapCache() 253 versionCache = cache.NewMapCache() 254 } 255 handle := &client{ 256 restService: restService, 257 schemaCache: schemaCache, 258 idCache: idCache, 259 versionCache: versionCache, 260 } 261 return handle, nil 262 } 263 264 // Register registers Schema aliased with subject 265 func (c *client) Register(subject string, schema SchemaInfo, normalize bool) (id int, err error) { 266 schemaJSON, err := schema.MarshalJSON() 267 if err != nil { 268 return -1, err 269 } 270 cacheKey := subjectJSON{ 271 subject: subject, 272 json: string(schemaJSON), 273 } 274 c.schemaCacheLock.RLock() 275 idValue, ok := c.schemaCache.Get(cacheKey) 276 c.schemaCacheLock.RUnlock() 277 if ok { 278 return idValue.(int), nil 279 } 280 281 metadata := SchemaMetadata{ 282 SchemaInfo: schema, 283 } 284 c.schemaCacheLock.Lock() 285 // another goroutine could have already put it in cache 286 idValue, ok = c.schemaCache.Get(cacheKey) 287 if !ok { 288 err = c.restService.handleRequest(newRequest("POST", versionNormalize, &metadata, url.PathEscape(subject), normalize), &metadata) 289 if err == nil { 290 c.schemaCache.Put(cacheKey, metadata.ID) 291 } else { 292 metadata.ID = -1 293 } 294 } else { 295 metadata.ID = idValue.(int) 296 } 297 c.schemaCacheLock.Unlock() 298 return metadata.ID, err 299 } 300 301 // GetBySubjectAndID returns the schema identified by id 302 // Returns Schema object on success 303 func (c *client) GetBySubjectAndID(subject string, id int) (schema SchemaInfo, err error) { 304 cacheKey := subjectID{ 305 subject: subject, 306 id: id, 307 } 308 c.idCacheLock.RLock() 309 infoValue, ok := c.idCache.Get(cacheKey) 310 c.idCacheLock.RUnlock() 311 if ok { 312 return *infoValue.(*SchemaInfo), nil 313 } 314 315 metadata := SchemaMetadata{} 316 newInfo := &SchemaInfo{} 317 c.idCacheLock.Lock() 318 // another goroutine could have already put it in cache 319 infoValue, ok = c.idCache.Get(cacheKey) 320 if !ok { 321 if len(subject) > 0 { 322 err = c.restService.handleRequest(newRequest("GET", schemasBySubject, nil, id, url.QueryEscape(subject)), &metadata) 323 } else { 324 err = c.restService.handleRequest(newRequest("GET", schemas, nil, id), &metadata) 325 } 326 if err == nil { 327 newInfo = &SchemaInfo{ 328 Schema: metadata.Schema, 329 SchemaType: metadata.SchemaType, 330 References: metadata.References, 331 } 332 c.idCache.Put(cacheKey, newInfo) 333 } 334 } else { 335 newInfo = infoValue.(*SchemaInfo) 336 } 337 c.idCacheLock.Unlock() 338 return *newInfo, err 339 } 340 341 // GetID checks if a schema has been registered with the subject. Returns ID if the registration can be found 342 func (c *client) GetID(subject string, schema SchemaInfo, normalize bool) (id int, err error) { 343 schemaJSON, err := schema.MarshalJSON() 344 if err != nil { 345 return -1, err 346 } 347 cacheKey := subjectJSON{ 348 subject: subject, 349 json: string(schemaJSON), 350 } 351 c.schemaCacheLock.RLock() 352 idValue, ok := c.schemaCache.Get(cacheKey) 353 c.schemaCacheLock.RUnlock() 354 if ok { 355 return idValue.(int), nil 356 } 357 358 metadata := SchemaMetadata{ 359 SchemaInfo: schema, 360 } 361 c.schemaCacheLock.Lock() 362 // another goroutine could have already put it in cache 363 idValue, ok = c.schemaCache.Get(cacheKey) 364 if !ok { 365 err = c.restService.handleRequest(newRequest("POST", subjectsNormalize, &metadata, url.PathEscape(subject), normalize), &metadata) 366 if err == nil { 367 c.schemaCache.Put(cacheKey, metadata.ID) 368 } else { 369 metadata.ID = -1 370 } 371 } else { 372 metadata.ID = idValue.(int) 373 } 374 c.schemaCacheLock.Unlock() 375 return metadata.ID, err 376 } 377 378 // GetLatestSchemaMetadata fetches latest version registered with the provided subject 379 // Returns SchemaMetadata object 380 func (c *client) GetLatestSchemaMetadata(subject string) (result SchemaMetadata, err error) { 381 err = c.restService.handleRequest(newRequest("GET", versions, nil, url.PathEscape(subject), "latest"), &result) 382 383 return result, err 384 } 385 386 // GetSchemaMetadata fetches the requested subject schema identified by version 387 // Returns SchemaMetadata object 388 func (c *client) GetSchemaMetadata(subject string, version int) (result SchemaMetadata, err error) { 389 err = c.restService.handleRequest(newRequest("GET", versions, nil, url.PathEscape(subject), version), &result) 390 391 return result, err 392 } 393 394 // GetAllVersions fetches a list of all version numbers associated with the provided subject registration 395 // Returns integer slice on success 396 func (c *client) GetAllVersions(subject string) (results []int, err error) { 397 var result []int 398 err = c.restService.handleRequest(newRequest("GET", version, nil, url.PathEscape(subject)), &result) 399 400 return result, err 401 } 402 403 // GetVersion finds the Subject SchemaMetadata associated with the provided schema 404 // Returns integer SchemaMetadata number 405 func (c *client) GetVersion(subject string, schema SchemaInfo, normalize bool) (version int, err error) { 406 schemaJSON, err := schema.MarshalJSON() 407 if err != nil { 408 return -1, err 409 } 410 cacheKey := subjectJSON{ 411 subject: subject, 412 json: string(schemaJSON), 413 } 414 c.versionCacheLock.RLock() 415 versionValue, ok := c.versionCache.Get(cacheKey) 416 c.versionCacheLock.RUnlock() 417 if ok { 418 return versionValue.(int), nil 419 } 420 421 metadata := SchemaMetadata{ 422 SchemaInfo: schema, 423 } 424 c.versionCacheLock.Lock() 425 // another goroutine could have already put it in cache 426 versionValue, ok = c.versionCache.Get(cacheKey) 427 if !ok { 428 err = c.restService.handleRequest(newRequest("POST", subjectsNormalize, &metadata, url.PathEscape(subject), normalize), &metadata) 429 if err == nil { 430 c.versionCache.Put(cacheKey, metadata.Version) 431 } else { 432 metadata.Version = -1 433 } 434 } else { 435 metadata.Version = versionValue.(int) 436 } 437 c.versionCacheLock.Unlock() 438 return metadata.Version, err 439 } 440 441 // Fetch all Subjects registered with the schema Registry 442 // Returns a string slice containing all registered subjects 443 func (c *client) GetAllSubjects() ([]string, error) { 444 var result []string 445 err := c.restService.handleRequest(newRequest("GET", subject, nil), &result) 446 447 return result, err 448 } 449 450 // Deletes provided Subject from registry 451 // Returns integer slice of versions removed by delete 452 func (c *client) DeleteSubject(subject string, permanent bool) (deleted []int, err error) { 453 c.schemaCacheLock.Lock() 454 for keyValue := range c.schemaCache.ToMap() { 455 key := keyValue.(subjectJSON) 456 if key.subject == subject { 457 c.schemaCache.Delete(key) 458 } 459 } 460 c.schemaCacheLock.Unlock() 461 c.versionCacheLock.Lock() 462 for keyValue := range c.versionCache.ToMap() { 463 key := keyValue.(subjectJSON) 464 if key.subject == subject { 465 c.versionCache.Delete(key) 466 } 467 } 468 c.versionCacheLock.Unlock() 469 c.idCacheLock.Lock() 470 for keyValue := range c.idCache.ToMap() { 471 key := keyValue.(subjectID) 472 if key.subject == subject { 473 c.idCache.Delete(key) 474 } 475 } 476 c.idCacheLock.Unlock() 477 var result []int 478 err = c.restService.handleRequest(newRequest("DELETE", subjectsDelete, nil, url.PathEscape(subject), permanent), &result) 479 return result, err 480 } 481 482 // DeleteSubjectVersion removes the version identified by delete from the subject's registration 483 // Returns integer id for the deleted version 484 func (c *client) DeleteSubjectVersion(subject string, version int, permanent bool) (deleted int, err error) { 485 c.versionCacheLock.Lock() 486 for keyValue, value := range c.versionCache.ToMap() { 487 key := keyValue.(subjectJSON) 488 if key.subject == subject && value == version { 489 c.versionCache.Delete(key) 490 schemaJSON := key.json 491 cacheKeySchema := subjectJSON{ 492 subject: subject, 493 json: string(schemaJSON), 494 } 495 c.schemaCacheLock.Lock() 496 idValue, ok := c.schemaCache.Get(cacheKeySchema) 497 if ok { 498 c.schemaCache.Delete(cacheKeySchema) 499 } 500 c.schemaCacheLock.Unlock() 501 if ok { 502 id := idValue.(int) 503 c.idCacheLock.Lock() 504 cacheKeyID := subjectID{ 505 subject: subject, 506 id: id, 507 } 508 c.idCache.Delete(cacheKeyID) 509 c.idCacheLock.Unlock() 510 } 511 } 512 } 513 c.versionCacheLock.Unlock() 514 var result int 515 err = c.restService.handleRequest(newRequest("DELETE", versionsDelete, nil, url.PathEscape(subject), version, permanent), &result) 516 return result, err 517 518 } 519 520 // Compatibility options 521 type Compatibility int 522 523 const ( 524 _ = iota 525 // None is no compatibility 526 None 527 // Backward compatibility 528 Backward 529 // Forward compatibility 530 Forward 531 // Full compatibility 532 Full 533 // BackwardTransitive compatibility 534 BackwardTransitive 535 // ForwardTransitive compatibility 536 ForwardTransitive 537 // FullTransitive compatibility 538 FullTransitive 539 ) 540 541 var compatibilityEnum = []string{ 542 "", 543 "NONE", 544 "BACKWARD", 545 "FORWARD", 546 "FULL", 547 "BACKWARD_TRANSITIVE", 548 "FORWARD_TRANSITIVE", 549 "FULL_TRANSITIVE", 550 } 551 552 /* NOTE: GET uses compatibilityLevel, POST uses compatibility */ 553 type compatibilityLevel struct { 554 CompatibilityUpdate Compatibility `json:"compatibility,omitempty"` 555 Compatibility Compatibility `json:"compatibilityLevel,omitempty"` 556 } 557 558 // MarshalJSON implements json.Marshaler 559 func (c Compatibility) MarshalJSON() ([]byte, error) { 560 return json.Marshal(c.String()) 561 } 562 563 // UnmarshalJSON implements json.Unmarshaler 564 func (c *Compatibility) UnmarshalJSON(b []byte) error { 565 val := string(b[1 : len(b)-1]) 566 return c.ParseString(val) 567 } 568 569 type compatibilityValue struct { 570 Compatible bool `json:"is_compatible,omitempty"` 571 } 572 573 func (c Compatibility) String() string { 574 return compatibilityEnum[c] 575 } 576 577 // ParseString returns a Compatibility for the given string 578 func (c *Compatibility) ParseString(val string) error { 579 for idx, elm := range compatibilityEnum { 580 if elm == val { 581 *c = Compatibility(idx) 582 return nil 583 } 584 } 585 586 return fmt.Errorf("failed to unmarshal Compatibility") 587 } 588 589 // Fetch compatibility level currently configured for provided subject 590 // Returns compatibility level string upon success 591 func (c *client) GetCompatibility(subject string) (compatibility Compatibility, err error) { 592 var result compatibilityLevel 593 err = c.restService.handleRequest(newRequest("GET", subjectConfig, nil, url.PathEscape(subject)), &result) 594 595 return result.Compatibility, err 596 } 597 598 // UpdateCompatibility updates subject's compatibility level 599 // Returns new compatibility level string upon success 600 func (c *client) UpdateCompatibility(subject string, update Compatibility) (compatibility Compatibility, err error) { 601 result := compatibilityLevel{ 602 CompatibilityUpdate: update, 603 } 604 err = c.restService.handleRequest(newRequest("PUT", subjectConfig, &result, url.PathEscape(subject)), &result) 605 606 return result.CompatibilityUpdate, err 607 } 608 609 // TestCompatibility verifies schema against the subject's compatibility policy 610 // Returns true if the schema is compatible, false otherwise 611 func (c *client) TestCompatibility(subject string, version int, schema SchemaInfo) (ok bool, err error) { 612 var result compatibilityValue 613 candidate := SchemaMetadata{ 614 SchemaInfo: schema, 615 } 616 617 err = c.restService.handleRequest(newRequest("POST", compatibility, &candidate, url.PathEscape(subject), version), &result) 618 619 return result.Compatible, err 620 } 621 622 // GetDefaultCompatibility fetches the global(default) compatibility level 623 // Returns global(default) compatibility level 624 func (c *client) GetDefaultCompatibility() (compatibility Compatibility, err error) { 625 var result compatibilityLevel 626 err = c.restService.handleRequest(newRequest("GET", config, nil), &result) 627 628 return result.Compatibility, err 629 } 630 631 // UpdateDefaultCompatibility updates the global(default) compatibility level level 632 // Returns new string compatibility level 633 func (c *client) UpdateDefaultCompatibility(update Compatibility) (compatibility Compatibility, err error) { 634 result := compatibilityLevel{ 635 CompatibilityUpdate: update, 636 } 637 err = c.restService.handleRequest(newRequest("PUT", config, &result), &result) 638 639 return result.CompatibilityUpdate, err 640 }