Skip to content

Commit

Permalink
debugserver
Browse files Browse the repository at this point in the history
  • Loading branch information
yalosev committed Oct 9, 2023
1 parent fb73147 commit ca00552
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 85 deletions.
105 changes: 60 additions & 45 deletions pkg/debug/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package debug
import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"os"
Expand All @@ -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"
Expand All @@ -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,
}
}

Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}
21 changes: 19 additions & 2 deletions pkg/hook/task_metadata/task_metadata_test.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
package task_metadata

import (
"fmt"
"strings"
"testing"

. "github.com/onsi/gomega"

. "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"
)

Expand Down Expand Up @@ -70,11 +71,27 @@ 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.")
g.Expect(queueDump).Should(ContainSubstring(":kubernetes:"), "Queue dump should show kubernetes binding.")
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()
}
36 changes: 21 additions & 15 deletions pkg/shell-operator/debug_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
})

Expand All @@ -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 {
Expand All @@ -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
Expand All @@ -61,46 +62,51 @@ 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
}
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 {
runtimeConfig.Set(name, value)
} else {
runtimeConfig.SetTemporarily(name, value, duration)
}

return nil, runtimeConfig.LastError(name)
})
}
Loading

0 comments on commit ca00552

Please sign in to comment.