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