github.com/cilium/statedb@v0.3.2/http_client.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package statedb
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/base64"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"io"
    14  	"iter"
    15  	"net/http"
    16  	"net/url"
    17  )
    18  
    19  // NewRemoteTable creates a new handle for querying a remote StateDB table over the HTTP.
    20  // Example usage:
    21  //
    22  //	devices := statedb.NewRemoteTable[*tables.Device](url.Parse("http://localhost:8080/db"), "devices")
    23  //
    24  //	// Get all devices ordered by name.
    25  //	iter, errs := devices.LowerBound(ctx, tables.DeviceByName(""))
    26  //	for device, revision, ok := iter.Next(); ok; device, revision, ok = iter.Next() { ... }
    27  //
    28  //	// Get device by name.
    29  //	iter, errs := devices.Get(ctx, tables.DeviceByName("eth0"))
    30  //	if dev, revision, ok := iter.Next(); ok { ... }
    31  //
    32  //	// Get devices in revision order, e.g. oldest changed devices first.
    33  //	iter, errs = devices.LowerBound(ctx, statedb.ByRevision(0))
    34  func NewRemoteTable[Obj any](base *url.URL, table TableName) *RemoteTable[Obj] {
    35  	return &RemoteTable[Obj]{base: base, tableName: table}
    36  }
    37  
    38  type RemoteTable[Obj any] struct {
    39  	client    http.Client
    40  	base      *url.URL
    41  	tableName TableName
    42  }
    43  
    44  func (t *RemoteTable[Obj]) SetTransport(tr *http.Transport) {
    45  	t.client.Transport = tr
    46  }
    47  
    48  func (t *RemoteTable[Obj]) query(ctx context.Context, lowerBound bool, q Query[Obj]) (seq iter.Seq2[Obj, Revision], errChan <-chan error) {
    49  	// Use a channel to return errors so we can use the same Iterator[Obj] interface as StateDB does.
    50  	errChanSend := make(chan error, 1)
    51  	errChan = errChanSend
    52  
    53  	key := base64.StdEncoding.EncodeToString(q.key)
    54  	queryReq := QueryRequest{
    55  		Key:        key,
    56  		Table:      t.tableName,
    57  		Index:      q.index,
    58  		LowerBound: lowerBound,
    59  	}
    60  	bs, err := json.Marshal(&queryReq)
    61  	if err != nil {
    62  		errChanSend <- err
    63  		return
    64  	}
    65  
    66  	url := t.base.JoinPath("/query")
    67  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), bytes.NewBuffer(bs))
    68  	if err != nil {
    69  		errChanSend <- err
    70  		return
    71  	}
    72  	req.Header.Add("Content-Type", "application/json")
    73  	req.Header.Add("Accept", "application/json")
    74  
    75  	resp, err := t.client.Do(req)
    76  	if err != nil {
    77  		errChanSend <- err
    78  		return
    79  	}
    80  	return remoteGetSeq[Obj](json.NewDecoder(resp.Body), errChanSend), errChan
    81  }
    82  
    83  func (t *RemoteTable[Obj]) Get(ctx context.Context, q Query[Obj]) (iter.Seq2[Obj, Revision], <-chan error) {
    84  	return t.query(ctx, false, q)
    85  }
    86  
    87  func (t *RemoteTable[Obj]) LowerBound(ctx context.Context, q Query[Obj]) (iter.Seq2[Obj, Revision], <-chan error) {
    88  	return t.query(ctx, true, q)
    89  }
    90  
    91  // responseObject is a typed counterpart of [queryResponseObject]
    92  type responseObject[Obj any] struct {
    93  	Rev uint64 `json:"rev"`
    94  	Obj Obj    `json:"obj"`
    95  	Err string `json:"err,omitempty"`
    96  }
    97  
    98  func remoteGetSeq[Obj any](dec *json.Decoder, errChan chan error) iter.Seq2[Obj, Revision] {
    99  	return func(yield func(Obj, Revision) bool) {
   100  		for {
   101  			var resp responseObject[Obj]
   102  			err := dec.Decode(&resp)
   103  			errString := ""
   104  			if err != nil {
   105  				if errors.Is(err, io.EOF) {
   106  					close(errChan)
   107  					break
   108  				}
   109  				errString = "Decode error: " + err.Error()
   110  			} else {
   111  				errString = resp.Err
   112  			}
   113  			if errString != "" {
   114  				errChan <- errors.New(errString)
   115  				break
   116  			}
   117  			if !yield(resp.Obj, resp.Rev) {
   118  				break
   119  			}
   120  		}
   121  	}
   122  }
   123  
   124  func (t *RemoteTable[Obj]) Changes(ctx context.Context) (seq iter.Seq2[Change[Obj], Revision], errChan <-chan error) {
   125  	// Use a channel to return errors so we can use the same Iterator[Obj] interface as StateDB does.
   126  	errChanSend := make(chan error, 1)
   127  	errChan = errChanSend
   128  
   129  	url := t.base.JoinPath("/changes", t.tableName)
   130  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
   131  	if err != nil {
   132  		errChanSend <- err
   133  		close(errChanSend)
   134  		return
   135  	}
   136  
   137  	req.Header.Add("Content-Type", "application/json")
   138  	req.Header.Add("Accept", "application/json")
   139  
   140  	resp, err := t.client.Do(req)
   141  	if err != nil {
   142  		errChanSend <- err
   143  		close(errChanSend)
   144  		return
   145  	}
   146  	return remoteChangeSeq[Obj](json.NewDecoder(resp.Body), errChanSend), errChan
   147  }
   148  
   149  func remoteChangeSeq[Obj any](dec *json.Decoder, errChan chan error) iter.Seq2[Change[Obj], Revision] {
   150  	return func(yield func(Change[Obj], Revision) bool) {
   151  		defer close(errChan)
   152  		for {
   153  			var change Change[Obj]
   154  			err := dec.Decode(&change)
   155  			if err == nil && change.Revision == 0 {
   156  				// Keep-alive message, skip it.
   157  				continue
   158  			}
   159  
   160  			if err != nil {
   161  				if !errors.Is(err, io.EOF) {
   162  					errChan <- fmt.Errorf("decode error: %w", err)
   163  				}
   164  				return
   165  			}
   166  
   167  			if !yield(change, change.Revision) {
   168  				return
   169  			}
   170  		}
   171  	}
   172  }