From ca0055287856f4242635790469b606e1ff79e94c Mon Sep 17 00:00:00 2001 From: Yuriy Losev Date: Mon, 9 Oct 2023 23:09:30 +0400 Subject: [PATCH] debugserver --- pkg/debug/server.go | 105 +++++++++++-------- pkg/hook/task_metadata/task_metadata_test.go | 21 +++- pkg/shell-operator/debug_server.go | 36 ++++--- pkg/task/dump/dump.go | 50 +++++---- 4 files changed, 127 insertions(+), 85 deletions(-) diff --git a/pkg/debug/server.go b/pkg/debug/server.go index e3aef25a..0161efae 100644 --- a/pkg/debug/server.go +++ b/pkg/debug/server.go @@ -3,6 +3,7 @@ package debug import ( "encoding/json" "fmt" + "io" "net" "net/http" "os" @@ -11,7 +12,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" log "github.com/sirupsen/logrus" - "sigs.k8s.io/yaml" + "gopkg.in/yaml.v3" utils "github.com/flant/shell-operator/pkg/utils/file" structured_logger "github.com/flant/shell-operator/pkg/utils/structured-logger" @@ -26,10 +27,15 @@ type Server struct { } func NewServer(prefix, socketPath, httpAddr string) *Server { + router := chi.NewRouter() + router.Use(structured_logger.NewStructuredLogger(log.StandardLogger(), "debugEndpoint")) + router.Use(middleware.Recoverer) + return &Server{ Prefix: prefix, SocketPath: socketPath, HttpAddr: httpAddr, + Router: router, } } @@ -64,10 +70,6 @@ func (s *Server) Init() (err error) { log.Infof("Debug endpoint listen on %s", address) - s.Router = chi.NewRouter() - s.Router.Use(structured_logger.NewStructuredLogger(log.StandardLogger(), "debugEndpoint")) - s.Router.Use(middleware.Recoverer) - go func() { if err := http.Serve(listener, s.Router); err != nil { log.Errorf("Error starting Debug socket server: %s", err) @@ -87,85 +89,90 @@ func (s *Server) Init() (err error) { return nil } -func (s *Server) Route(pattern string, handler func(request *http.Request) (interface{}, error)) { - // Should not happen. - if handler == nil { +// RegisterHandler registers http handler for unix/http debug server +func (s *Server) RegisterHandler(method, pattern string, handler func(request *http.Request) (interface{}, error)) { + if method == "" { + return + } + if pattern == "" { return } - s.Router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) { - handleFormattedOutput(writer, request, handler) - }) -} - -func (s *Server) RoutePOST(pattern string, handler func(request *http.Request) (interface{}, error)) { - // Should not happen. if handler == nil { return } - s.Router.Post(pattern, func(writer http.ResponseWriter, request *http.Request) { - // - err := request.ParseForm() - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - _, _ = fmt.Fprintf(writer, "Error: %s", err) - return - } - handleFormattedOutput(writer, request, handler) - }) + switch method { + case http.MethodGet: + s.Router.Get(pattern, func(writer http.ResponseWriter, request *http.Request) { + handleFormattedOutput(writer, request, handler) + }) + + case http.MethodPost: + s.Router.Post(pattern, func(writer http.ResponseWriter, request *http.Request) { + handleFormattedOutput(writer, request, handler) + }) + } } func handleFormattedOutput(writer http.ResponseWriter, request *http.Request, handler func(request *http.Request) (interface{}, error)) { out, err := handler(request) if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - _, _ = fmt.Fprintf(writer, "Error: %s", err) + if _, ok := err.(*BadRequestError); ok { + http.Error(writer, err.Error(), http.StatusBadRequest) + return + } + + http.Error(writer, err.Error(), http.StatusInternalServerError) return } - if out == nil && err == nil { + if out == nil { + writer.WriteHeader(http.StatusOK) return } format := FormatFromRequest(request) structured_logger.GetLogEntry(request).Debugf("use format '%s'", format) - outBytes, err := transformUsingFormat(out, format) - if err != nil { - writer.WriteHeader(http.StatusInternalServerError) - _, _ = fmt.Fprintf(writer, "Error '%s' transform: %s", format, err) - return + switch format { + case "text": + writer.Header().Set("Content-Type", "text/plain; charset=utf-8") + case "json": + writer.Header().Set("Content-Type", "application/json") + case "yaml": + writer.Header().Set("Content-Type", "application/yaml") } + writer.WriteHeader(http.StatusOK) - _, _ = writer.Write(outBytes) + err = transformUsingFormat(writer, out, format) + if err != nil { + http.Error(writer, fmt.Sprintf("Error '%s' transform: %s", format, err), http.StatusInternalServerError) + } } -func transformUsingFormat(val interface{}, format string) ([]byte, error) { - var outBytes []byte - var err error - +func transformUsingFormat(w io.Writer, val interface{}, format string) (err error) { switch format { case "yaml": - outBytes, err = yaml.Marshal(val) + err = yaml.NewEncoder(w).Encode(val) case "text": switch v := val.(type) { case string: - outBytes = []byte(v) + _, err = w.Write([]byte(v)) case fmt.Stringer: - outBytes = []byte(v.String()) + _, err = w.Write([]byte(v.String())) case []byte: - outBytes = v + _, err = w.Write(v) } - if outBytes != nil { + if err == nil { break } fallthrough case "json": fallthrough default: - outBytes, err = json.Marshal(val) + err = json.NewEncoder(w).Encode(val) } - return outBytes, err + return err } func FormatFromRequest(request *http.Request) string { @@ -175,3 +182,11 @@ func FormatFromRequest(request *http.Request) string { } return format } + +type BadRequestError struct { + Msg string +} + +func (be *BadRequestError) Error() string { + return be.Msg +} diff --git a/pkg/hook/task_metadata/task_metadata_test.go b/pkg/hook/task_metadata/task_metadata_test.go index 620be301..ce3dd0e2 100644 --- a/pkg/hook/task_metadata/task_metadata_test.go +++ b/pkg/hook/task_metadata/task_metadata_test.go @@ -1,6 +1,8 @@ package task_metadata import ( + "fmt" + "strings" "testing" . "github.com/onsi/gomega" @@ -8,7 +10,6 @@ import ( . "github.com/flant/shell-operator/pkg/hook/binding_context" . "github.com/flant/shell-operator/pkg/hook/types" "github.com/flant/shell-operator/pkg/task" - "github.com/flant/shell-operator/pkg/task/dump" "github.com/flant/shell-operator/pkg/task/queue" ) @@ -70,7 +71,7 @@ func Test_HookMetadata_QueueDump_Task_Description(t *testing.T) { WithLogLabels(logLabels). WithQueueName("main")) - queueDump := dump.TaskQueueToText(q) + queueDump := taskQueueToText(q) g.Expect(queueDump).Should(ContainSubstring("hook1.sh"), "Queue dump should reveal a hook name.") g.Expect(queueDump).Should(ContainSubstring("EnableKubernetesBindings"), "Queue dump should reveal EnableKubernetesBindings.") @@ -78,3 +79,19 @@ func Test_HookMetadata_QueueDump_Task_Description(t *testing.T) { g.Expect(queueDump).Should(ContainSubstring(":schedule:"), "Queue dump should show schedule binding.") g.Expect(queueDump).Should(ContainSubstring("group=monitor_pods"), "Queue dump should show group name.") } + +func taskQueueToText(q *queue.TaskQueue) string { + var buf strings.Builder + buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", q.Name, q.Length(), q.Status)) + buf.WriteString("\n") + + index := 1 + q.Iterate(func(task task.Task) { + buf.WriteString(fmt.Sprintf("%2d. ", index)) + buf.WriteString(task.GetDescription()) + buf.WriteString("\n") + index++ + }) + + return buf.String() +} diff --git a/pkg/shell-operator/debug_server.go b/pkg/shell-operator/debug_server.go index 8ad99558..82e879e3 100644 --- a/pkg/shell-operator/debug_server.go +++ b/pkg/shell-operator/debug_server.go @@ -18,7 +18,7 @@ import ( func RunDefaultDebugServer(unixSocket, httpServerAddress string) (*debug.Server, error) { dbgSrv := debug.NewServer("/debug", unixSocket, httpServerAddress) - dbgSrv.Route("/", func(_ *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodGet, "/", func(_ *http.Request) (interface{}, error) { return "debug endpoint is alive", nil }) @@ -30,11 +30,12 @@ func RunDefaultDebugServer(unixSocket, httpServerAddress string) (*debug.Server, // RegisterDebugQueueRoutes register routes for dumping main queue // this method is also used in addon-operator func (op *ShellOperator) RegisterDebugQueueRoutes(dbgSrv *debug.Server) { - dbgSrv.Route("/queue/main.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) { - return dump.TaskQueueMainToText(op.TaskQueues), nil + dbgSrv.RegisterHandler(http.MethodGet, "/queue/main.{format:(json|yaml|text)}", func(req *http.Request) (interface{}, error) { + format := debug.FormatFromRequest(req) + return dump.TaskMainQueue(op.TaskQueues, format), nil }) - dbgSrv.Route("/queue/list.{format:(json|yaml|text)}", func(req *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodGet, "/queue/list.{format:(json|yaml|text)}", func(req *http.Request) (interface{}, error) { showEmptyStr := req.URL.Query().Get("showEmpty") showEmpty, err := strconv.ParseBool(showEmptyStr) if err != nil { @@ -47,11 +48,11 @@ func (op *ShellOperator) RegisterDebugQueueRoutes(dbgSrv *debug.Server) { // RegisterDebugHookRoutes register routes for dumping queues func (op *ShellOperator) RegisterDebugHookRoutes(dbgSrv *debug.Server) { - dbgSrv.Route("/hook/list.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodGet, "/hook/list.{format:(json|yaml|text)}", func(_ *http.Request) (interface{}, error) { return op.HookManager.GetHookNames(), nil }) - dbgSrv.Route("/hook/{name}/snapshots.{format:(json|yaml|text)}", func(r *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodGet, "/hook/{name}/snapshots.{format:(json|yaml|text)}", func(r *http.Request) (interface{}, error) { hookName := chi.URLParam(r, "name") h := op.HookManager.GetHook(hookName) return h.HookController.SnapshotsDump(), nil @@ -61,7 +62,7 @@ func (op *ShellOperator) RegisterDebugHookRoutes(dbgSrv *debug.Server) { // RegisterDebugConfigRoutes registers routes to manage runtime configuration. // This method is also used in addon-operator func (op *ShellOperator) RegisterDebugConfigRoutes(dbgSrv *debug.Server, runtimeConfig *config.Config) { - dbgSrv.Route("/config/list.{format:(json|yaml|text)}", func(r *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodGet, "/config/list.{format:(json|yaml|text)}", func(r *http.Request) (interface{}, error) { format := debug.FormatFromRequest(r) if format == "text" { return runtimeConfig.String(), nil @@ -69,31 +70,35 @@ func (op *ShellOperator) RegisterDebugConfigRoutes(dbgSrv *debug.Server, runtime return runtimeConfig.List(), nil }) - dbgSrv.RoutePOST("/config/set", func(r *http.Request) (interface{}, error) { + dbgSrv.RegisterHandler(http.MethodPost, "/config/set", func(r *http.Request) (interface{}, error) { + err := r.ParseForm() + if err != nil { + return nil, err + } + name := r.PostForm.Get("name") if name == "" { - return nil, fmt.Errorf("'name' parameter is required") + return nil, &debug.BadRequestError{Msg: "'name' parameter is required"} } if !runtimeConfig.Has(name) { - return nil, fmt.Errorf("unknown runtime parameter '%s'", name) + return nil, &debug.BadRequestError{Msg: fmt.Sprintf("unknown runtime parameter %q", name)} } value := r.PostForm.Get("value") if name == "" { - return nil, fmt.Errorf("'value' parameter is required") + return nil, &debug.BadRequestError{Msg: "'value' parameter is required"} } - if err := runtimeConfig.IsValid(name, value); err != nil { - return nil, fmt.Errorf("'value' parameter is invalid: %w", err) + if err = runtimeConfig.IsValid(name, value); err != nil { + return nil, &debug.BadRequestError{Msg: fmt.Sprintf("'value' parameter is invalid: %s", err)} } var duration time.Duration - var err error durationStr := r.PostForm.Get("duration") if durationStr != "" { duration, err = time.ParseDuration(durationStr) if err != nil { - return nil, fmt.Errorf("parse duration: %v", err) + return nil, &debug.BadRequestError{Msg: fmt.Sprintf("parse duration %q failed: %s", durationStr, err)} } } if duration == 0 { @@ -101,6 +106,7 @@ func (op *ShellOperator) RegisterDebugConfigRoutes(dbgSrv *debug.Server, runtime } else { runtimeConfig.SetTemporarily(name, value, duration) } + return nil, runtimeConfig.LastError(name) }) } diff --git a/pkg/task/dump/dump.go b/pkg/task/dump/dump.go index a3477ca1..5e20f6e4 100644 --- a/pkg/task/dump/dump.go +++ b/pkg/task/dump/dump.go @@ -29,18 +29,39 @@ func (a asQueueNames) Less(i, j int) bool { return p.Name < q.Name } -// TaskQueueMainToText dumps only the 'main' queue. -func TaskQueueMainToText(tqs *queue.TaskQueueSet) string { - var buf strings.Builder +// TaskMainQueue dumps 'main' queue with the given +func TaskMainQueue(tqs *queue.TaskQueueSet, format string) interface{} { + var dq dumpQueue q := tqs.GetMain() if q == nil { - buf.WriteString(fmt.Sprintf("Queue '%s' is not created\n", queue.MainQueueName)) + dq.Name = queue.MainQueueName + dq.Status = "Queue is not created" } else { - buf.WriteString(TaskQueueToText(q)) + tasks := getTasksForQueue(q) + dq = dumpQueue{ + Name: q.Name, + TasksCount: q.Length(), + Status: q.Status, + Tasks: tasks, + } } - return buf.String() + if format == "text" { + var buf strings.Builder + buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", dq.Name, dq.TasksCount, dq.Status)) + buf.WriteString("\n") + + for _, ts := range dq.Tasks { + buf.WriteString(fmt.Sprintf("%2d. ", ts.Index)) + buf.WriteString(ts.Description) + buf.WriteString("\n") + } + + return buf.String() + } + + return dq } // TaskQueues dumps all queues. @@ -135,23 +156,6 @@ func pluralize(n int, zero, one, many string) string { return fmt.Sprintf("%d %s", n, description) } -// TaskQueueToText dumps all tasks in queue. -func TaskQueueToText(q *queue.TaskQueue) string { - var buf strings.Builder - buf.WriteString(fmt.Sprintf("Queue '%s': length %d, status: '%s'\n", q.Name, q.Length(), q.Status)) - buf.WriteString("\n") - - index := 1 - q.Iterate(func(task task.Task) { - buf.WriteString(fmt.Sprintf("%2d. ", index)) - buf.WriteString(task.GetDescription()) - buf.WriteString("\n") - index++ - }) - - return buf.String() -} - func getTasksForQueue(q *queue.TaskQueue) []dumpTask { tasks := make([]dumpTask, 0, q.Length())