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  //------------------------------------------------------------------------------