Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+148
View File
@@ -0,0 +1,148 @@
package handlers
import (
"context"
"fmt"
"io"
"maps"
"net"
"net/http"
"strings"
"github.com/qsfera/server/pkg/log"
"golang.org/x/sync/errgroup"
)
// check is a function that performs a check.
type checker func(ctx context.Context) error
// CheckHandlerConfiguration defines the configuration for the CheckHandler.
type CheckHandlerConfiguration struct {
checks map[string]checker
logger log.Logger
limit int
statusFailed int
statusSuccess int
}
// NewCheckHandlerConfiguration initializes a new CheckHandlerConfiguration.
func NewCheckHandlerConfiguration() CheckHandlerConfiguration {
return CheckHandlerConfiguration{
checks: make(map[string]checker),
limit: -1,
statusFailed: http.StatusInternalServerError,
statusSuccess: http.StatusOK,
}
}
// WithLogger sets the logger for the CheckHandlerConfiguration.
func (c CheckHandlerConfiguration) WithLogger(l log.Logger) CheckHandlerConfiguration {
c.logger = l
return c
}
// WithCheck sets a check for the CheckHandlerConfiguration.
func (c CheckHandlerConfiguration) WithCheck(name string, check checker) CheckHandlerConfiguration {
if _, ok := c.checks[name]; ok {
c.logger.Panic().Str("check", name).Msg("check already exists")
}
c.checks = maps.Clone(c.checks) // prevent propagated check duplication, maps are references;
c.checks[name] = check
return c
}
// WithLimit limits the number of active goroutines for the checks to at most n
func (c CheckHandlerConfiguration) WithLimit(n int) CheckHandlerConfiguration {
c.limit = n
return c
}
// WithStatusFailed sets the status code for the failed checks.
func (c CheckHandlerConfiguration) WithStatusFailed(status int) CheckHandlerConfiguration {
c.statusFailed = status
return c
}
// WithStatusSuccess sets the status code for the successful checks.
func (c CheckHandlerConfiguration) WithStatusSuccess(status int) CheckHandlerConfiguration {
c.statusSuccess = status
return c
}
// CheckHandler is a http Handler that performs different checks.
type CheckHandler struct {
conf CheckHandlerConfiguration
}
// NewCheckHandler initializes a new CheckHandler.
func NewCheckHandler(c CheckHandlerConfiguration) *CheckHandler {
c.checks = maps.Clone(c.checks) // prevent check duplication after initialization
return &CheckHandler{
conf: c,
}
}
func (h *CheckHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
g, ctx := errgroup.WithContext(r.Context())
g.SetLimit(h.conf.limit)
for name, check := range h.conf.checks {
checker := check
checkerName := name
g.Go(func() error { // https://go.dev/blog/loopvar-preview per iteration scope since go 1.22
if err := checker(ctx); err != nil { // since go 1.22 for loops have a per-iteration scope instead of per-loop scope, no need to pin the check...
return fmt.Errorf("'%s': %w", checkerName, err)
}
return nil
})
}
status := h.conf.statusSuccess
if err := g.Wait(); err != nil {
status = h.conf.statusFailed
h.conf.logger.Error().Err(err).Msg("check failed")
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(status)
if _, err := io.WriteString(w, http.StatusText(status)); err != nil { // io.WriteString should not fail, but if it does, we want to know.
h.conf.logger.Panic().Err(err).Msg("failed to write response")
}
}
// FailSaveAddress replaces wildcard addresses with the outbound IP.
func FailSaveAddress(address string) (string, error) {
if strings.Contains(address, "0.0.0.0") || strings.Contains(address, "::") {
outboundIp, err := getOutBoundIP()
if err != nil {
return "", err
}
address = strings.Replace(address, "0.0.0.0", outboundIp, 1)
address = strings.Replace(address, "::", "["+outboundIp+"]", 1)
address = strings.Replace(address, "[::]", "["+outboundIp+"]", 1)
}
return address, nil
}
// getOutBoundIP returns the outbound IP address.
func getOutBoundIP() (string, error) {
interfacesAddresses, err := net.InterfaceAddrs()
if err != nil {
return "", err
}
for _, address := range interfacesAddresses {
if ipNet, ok := address.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
if ipNet.IP.To4() != nil {
return ipNet.IP.String(), nil
}
}
}
return "", fmt.Errorf("no IP found")
}
+127
View File
@@ -0,0 +1,127 @@
package handlers_test
import (
"bytes"
"context"
"errors"
"net/http"
"net/http/httptest"
"slices"
"testing"
"github.com/test-go/testify/require"
"github.com/tidwall/gjson"
"github.com/qsfera/server/pkg/handlers"
"github.com/qsfera/server/pkg/log"
)
func TestCheckHandlerConfiguration(t *testing.T) {
nopCheckCounter := 0
nopCheck := func(_ context.Context) error { nopCheckCounter++; return nil }
handlerConfiguration := handlers.NewCheckHandlerConfiguration().WithCheck("check-1", nopCheck)
t.Run("add check", func(t *testing.T) {
localCounter := 0
handlers.NewCheckHandler(handlerConfiguration).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
require.Equal(t, 1, nopCheckCounter)
handlers.NewCheckHandler(handlerConfiguration.WithCheck("check-2", func(_ context.Context) error { localCounter++; return nil })).ServeHTTP(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil))
require.Equal(t, 2, nopCheckCounter)
require.Equal(t, 1, localCounter)
})
t.Run("checks are unique", func(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Error("checks should be unique")
}
}()
handlerConfiguration.WithCheck("check-1", nopCheck)
require.Equal(t, 3, nopCheckCounter)
})
}
func TestCheckHandler(t *testing.T) {
checkFactory := func(err error) func(ctx context.Context) error {
return func(ctx context.Context) error {
if err != nil {
return err
}
<-ctx.Done()
return nil
}
}
t.Run("passes with custom status", func(t *testing.T) {
rec := httptest.NewRecorder()
handler := handlers.NewCheckHandler(
handlers.
NewCheckHandlerConfiguration().
WithStatusSuccess(http.StatusCreated),
)
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
require.Equal(t, http.StatusCreated, rec.Code)
require.Equal(t, http.StatusText(http.StatusCreated), rec.Body.String())
})
t.Run("is not ok if any check fails", func(t *testing.T) {
rec := httptest.NewRecorder()
handler := handlers.NewCheckHandler(
handlers.
NewCheckHandlerConfiguration().
WithCheck("check-1", checkFactory(errors.New("failed"))),
)
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
require.Equal(t, http.StatusInternalServerError, rec.Code)
require.Equal(t, http.StatusText(http.StatusInternalServerError), rec.Body.String())
})
t.Run("fails with custom status", func(t *testing.T) {
rec := httptest.NewRecorder()
handler := handlers.NewCheckHandler(
handlers.
NewCheckHandlerConfiguration().
WithCheck("check-1", checkFactory(errors.New("failed"))).
WithStatusFailed(http.StatusTeapot),
)
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
require.Equal(t, http.StatusTeapot, rec.Code)
require.Equal(t, http.StatusText(http.StatusTeapot), rec.Body.String())
})
t.Run("exits all other running tests on failure", func(t *testing.T) {
var errs []error
rec := httptest.NewRecorder()
buffer := &bytes.Buffer{}
logger := log.Logger{Logger: log.NewLogger().Output(buffer)}
handler := handlers.NewCheckHandler(
handlers.
NewCheckHandlerConfiguration().
WithLogger(logger).
WithCheck("check-1", func(ctx context.Context) error {
err := checkFactory(nil)(ctx)
errs = append(errs, err)
return err
}).
WithCheck("check-2", func(ctx context.Context) error {
err := checkFactory(errors.New("failed"))(ctx)
errs = append(errs, err)
return err
}).
WithCheck("check-3", func(ctx context.Context) error {
err := checkFactory(nil)(ctx)
errs = append(errs, err)
return err
}),
)
handler.ServeHTTP(rec, httptest.NewRequest("GET", "/", nil))
require.Equal(t, "'check-2': failed", gjson.Get(buffer.String(), "error").String())
require.Equal(t, 1, len(slices.DeleteFunc(errs, func(err error) bool { return err == nil })))
require.Equal(t, 2, len(slices.DeleteFunc(errs, func(err error) bool { return err != nil })))
})
}