github.com/Jeffail/benthos/v3@v3.65.0/lib/api/dynamic_crud.go (about) 1 package api 2 3 import ( 4 "crypto/sha256" 5 "encoding/hex" 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "sync" 11 "time" 12 13 "github.com/gorilla/mux" 14 ) 15 16 //------------------------------------------------------------------------------ 17 18 // dynamicConfMgr maintains a map of config hashes to ids for dynamic 19 // inputs/outputs and thereby tracks whether a new configuration has changed for 20 // a particular id. 21 type dynamicConfMgr struct { 22 configHashes map[string]string 23 } 24 25 func newDynamicConfMgr() *dynamicConfMgr { 26 return &dynamicConfMgr{ 27 configHashes: map[string]string{}, 28 } 29 } 30 31 // Set will cache the config hash as the latest for the id and returns whether 32 // this hash is different to the previous config. 33 func (d *dynamicConfMgr) Set(id string, conf []byte) bool { 34 hasher := sha256.New() 35 hasher.Write(conf) 36 newHash := hex.EncodeToString(hasher.Sum(nil)) 37 38 if hash, exists := d.configHashes[id]; exists { 39 if hash == newHash { 40 // Same config as before, ignore. 41 return false 42 } 43 } 44 45 d.configHashes[id] = newHash 46 return true 47 } 48 49 // Matches checks whether a provided config matches an existing config for the 50 // same id. 51 func (d *dynamicConfMgr) Matches(id string, conf []byte) bool { 52 if hash, exists := d.configHashes[id]; exists { 53 hasher := sha256.New() 54 hasher.Write(conf) 55 newHash := hex.EncodeToString(hasher.Sum(nil)) 56 57 if hash == newHash { 58 // Same config as before. 59 return true 60 } 61 } 62 63 return false 64 } 65 66 // Remove will delete a cached hash for id if there is one. 67 func (d *dynamicConfMgr) Remove(id string) { 68 delete(d.configHashes, id) 69 } 70 71 //------------------------------------------------------------------------------ 72 73 // Dynamic is a type for exposing CRUD operations on dynamic broker 74 // configurations as an HTTP interface. Events can be registered for listening 75 // to configuration changes, and these events should be forwarded to the 76 // dynamic broker. 77 type Dynamic struct { 78 onUpdate func(id string, conf []byte) error 79 onDelete func(id string) error 80 81 // configs is a map of the latest sanitised configs from our CRUD clients. 82 configs map[string][]byte 83 configHashes *dynamicConfMgr 84 configsMut sync.Mutex 85 86 // ids is a map of dynamic components that are currently active and their 87 // start times. 88 ids map[string]time.Time 89 idsMut sync.Mutex 90 } 91 92 // NewDynamic creates a new Dynamic API type. 93 func NewDynamic() *Dynamic { 94 return &Dynamic{ 95 onUpdate: func(id string, conf []byte) error { return nil }, 96 onDelete: func(id string) error { return nil }, 97 configs: map[string][]byte{}, 98 configHashes: newDynamicConfMgr(), 99 ids: map[string]time.Time{}, 100 } 101 } 102 103 //------------------------------------------------------------------------------ 104 105 // OnUpdate registers a func to handle CRUD events where a request wants to set 106 // a new value for a dynamic configuration. An error should be returned if the 107 // configuration is invalid or the component failed. 108 func (d *Dynamic) OnUpdate(onUpdate func(id string, conf []byte) error) { 109 d.onUpdate = onUpdate 110 } 111 112 // OnDelete registers a func to handle CRUD events where a request wants to 113 // remove a dynamic configuration. An error should be returned if the component 114 // failed to close. 115 func (d *Dynamic) OnDelete(onDelete func(id string) error) { 116 d.onDelete = onDelete 117 } 118 119 // Stopped should be called whenever an active dynamic component has closed, 120 // whether by naturally winding down or from a request. 121 func (d *Dynamic) Stopped(id string) { 122 d.idsMut.Lock() 123 defer d.idsMut.Unlock() 124 125 delete(d.ids, id) 126 } 127 128 // Started should be called whenever an active dynamic component has started 129 // with a new configuration. A normalised form of the configuration should be 130 // provided and will be delivered to clients that query the component contents. 131 func (d *Dynamic) Started(id string, config []byte) { 132 d.idsMut.Lock() 133 d.ids[id] = time.Now() 134 d.idsMut.Unlock() 135 136 if len(config) > 0 { 137 d.configsMut.Lock() 138 d.configs[id] = config 139 d.configsMut.Unlock() 140 } 141 } 142 143 //------------------------------------------------------------------------------ 144 145 // HandleList is an http.HandleFunc for returning maps of active dynamic 146 // components by their id to uptime. 147 func (d *Dynamic) HandleList(w http.ResponseWriter, r *http.Request) { 148 var httpErr error 149 defer func() { 150 if r.Body != nil { 151 r.Body.Close() 152 } 153 if httpErr != nil { 154 http.Error(w, "Internal server error", http.StatusBadGateway) 155 return 156 } 157 }() 158 159 type confInfo struct { 160 Uptime string `json:"uptime"` 161 Config json.RawMessage `json:"config"` 162 } 163 uptimes := map[string]confInfo{} 164 165 d.idsMut.Lock() 166 for k, v := range d.ids { 167 uptimes[k] = confInfo{ 168 Uptime: time.Since(v).String(), 169 Config: []byte(`null`), 170 } 171 } 172 d.idsMut.Unlock() 173 174 d.configsMut.Lock() 175 for k, v := range d.configs { 176 if info, exists := uptimes[k]; exists { 177 info.Config = v 178 uptimes[k] = info 179 } else { 180 uptimes[k] = confInfo{ 181 Uptime: "stopped", 182 Config: v, 183 } 184 } 185 } 186 d.configsMut.Unlock() 187 188 var resBytes []byte 189 if resBytes, httpErr = json.Marshal(uptimes); httpErr == nil { 190 w.Write(resBytes) 191 } 192 } 193 194 func (d *Dynamic) handleGETInput(w http.ResponseWriter, r *http.Request) error { 195 id := mux.Vars(r)["id"] 196 197 d.configsMut.Lock() 198 conf, exists := d.configs[id] 199 d.configsMut.Unlock() 200 if !exists { 201 http.Error(w, fmt.Sprintf("Dynamic component '%v' is not active", id), http.StatusNotFound) 202 return nil 203 } 204 w.Write(conf) 205 return nil 206 } 207 208 func (d *Dynamic) handlePOSTInput(w http.ResponseWriter, r *http.Request) error { 209 id := mux.Vars(r)["id"] 210 211 reqBytes, err := io.ReadAll(r.Body) 212 if err != nil { 213 return err 214 } 215 216 d.configsMut.Lock() 217 matched := d.configHashes.Matches(id, reqBytes) 218 d.configsMut.Unlock() 219 if matched { 220 return nil 221 } 222 223 if err := d.onUpdate(id, reqBytes); err != nil { 224 return err 225 } 226 227 d.configsMut.Lock() 228 d.configHashes.Set(id, reqBytes) 229 d.configsMut.Unlock() 230 return nil 231 } 232 233 func (d *Dynamic) handleDELInput(w http.ResponseWriter, r *http.Request) error { 234 id := mux.Vars(r)["id"] 235 236 if err := d.onDelete(id); err != nil { 237 return err 238 } 239 240 d.configsMut.Lock() 241 d.configHashes.Remove(id) 242 delete(d.configs, id) 243 d.configsMut.Unlock() 244 245 return nil 246 } 247 248 // HandleCRUD is an http.HandleFunc for performing CRUD operations on dynamic 249 // components by their ids. 250 func (d *Dynamic) HandleCRUD(w http.ResponseWriter, r *http.Request) { 251 var httpErr error 252 defer func() { 253 if r.Body != nil { 254 r.Body.Close() 255 } 256 if httpErr != nil { 257 http.Error(w, fmt.Sprintf("Error: %v", httpErr), http.StatusBadGateway) 258 return 259 } 260 }() 261 262 id := mux.Vars(r)["id"] 263 if id == "" { 264 http.Error(w, "Var `id` must be set", http.StatusBadRequest) 265 return 266 } 267 268 switch r.Method { 269 case "POST": 270 httpErr = d.handlePOSTInput(w, r) 271 case "GET": 272 httpErr = d.handleGETInput(w, r) 273 case "DELETE": 274 httpErr = d.handleDELInput(w, r) 275 default: 276 httpErr = fmt.Errorf("verb not supported: %v", r.Method) 277 } 278 } 279 280 //------------------------------------------------------------------------------