github.com/Jeffail/benthos/v3@v3.65.0/lib/stream/manager/api.go (about)

     1  package manager
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net/http"
    10  	"sort"
    11  	"strings"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/Jeffail/benthos/v3/internal/bundle"
    16  	"github.com/Jeffail/benthos/v3/internal/docs"
    17  	"github.com/Jeffail/benthos/v3/lib/buffer"
    18  	"github.com/Jeffail/benthos/v3/lib/cache"
    19  	"github.com/Jeffail/benthos/v3/lib/input"
    20  	"github.com/Jeffail/benthos/v3/lib/output"
    21  	"github.com/Jeffail/benthos/v3/lib/pipeline"
    22  	"github.com/Jeffail/benthos/v3/lib/processor"
    23  	"github.com/Jeffail/benthos/v3/lib/ratelimit"
    24  	"github.com/Jeffail/benthos/v3/lib/stream"
    25  	"github.com/Jeffail/benthos/v3/lib/util/text"
    26  	"github.com/Jeffail/gabs/v2"
    27  	"github.com/gorilla/mux"
    28  	"gopkg.in/yaml.v3"
    29  )
    30  
    31  //------------------------------------------------------------------------------
    32  
    33  func (m *Type) registerEndpoints(enableCrud bool) {
    34  	m.manager.RegisterEndpoint(
    35  		"/ready",
    36  		"Returns 200 OK if the inputs and outputs of all running streams are connected, otherwise a 503 is returned. If there are no active streams 200 is returned.",
    37  		m.HandleStreamReady,
    38  	)
    39  	if !enableCrud {
    40  		return
    41  	}
    42  	m.manager.RegisterEndpoint(
    43  		"/streams",
    44  		"GET: List all streams along with their status and uptimes."+
    45  			" POST: Post an object of stream ids to stream configs, all"+
    46  			" streams will be replaced by this new set.",
    47  		m.HandleStreamsCRUD,
    48  	)
    49  	m.manager.RegisterEndpoint(
    50  		"/streams/{id}",
    51  		"Perform CRUD operations on streams, supporting POST (Create),"+
    52  			" GET (Read), PUT (Update), PATCH (Patch update)"+
    53  			" and DELETE (Delete).",
    54  		m.HandleStreamCRUD,
    55  	)
    56  	m.manager.RegisterEndpoint(
    57  		"/streams/{id}/stats",
    58  		"GET a structured JSON object containing metrics for the stream.",
    59  		m.HandleStreamStats,
    60  	)
    61  	m.manager.RegisterEndpoint(
    62  		"/resources/{type}/{id}",
    63  		"POST: Create or replace a given resource configuration of a specified type. Types supported are `cache`, `input`, `output`, `processor` and `rate_limit`.",
    64  		m.HandleResourceCRUD,
    65  	)
    66  }
    67  
    68  // ConfigSet is a map of stream configurations mapped by ID, which can be YAML
    69  // parsed without losing default values inside the stream configs.
    70  type ConfigSet map[string]stream.Config
    71  
    72  // UnmarshalYAML ensures that when parsing configs that are in a map or slice
    73  // the default values are still applied.
    74  func (c ConfigSet) UnmarshalYAML(value *yaml.Node) error {
    75  	tmpSet := map[string]yaml.Node{}
    76  	if err := value.Decode(&tmpSet); err != nil {
    77  		return err
    78  	}
    79  	for k, v := range tmpSet {
    80  		conf := stream.NewConfig()
    81  		if err := v.Decode(&conf); err != nil {
    82  			return err
    83  		}
    84  		c[k] = conf
    85  	}
    86  	return nil
    87  }
    88  
    89  func lintStreamConfigNode(node *yaml.Node) (lints []string) {
    90  	for _, dLint := range stream.Spec().LintYAML(docs.NewLintContext(), node) {
    91  		lints = append(lints, fmt.Sprintf("line %v: %v", dLint.Line, dLint.What))
    92  	}
    93  	return
    94  }
    95  
    96  // HandleStreamsCRUD is an http.HandleFunc for returning maps of active benthos
    97  // streams by their id, status and uptime or overwriting the entire set of
    98  // streams.
    99  func (m *Type) HandleStreamsCRUD(w http.ResponseWriter, r *http.Request) {
   100  	var serverErr, requestErr error
   101  	defer func() {
   102  		if r.Body != nil {
   103  			r.Body.Close()
   104  		}
   105  		if serverErr != nil {
   106  			m.logger.Errorf("Streams CRUD Error: %v\n", serverErr)
   107  			http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway)
   108  			return
   109  		}
   110  		if requestErr != nil {
   111  			m.logger.Debugf("Streams request CRUD Error: %v\n", requestErr)
   112  			http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest)
   113  			return
   114  		}
   115  	}()
   116  
   117  	type confInfo struct {
   118  		Active    bool    `json:"active"`
   119  		Uptime    float64 `json:"uptime"`
   120  		UptimeStr string  `json:"uptime_str"`
   121  	}
   122  	infos := map[string]confInfo{}
   123  
   124  	m.lock.Lock()
   125  	for id, strInfo := range m.streams {
   126  		infos[id] = confInfo{
   127  			Active:    strInfo.IsRunning(),
   128  			Uptime:    strInfo.Uptime().Seconds(),
   129  			UptimeStr: strInfo.Uptime().String(),
   130  		}
   131  	}
   132  	m.lock.Unlock()
   133  
   134  	switch r.Method {
   135  	case "GET":
   136  		var resBytes []byte
   137  		if resBytes, serverErr = json.Marshal(infos); serverErr == nil {
   138  			w.Header().Set("Content-Type", "application/json")
   139  			w.Write(resBytes)
   140  		}
   141  		return
   142  	case "POST":
   143  	default:
   144  		requestErr = errors.New("method not supported")
   145  		return
   146  	}
   147  
   148  	var setBytes []byte
   149  	if setBytes, requestErr = io.ReadAll(r.Body); requestErr != nil {
   150  		return
   151  	}
   152  
   153  	if r.URL.Query().Get("chilled") != "true" {
   154  		nodeSet := map[string]yaml.Node{}
   155  		if requestErr = yaml.Unmarshal(setBytes, &nodeSet); requestErr != nil {
   156  			return
   157  		}
   158  		var lints []string
   159  		for k, n := range nodeSet {
   160  			for _, l := range lintStreamConfigNode(&n) {
   161  				keyLint := fmt.Sprintf("stream '%v': %v", k, l)
   162  				lints = append(lints, keyLint)
   163  				m.logger.Debugf("Streams request linting error: %v\n", keyLint)
   164  			}
   165  		}
   166  		if len(lints) > 0 {
   167  			sort.Strings(lints)
   168  			errBytes, _ := json.Marshal(struct {
   169  				LintErrs []string `json:"lint_errors"`
   170  			}{
   171  				LintErrs: lints,
   172  			})
   173  			w.WriteHeader(http.StatusBadRequest)
   174  			w.Write(errBytes)
   175  			return
   176  		}
   177  	}
   178  
   179  	newSet := ConfigSet{}
   180  	if requestErr = yaml.Unmarshal(setBytes, &newSet); requestErr != nil {
   181  		return
   182  	}
   183  
   184  	toDelete := []string{}
   185  	toUpdate := map[string]stream.Config{}
   186  	toCreate := map[string]stream.Config{}
   187  
   188  	for id := range infos {
   189  		if newConf, exists := newSet[id]; !exists {
   190  			toDelete = append(toDelete, id)
   191  		} else {
   192  			toUpdate[id] = newConf
   193  		}
   194  	}
   195  	for id, conf := range newSet {
   196  		if _, exists := infos[id]; !exists {
   197  			toCreate[id] = conf
   198  		}
   199  	}
   200  
   201  	deadline, hasDeadline := r.Context().Deadline()
   202  	if !hasDeadline {
   203  		deadline = time.Now().Add(m.apiTimeout)
   204  	}
   205  
   206  	wg := sync.WaitGroup{}
   207  	wg.Add(len(toDelete))
   208  	wg.Add(len(toUpdate))
   209  	wg.Add(len(toCreate))
   210  
   211  	errDelete := make([]error, len(toDelete))
   212  	errUpdate := make([]error, len(toUpdate))
   213  	errCreate := make([]error, len(toCreate))
   214  
   215  	for i, id := range toDelete {
   216  		go func(sid string, j int) {
   217  			errDelete[j] = m.Delete(sid, time.Until(deadline))
   218  			wg.Done()
   219  		}(id, i)
   220  	}
   221  	i := 0
   222  	for id, conf := range toUpdate {
   223  		newConf := conf
   224  		go func(sid string, sconf *stream.Config, j int) {
   225  			errUpdate[j] = m.Update(sid, *sconf, time.Until(deadline))
   226  			wg.Done()
   227  		}(id, &newConf, i)
   228  		i++
   229  	}
   230  	i = 0
   231  	for id, conf := range toCreate {
   232  		newConf := conf
   233  		go func(sid string, sconf *stream.Config, j int) {
   234  			errCreate[j] = m.Create(sid, *sconf)
   235  			wg.Done()
   236  		}(id, &newConf, i)
   237  		i++
   238  	}
   239  
   240  	wg.Wait()
   241  
   242  	errs := []string{}
   243  	for _, err := range errDelete {
   244  		if err != nil {
   245  			errs = append(errs, fmt.Sprintf("failed to delete stream: %v", err))
   246  		}
   247  	}
   248  	for _, err := range errUpdate {
   249  		if err != nil {
   250  			errs = append(errs, fmt.Sprintf("failed to update stream: %v", err))
   251  		}
   252  	}
   253  	for _, err := range errCreate {
   254  		if err != nil {
   255  			errs = append(errs, fmt.Sprintf("failed to create stream: %v", err))
   256  		}
   257  	}
   258  
   259  	if len(errs) > 0 {
   260  		requestErr = errors.New(strings.Join(errs, "\n"))
   261  	}
   262  }
   263  
   264  // HandleStreamCRUD is an http.HandleFunc for performing CRUD operations on
   265  // individual streams.
   266  func (m *Type) HandleStreamCRUD(w http.ResponseWriter, r *http.Request) {
   267  	var serverErr, requestErr error
   268  	defer func() {
   269  		if r.Body != nil {
   270  			r.Body.Close()
   271  		}
   272  		if serverErr != nil {
   273  			m.logger.Errorf("Streams CRUD Error: %v\n", serverErr)
   274  			http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway)
   275  			return
   276  		}
   277  		if requestErr != nil {
   278  			m.logger.Debugf("Streams request CRUD Error: %v\n", requestErr)
   279  			http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest)
   280  			return
   281  		}
   282  	}()
   283  
   284  	id := mux.Vars(r)["id"]
   285  	if id == "" {
   286  		http.Error(w, "Var `id` must be set", http.StatusBadRequest)
   287  		return
   288  	}
   289  
   290  	readConfig := func() (confOut stream.Config, lints []string, err error) {
   291  		var confBytes []byte
   292  		if confBytes, err = io.ReadAll(r.Body); err != nil {
   293  			return
   294  		}
   295  		confBytes = text.ReplaceEnvVariables(confBytes)
   296  
   297  		if r.URL.Query().Get("chilled") != "true" {
   298  			var node yaml.Node
   299  			if err = yaml.Unmarshal(confBytes, &node); err != nil {
   300  				return
   301  			}
   302  			lints = lintStreamConfigNode(&node)
   303  			for _, l := range lints {
   304  				m.logger.Infof("Stream '%v' config: %v\n", id, l)
   305  			}
   306  		}
   307  
   308  		confOut = stream.NewConfig()
   309  		err = yaml.Unmarshal(confBytes, &confOut)
   310  		return
   311  	}
   312  	patchConfig := func(confIn stream.Config) (confOut stream.Config, err error) {
   313  		var patchBytes []byte
   314  		if patchBytes, err = io.ReadAll(r.Body); err != nil {
   315  			return
   316  		}
   317  
   318  		type aliasedIn input.Config
   319  		type aliasedBuf buffer.Config
   320  		type aliasedPipe pipeline.Config
   321  		type aliasedOut output.Config
   322  
   323  		aliasedConf := struct {
   324  			Input    aliasedIn   `json:"input"`
   325  			Buffer   aliasedBuf  `json:"buffer"`
   326  			Pipeline aliasedPipe `json:"pipeline"`
   327  			Output   aliasedOut  `json:"output"`
   328  		}{
   329  			Input:    aliasedIn(confIn.Input),
   330  			Buffer:   aliasedBuf(confIn.Buffer),
   331  			Pipeline: aliasedPipe(confIn.Pipeline),
   332  			Output:   aliasedOut(confIn.Output),
   333  		}
   334  		if err = yaml.Unmarshal(patchBytes, &aliasedConf); err != nil {
   335  			return
   336  		}
   337  		confOut = stream.Config{
   338  			Input:    input.Config(aliasedConf.Input),
   339  			Buffer:   buffer.Config(aliasedConf.Buffer),
   340  			Pipeline: pipeline.Config(aliasedConf.Pipeline),
   341  			Output:   output.Config(aliasedConf.Output),
   342  		}
   343  		return
   344  	}
   345  
   346  	deadline, hasDeadline := r.Context().Deadline()
   347  	if !hasDeadline {
   348  		deadline = time.Now().Add(m.apiTimeout)
   349  	}
   350  
   351  	var conf stream.Config
   352  	var lints []string
   353  	switch r.Method {
   354  	case "POST":
   355  		if conf, lints, requestErr = readConfig(); requestErr != nil {
   356  			return
   357  		}
   358  		if len(lints) > 0 {
   359  			errBytes, _ := json.Marshal(struct {
   360  				LintErrs []string `json:"lint_errors"`
   361  			}{
   362  				LintErrs: lints,
   363  			})
   364  			w.WriteHeader(http.StatusBadRequest)
   365  			w.Write(errBytes)
   366  			return
   367  		}
   368  		serverErr = m.Create(id, conf)
   369  	case "GET":
   370  		var info *StreamStatus
   371  		if info, serverErr = m.Read(id); serverErr == nil {
   372  			sanit, _ := info.Config().Sanitised()
   373  
   374  			var bodyBytes []byte
   375  			if bodyBytes, serverErr = json.Marshal(struct {
   376  				Active    bool        `json:"active"`
   377  				Uptime    float64     `json:"uptime"`
   378  				UptimeStr string      `json:"uptime_str"`
   379  				Config    interface{} `json:"config"`
   380  			}{
   381  				Active:    info.IsRunning(),
   382  				Uptime:    info.Uptime().Seconds(),
   383  				UptimeStr: info.Uptime().String(),
   384  				Config:    sanit,
   385  			}); serverErr != nil {
   386  				return
   387  			}
   388  
   389  			w.Header().Set("Content-Type", "application/json")
   390  			w.Write(bodyBytes)
   391  		}
   392  	case "PUT":
   393  		if conf, lints, requestErr = readConfig(); requestErr != nil {
   394  			return
   395  		}
   396  		if len(lints) > 0 {
   397  			errBytes, _ := json.Marshal(struct {
   398  				LintErrs []string `json:"lint_errors"`
   399  			}{
   400  				LintErrs: lints,
   401  			})
   402  			w.WriteHeader(http.StatusBadRequest)
   403  			w.Write(errBytes)
   404  			return
   405  		}
   406  		serverErr = m.Update(id, conf, time.Until(deadline))
   407  	case "DELETE":
   408  		serverErr = m.Delete(id, time.Until(deadline))
   409  	case "PATCH":
   410  		var info *StreamStatus
   411  		if info, serverErr = m.Read(id); serverErr == nil {
   412  			if conf, requestErr = patchConfig(info.Config()); requestErr != nil {
   413  				return
   414  			}
   415  			serverErr = m.Update(id, conf, time.Until(deadline))
   416  		}
   417  	default:
   418  		requestErr = fmt.Errorf("verb not supported: %v", r.Method)
   419  	}
   420  
   421  	if serverErr == ErrStreamDoesNotExist {
   422  		serverErr = nil
   423  		http.Error(w, "Stream not found", http.StatusNotFound)
   424  		return
   425  	}
   426  	if serverErr == ErrStreamExists {
   427  		serverErr = nil
   428  		http.Error(w, "Stream already exists", http.StatusBadRequest)
   429  		return
   430  	}
   431  }
   432  
   433  // HandleResourceCRUD is an http.HandleFunc for performing CRUD operations on
   434  // resource components.
   435  func (m *Type) HandleResourceCRUD(w http.ResponseWriter, r *http.Request) {
   436  	var serverErr, requestErr error
   437  	defer func() {
   438  		if r.Body != nil {
   439  			r.Body.Close()
   440  		}
   441  		if serverErr != nil {
   442  			m.logger.Errorf("Resource CRUD Error: %v\n", serverErr)
   443  			http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway)
   444  			return
   445  		}
   446  		if requestErr != nil {
   447  			m.logger.Debugf("Resource request CRUD Error: %v\n", requestErr)
   448  			http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest)
   449  			return
   450  		}
   451  	}()
   452  
   453  	if r.Method != "POST" {
   454  		requestErr = fmt.Errorf("verb not supported: %v", r.Method)
   455  		return
   456  	}
   457  
   458  	id := mux.Vars(r)["id"]
   459  	if id == "" {
   460  		http.Error(w, "Var `id` must be set", http.StatusBadRequest)
   461  		return
   462  	}
   463  
   464  	newMgr, ok := m.manager.(bundle.NewManagement)
   465  	if !ok {
   466  		serverErr = errors.New("server does not support resource CRUD operations")
   467  		return
   468  	}
   469  
   470  	ctx, done := context.WithDeadline(r.Context(), time.Now().Add(m.apiTimeout))
   471  	defer done()
   472  
   473  	var storeFn func(*yaml.Node)
   474  
   475  	docType := docs.Type(mux.Vars(r)["type"])
   476  	switch docType {
   477  	case docs.TypeCache:
   478  		storeFn = func(n *yaml.Node) {
   479  			cacheConf := cache.NewConfig()
   480  			if requestErr = n.Decode(&cacheConf); requestErr != nil {
   481  				return
   482  			}
   483  			serverErr = newMgr.StoreCache(ctx, id, cacheConf)
   484  		}
   485  	case docs.TypeInput:
   486  		storeFn = func(n *yaml.Node) {
   487  			inputConf := input.NewConfig()
   488  			if requestErr = n.Decode(&inputConf); requestErr != nil {
   489  				return
   490  			}
   491  			serverErr = newMgr.StoreInput(ctx, id, inputConf)
   492  		}
   493  	case docs.TypeOutput:
   494  		storeFn = func(n *yaml.Node) {
   495  			outputConf := output.NewConfig()
   496  			if requestErr = n.Decode(&outputConf); requestErr != nil {
   497  				return
   498  			}
   499  			serverErr = newMgr.StoreOutput(ctx, id, outputConf)
   500  		}
   501  	case docs.TypeProcessor:
   502  		storeFn = func(n *yaml.Node) {
   503  			procConf := processor.NewConfig()
   504  			if requestErr = n.Decode(&procConf); requestErr != nil {
   505  				return
   506  			}
   507  			serverErr = newMgr.StoreProcessor(ctx, id, procConf)
   508  		}
   509  	case docs.TypeRateLimit:
   510  		storeFn = func(n *yaml.Node) {
   511  			rlConf := ratelimit.NewConfig()
   512  			if requestErr = n.Decode(&rlConf); requestErr != nil {
   513  				return
   514  			}
   515  			serverErr = newMgr.StoreRateLimit(ctx, id, rlConf)
   516  		}
   517  	default:
   518  		http.Error(w, "Var `type` must be set to one of `cache`, `input`, `output`, `processor` or `rate_limit`", http.StatusBadRequest)
   519  		return
   520  	}
   521  
   522  	var confNode *yaml.Node
   523  	var lints []string
   524  	{
   525  		var confBytes []byte
   526  		if confBytes, requestErr = io.ReadAll(r.Body); requestErr != nil {
   527  			return
   528  		}
   529  		confBytes = text.ReplaceEnvVariables(confBytes)
   530  
   531  		var node yaml.Node
   532  		if requestErr = yaml.Unmarshal(confBytes, &node); requestErr != nil {
   533  			return
   534  		}
   535  		confNode = &node
   536  
   537  		if r.URL.Query().Get("chilled") != "true" {
   538  			for _, l := range docs.LintYAML(docs.NewLintContext(), docType, &node) {
   539  				lints = append(lints, fmt.Sprintf("line %v: %v", l.Line, l.What))
   540  				m.logger.Infof("Resource '%v' config: %v\n", id, l)
   541  			}
   542  		}
   543  	}
   544  	if len(lints) > 0 {
   545  		errBytes, _ := json.Marshal(struct {
   546  			LintErrs []string `json:"lint_errors"`
   547  		}{
   548  			LintErrs: lints,
   549  		})
   550  		w.WriteHeader(http.StatusBadRequest)
   551  		w.Write(errBytes)
   552  		return
   553  	}
   554  
   555  	storeFn(confNode)
   556  }
   557  
   558  // HandleStreamStats is an http.HandleFunc for obtaining metrics for a stream.
   559  func (m *Type) HandleStreamStats(w http.ResponseWriter, r *http.Request) {
   560  	var serverErr, requestErr error
   561  	defer func() {
   562  		if r.Body != nil {
   563  			r.Body.Close()
   564  		}
   565  		if serverErr != nil {
   566  			m.logger.Errorf("Stream stats Error: %v\n", serverErr)
   567  			http.Error(w, fmt.Sprintf("Error: %v", serverErr), http.StatusBadGateway)
   568  			return
   569  		}
   570  		if requestErr != nil {
   571  			m.logger.Debugf("Stream request stats Error: %v\n", requestErr)
   572  			http.Error(w, fmt.Sprintf("Error: %v", requestErr), http.StatusBadRequest)
   573  			return
   574  		}
   575  	}()
   576  
   577  	id := mux.Vars(r)["id"]
   578  	if id == "" {
   579  		http.Error(w, "Var `id` must be set", http.StatusBadRequest)
   580  		return
   581  	}
   582  
   583  	switch r.Method {
   584  	case "GET":
   585  		var info *StreamStatus
   586  		if info, serverErr = m.Read(id); serverErr == nil {
   587  			uptime := info.Uptime().String()
   588  			counters := info.Metrics().GetCounters()
   589  			timings := info.Metrics().GetTimings()
   590  
   591  			obj := gabs.New()
   592  			for k, v := range counters {
   593  				k = strings.TrimPrefix(k, id+".")
   594  				obj.SetP(v, k)
   595  			}
   596  			for k, v := range timings {
   597  				k = strings.TrimPrefix(k, id+".")
   598  				obj.SetP(v, k)
   599  				obj.SetP(time.Duration(v).String(), k+"_readable")
   600  			}
   601  			obj.SetP(uptime, "uptime")
   602  			w.Header().Set("Content-Type", "application/json")
   603  			w.Write(obj.Bytes())
   604  		}
   605  	default:
   606  		requestErr = fmt.Errorf("verb not supported: %v", r.Method)
   607  	}
   608  	if serverErr == ErrStreamDoesNotExist {
   609  		serverErr = nil
   610  		http.Error(w, "Stream not found", http.StatusNotFound)
   611  		return
   612  	}
   613  }
   614  
   615  // HandleStreamReady is an http.HandleFunc for providing a ready check across
   616  // all streams.
   617  func (m *Type) HandleStreamReady(w http.ResponseWriter, r *http.Request) {
   618  	var notReady []string
   619  
   620  	m.lock.Lock()
   621  	for k, v := range m.streams {
   622  		if !v.IsReady() {
   623  			notReady = append(notReady, k)
   624  		}
   625  	}
   626  	m.lock.Unlock()
   627  
   628  	if len(notReady) == 0 {
   629  		w.Write([]byte("OK"))
   630  		return
   631  	}
   632  
   633  	w.WriteHeader(http.StatusServiceUnavailable)
   634  	fmt.Fprintf(w, "streams %v are not connected\n", strings.Join(notReady, ", "))
   635  }
   636  
   637  //------------------------------------------------------------------------------