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  }