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 }