Multi-layer routing
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
8392503df7
commit
d6598f370c
37 changed files with 2834 additions and 37 deletions
|
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/containous/alice"
|
||||
|
|
@ -149,6 +151,12 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, entryPointName str
|
|||
continue
|
||||
}
|
||||
|
||||
// Only build handlers for root routers (routers without ParentRefs).
|
||||
// Routers with ParentRefs will be built as part of their parent router's muxer.
|
||||
if len(routerConfig.ParentRefs) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
handler, err := m.buildRouterHandler(ctxRouter, routerName, routerConfig)
|
||||
if err != nil {
|
||||
routerConfig.AddError(err, true)
|
||||
|
|
@ -215,33 +223,272 @@ func (m *Manager) buildHTTPHandler(ctx context.Context, router *runtime.RouterIn
|
|||
}
|
||||
router.Middlewares = qualifiedNames
|
||||
|
||||
if router.Service == "" {
|
||||
return nil, errors.New("the service is missing on the router")
|
||||
}
|
||||
|
||||
qualifiedService := provider.GetQualifiedName(ctx, router.Service)
|
||||
|
||||
chain := alice.New()
|
||||
|
||||
if router.DefaultRule {
|
||||
chain = chain.Append(denyrouterrecursion.WrapHandler(routerName))
|
||||
}
|
||||
|
||||
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
|
||||
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, qualifiedService))
|
||||
metricsHandler := metricsMiddle.RouterMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, qualifiedService)
|
||||
var (
|
||||
nextHandler http.Handler
|
||||
serviceName string
|
||||
err error
|
||||
)
|
||||
|
||||
// Check if this router has child routers or a service.
|
||||
switch {
|
||||
case len(router.ChildRefs) > 0:
|
||||
// This router routes to child routers - create a muxer for them
|
||||
nextHandler, err = m.buildChildRoutersMuxer(ctx, router.ChildRefs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("building child routers muxer: %w", err)
|
||||
}
|
||||
serviceName = fmt.Sprintf("%s-muxer", routerName)
|
||||
case router.Service != "":
|
||||
// This router routes to a service
|
||||
qualifiedService := provider.GetQualifiedName(ctx, router.Service)
|
||||
nextHandler, err = m.serviceManager.BuildHTTP(ctx, qualifiedService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
serviceName = qualifiedService
|
||||
default:
|
||||
return nil, errors.New("router must have either a service or child routers")
|
||||
}
|
||||
|
||||
// Access logs, metrics, and tracing middlewares are idempotent if the associated signal is disabled.
|
||||
chain = chain.Append(observability.WrapRouterHandler(ctx, routerName, router.Rule, serviceName))
|
||||
|
||||
metricsHandler := metricsMiddle.RouterMetricsHandler(ctx, m.observabilityMgr.MetricsRegistry(), routerName, serviceName)
|
||||
chain = chain.Append(observability.WrapMiddleware(ctx, metricsHandler))
|
||||
|
||||
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
||||
return accesslog.NewFieldHandler(next, accesslog.RouterName, routerName, nil), nil
|
||||
return accesslog.NewConcatFieldHandler(next, accesslog.RouterName, routerName), nil
|
||||
})
|
||||
|
||||
mHandler := m.middlewaresBuilder.BuildChain(ctx, router.Middlewares)
|
||||
|
||||
sHandler, err := m.serviceManager.BuildHTTP(ctx, qualifiedService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return chain.Extend(*mHandler).Then(nextHandler)
|
||||
}
|
||||
|
||||
// ParseRouterTree sets up router tree and validates router configuration.
|
||||
// This function performs the following operations in order:
|
||||
//
|
||||
// 1. Populate ChildRefs: Uses ParentRefs to build the parent-child relationship graph
|
||||
// 2. Root-first traversal: Starting from root routers (no ParentRefs), traverses the tree
|
||||
// 3. Cycle detection: Detects circular dependencies and removes cyclic links
|
||||
// 4. Reachability check: Marks routers unreachable from any root as disabled
|
||||
// 5. Dead-end detection: Marks routers with no service and no children as disabled
|
||||
// 6. Validation: Checks for configuration errors
|
||||
//
|
||||
// Router status is set during this process:
|
||||
// - Enabled: Reachable routers with valid configuration
|
||||
// - Disabled: Unreachable, dead-end, or routers with critical errors
|
||||
// - Warning: Routers with non-critical errors (like cycles)
|
||||
//
|
||||
// The function modifies router.Status, router.ChildRefs, and adds errors to router.Err.
|
||||
func (m *Manager) ParseRouterTree() {
|
||||
if m.conf == nil || m.conf.Routers == nil {
|
||||
return
|
||||
}
|
||||
|
||||
return chain.Extend(*mHandler).Then(sHandler)
|
||||
// Populate ChildRefs based on ParentRefs and find root routers.
|
||||
var rootRouters []string
|
||||
for routerName, router := range m.conf.Routers {
|
||||
if len(router.ParentRefs) == 0 {
|
||||
rootRouters = append(rootRouters, routerName)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, parentName := range router.ParentRefs {
|
||||
if parentRouter, exists := m.conf.Routers[parentName]; exists {
|
||||
// Add this router as a child of its parent
|
||||
if !slices.Contains(parentRouter.ChildRefs, routerName) {
|
||||
parentRouter.ChildRefs = append(parentRouter.ChildRefs, routerName)
|
||||
}
|
||||
} else {
|
||||
router.AddError(fmt.Errorf("parent router %q does not exist", parentName), true)
|
||||
}
|
||||
}
|
||||
|
||||
// Check for non-root router with TLS config.
|
||||
if router.TLS != nil {
|
||||
router.AddError(errors.New("non-root router cannot have TLS configuration"), true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for non-root router with Observability config.
|
||||
if router.Observability != nil {
|
||||
router.AddError(errors.New("non-root router cannot have Observability configuration"), true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for non-root router with Entrypoint config.
|
||||
if len(router.EntryPoints) > 0 {
|
||||
router.AddError(errors.New("non-root router cannot have Entrypoints configuration"), true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
sort.Strings(rootRouters)
|
||||
|
||||
// Root-first traversal with cycle detection.
|
||||
visited := make(map[string]bool)
|
||||
currentPath := make(map[string]bool)
|
||||
var path []string
|
||||
|
||||
for _, rootName := range rootRouters {
|
||||
if !visited[rootName] {
|
||||
m.traverse(rootName, visited, currentPath, path)
|
||||
}
|
||||
}
|
||||
|
||||
for routerName, router := range m.conf.Routers {
|
||||
// Set status for all routers based on reachability.
|
||||
if !visited[routerName] {
|
||||
router.AddError(errors.New("router is not reachable"), true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect dead-end routers (no service + no children) - AFTER cycle handling.
|
||||
if router.Service == "" && len(router.ChildRefs) == 0 {
|
||||
router.AddError(errors.New("router has no service and no child routers"), true)
|
||||
continue
|
||||
}
|
||||
|
||||
// Check for router with service that is referenced as a parent.
|
||||
if router.Service != "" && len(router.ChildRefs) > 0 {
|
||||
router.AddError(errors.New("router has both a service and is referenced as a parent by other routers"), true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// traverse performs a depth-first traversal starting from root routers,
|
||||
// detecting cycles and marking visited routers for reachability detection.
|
||||
func (m *Manager) traverse(routerName string, visited, currentPath map[string]bool, path []string) {
|
||||
if currentPath[routerName] {
|
||||
// Found a cycle - handle it properly.
|
||||
m.handleCycle(routerName, path)
|
||||
return
|
||||
}
|
||||
|
||||
if visited[routerName] {
|
||||
return
|
||||
}
|
||||
|
||||
router, exists := m.conf.Routers[routerName]
|
||||
// Since the ChildRefs population already guarantees router existence, this check is purely defensive.
|
||||
if !exists {
|
||||
visited[routerName] = true
|
||||
return
|
||||
}
|
||||
|
||||
visited[routerName] = true
|
||||
currentPath[routerName] = true
|
||||
newPath := append(path, routerName)
|
||||
|
||||
// Sort ChildRefs for deterministic behavior.
|
||||
sortedChildRefs := make([]string, len(router.ChildRefs))
|
||||
copy(sortedChildRefs, router.ChildRefs)
|
||||
sort.Strings(sortedChildRefs)
|
||||
|
||||
// Traverse children.
|
||||
for _, childName := range sortedChildRefs {
|
||||
m.traverse(childName, visited, currentPath, newPath)
|
||||
}
|
||||
|
||||
currentPath[routerName] = false
|
||||
}
|
||||
|
||||
// handleCycle handles cycle detection and removes the victim from guilty router's ChildRefs.
|
||||
func (m *Manager) handleCycle(victimRouter string, path []string) {
|
||||
// Find where the cycle starts in the path
|
||||
cycleStart := -1
|
||||
for i, name := range path {
|
||||
if name == victimRouter {
|
||||
cycleStart = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if cycleStart < 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// Build the cycle path: from cycle start to current + victim.
|
||||
cyclePath := append(path[cycleStart:], victimRouter)
|
||||
cycleRouters := strings.Join(cyclePath, " -> ")
|
||||
|
||||
// The guilty router is the last one in the path (the one creating the cycle).
|
||||
if len(path) > 0 {
|
||||
guiltyRouterName := path[len(path)-1]
|
||||
guiltyRouter, exists := m.conf.Routers[guiltyRouterName]
|
||||
if !exists {
|
||||
return
|
||||
}
|
||||
|
||||
// Add cycle error to guilty router.
|
||||
guiltyRouter.AddError(fmt.Errorf("cyclic reference detected in router tree: %s", cycleRouters), false)
|
||||
|
||||
// Remove victim from guilty router's ChildRefs.
|
||||
for i, childRef := range guiltyRouter.ChildRefs {
|
||||
if childRef == victimRouter {
|
||||
guiltyRouter.ChildRefs = append(guiltyRouter.ChildRefs[:i], guiltyRouter.ChildRefs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// buildChildRoutersMuxer creates a muxer for child routers.
|
||||
func (m *Manager) buildChildRoutersMuxer(ctx context.Context, childRefs []string) (http.Handler, error) {
|
||||
childMuxer := httpmuxer.NewMuxer(m.parser)
|
||||
|
||||
// Set a default handler for the child muxer (404 Not Found).
|
||||
childMuxer.SetDefaultHandler(http.NotFoundHandler())
|
||||
|
||||
childCount := 0
|
||||
for _, childName := range childRefs {
|
||||
childRouter, exists := m.conf.Routers[childName]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("child router %q does not exist", childName)
|
||||
}
|
||||
|
||||
// Skip child routers with errors.
|
||||
if len(childRouter.Err) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
logger := log.Ctx(ctx).With().Str(logs.RouterName, childName).Logger()
|
||||
ctxChild := logger.WithContext(provider.AddInContext(ctx, childName))
|
||||
|
||||
// Set priority if not set.
|
||||
if childRouter.Priority == 0 {
|
||||
childRouter.Priority = httpmuxer.GetRulePriority(childRouter.Rule)
|
||||
}
|
||||
|
||||
// Build the child router handler.
|
||||
childHandler, err := m.buildRouterHandler(ctxChild, childName, childRouter)
|
||||
if err != nil {
|
||||
childRouter.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
continue
|
||||
}
|
||||
|
||||
// Add the child router to the muxer.
|
||||
if err = childMuxer.AddRoute(childRouter.Rule, childRouter.RuleSyntax, childRouter.Priority, childHandler); err != nil {
|
||||
childRouter.AddError(err, true)
|
||||
logger.Error().Err(err).Send()
|
||||
continue
|
||||
}
|
||||
|
||||
childCount++
|
||||
}
|
||||
|
||||
// Prevent empty muxer.
|
||||
if childCount == 0 {
|
||||
return nil, fmt.Errorf("no child routers could be added to muxer (%d skipped)", len(childRefs))
|
||||
}
|
||||
|
||||
return childMuxer, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"io"
|
||||
"math"
|
||||
|
|
@ -11,6 +12,7 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/containous/alice"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
ptypes "github.com/traefik/paerser/types"
|
||||
|
|
@ -927,6 +929,940 @@ func BenchmarkService(b *testing.B) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestManager_ComputeMultiLayerRouting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers map[string]*dynamic.Router
|
||||
expectedStatuses map[string]string
|
||||
expectedChildRefs map[string][]string
|
||||
expectedErrors map[string][]string
|
||||
expectedErrorCounts map[string]int
|
||||
}{
|
||||
{
|
||||
desc: "Simple router",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {
|
||||
Service: "A-service",
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
// A->B1
|
||||
// ->B2
|
||||
desc: "Router with two children",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B1": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B1-service",
|
||||
},
|
||||
"B2": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B2-service",
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B1": runtime.StatusEnabled,
|
||||
"B2": runtime.StatusEnabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B1", "B2"},
|
||||
"B1": nil,
|
||||
"B2": nil,
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non-root router with TLS config",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B-service",
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": nil,
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"B": {"non-root router cannot have TLS configuration"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non-root router with observability config",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B-service",
|
||||
Observability: &dynamic.RouterObservabilityConfig{},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": nil,
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"B": {"non-root router cannot have Observability configuration"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Non-root router with EntryPoints config",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B-service",
|
||||
EntryPoints: []string{"web"},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": nil,
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"B": {"non-root router cannot have Entrypoints configuration"},
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
desc: "Router with non-existing parent",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"B": {
|
||||
ParentRefs: []string{"A"},
|
||||
Service: "B-service",
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"B": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"B": nil,
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"B": {"parent router \"A\" does not exist", "router is not reachable"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Dead-end router with no child and no service",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {},
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"A": {"router has no service and no child routers"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// A->B->A
|
||||
desc: "Router is not reachable",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {
|
||||
ParentRefs: []string{"B"},
|
||||
},
|
||||
"B": {
|
||||
ParentRefs: []string{"A"},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusDisabled,
|
||||
"B": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": {"A"},
|
||||
},
|
||||
// Cycle detection does not visit unreachable routers (it avoids computing the cycle dependency graph for unreachable routers).
|
||||
expectedErrors: map[string][]string{
|
||||
"A": {"router is not reachable"},
|
||||
"B": {"router is not reachable"},
|
||||
},
|
||||
},
|
||||
{
|
||||
// A->B->C->D->B
|
||||
desc: "Router creating a cycle is a dead-end and should be disabled",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B": {
|
||||
ParentRefs: []string{"A", "D"},
|
||||
},
|
||||
"C": {
|
||||
ParentRefs: []string{"B"},
|
||||
},
|
||||
"D": {
|
||||
ParentRefs: []string{"C"},
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B": runtime.StatusEnabled,
|
||||
"C": runtime.StatusEnabled,
|
||||
"D": runtime.StatusDisabled, // Dead-end router is disabled, because the cycle error broke the link with B.
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": {"C"},
|
||||
"C": {"D"},
|
||||
"D": {},
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"D": {
|
||||
"cyclic reference detected in router tree: B -> C -> D -> B",
|
||||
"router has no service and no child routers",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// A->B->C->D->B
|
||||
// ->E
|
||||
desc: "Router creating a cycle A->B->C->D->B but which is referenced elsewhere, must be set to warning status",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"A": {},
|
||||
"B": {
|
||||
ParentRefs: []string{"A", "D"},
|
||||
},
|
||||
"C": {
|
||||
ParentRefs: []string{"B"},
|
||||
},
|
||||
"D": {
|
||||
ParentRefs: []string{"C"},
|
||||
},
|
||||
"E": {
|
||||
ParentRefs: []string{"D"},
|
||||
Service: "E-service",
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"A": runtime.StatusEnabled,
|
||||
"B": runtime.StatusEnabled,
|
||||
"C": runtime.StatusEnabled,
|
||||
"D": runtime.StatusWarning,
|
||||
"E": runtime.StatusEnabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"A": {"B"},
|
||||
"B": {"C"},
|
||||
"C": {"D"},
|
||||
"D": {"E"},
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"D": {"cyclic reference detected in router tree: B -> C -> D -> B"},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Parent router with all children having errors",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"parent": {},
|
||||
"child-a": {
|
||||
ParentRefs: []string{"parent"},
|
||||
Service: "child-a-service",
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
|
||||
},
|
||||
"child-b": {
|
||||
ParentRefs: []string{"parent"},
|
||||
Service: "child-b-service",
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root cannot have TLS
|
||||
},
|
||||
},
|
||||
expectedStatuses: map[string]string{
|
||||
"parent": runtime.StatusEnabled, // Enabled during ParseRouterTree (no config errors). Would be disabled during handler building when empty muxer is detected.
|
||||
"child-a": runtime.StatusDisabled,
|
||||
"child-b": runtime.StatusDisabled,
|
||||
},
|
||||
expectedChildRefs: map[string][]string{
|
||||
"parent": {"child-a", "child-b"},
|
||||
"child-a": nil,
|
||||
"child-b": nil,
|
||||
},
|
||||
expectedErrors: map[string][]string{
|
||||
"child-a": {"non-root router cannot have TLS configuration"},
|
||||
"child-b": {"non-root router cannot have TLS configuration"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Create runtime routers
|
||||
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||
for name, router := range test.routers {
|
||||
runtimeRouters[name] = &runtime.RouterInfo{
|
||||
Router: router,
|
||||
Status: runtime.StatusEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
conf := &runtime.Configuration{
|
||||
Routers: runtimeRouters,
|
||||
}
|
||||
|
||||
manager := &Manager{
|
||||
conf: conf,
|
||||
}
|
||||
|
||||
// Execute the function we're testing
|
||||
manager.ParseRouterTree()
|
||||
|
||||
// Verify ChildRefs are populated correctly
|
||||
for routerName, expectedChildren := range test.expectedChildRefs {
|
||||
router := runtimeRouters[routerName]
|
||||
assert.ElementsMatch(t, expectedChildren, router.ChildRefs)
|
||||
}
|
||||
|
||||
// Verify statuses are set correctly
|
||||
var gotStatuses map[string]string
|
||||
for routerName, router := range runtimeRouters {
|
||||
if gotStatuses == nil {
|
||||
gotStatuses = make(map[string]string)
|
||||
}
|
||||
gotStatuses[routerName] = router.Status
|
||||
}
|
||||
assert.Equal(t, test.expectedStatuses, gotStatuses)
|
||||
|
||||
// Verify errors are added correctly
|
||||
var gotErrors map[string][]string
|
||||
for routerName, router := range runtimeRouters {
|
||||
for _, err := range router.Err {
|
||||
if gotErrors == nil {
|
||||
gotErrors = make(map[string][]string)
|
||||
}
|
||||
gotErrors[routerName] = append(gotErrors[routerName], err)
|
||||
}
|
||||
}
|
||||
assert.Equal(t, test.expectedErrors, gotErrors)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_buildChildRoutersMuxer(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
childRefs []string
|
||||
routers map[string]*dynamic.Router
|
||||
services map[string]*dynamic.Service
|
||||
middlewares map[string]*dynamic.Middleware
|
||||
expectedError string
|
||||
expectedRequests []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}
|
||||
}{
|
||||
{
|
||||
desc: "simple child router with service",
|
||||
childRefs: []string{"child1"},
|
||||
routers: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api`)",
|
||||
Service: "child1-service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/api", statusCode: http.StatusOK},
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple child routers with different rules",
|
||||
childRefs: []string{"child1", "child2"},
|
||||
routers: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api`)",
|
||||
Service: "child1-service",
|
||||
},
|
||||
"child2": {
|
||||
Rule: "Path(`/web`)",
|
||||
Service: "child2-service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"child2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/api", statusCode: http.StatusOK},
|
||||
{path: "/web", statusCode: http.StatusOK},
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "child router with middleware",
|
||||
childRefs: []string{"child1"},
|
||||
routers: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api`)",
|
||||
Service: "child1-service",
|
||||
Middlewares: []string{"test-middleware"},
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
middlewares: map[string]*dynamic.Middleware{
|
||||
"test-middleware": {
|
||||
Headers: &dynamic.Headers{
|
||||
CustomRequestHeaders: map[string]string{"X-Test": "value"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/api", statusCode: http.StatusOK},
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "nested child routers (child with its own children)",
|
||||
childRefs: []string{"intermediate"},
|
||||
routers: map[string]*dynamic.Router{
|
||||
"intermediate": {
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
// No service - this will have its own children
|
||||
},
|
||||
"leaf1": {
|
||||
Rule: "Path(`/api/v1`)",
|
||||
Service: "leaf1-service",
|
||||
ParentRefs: []string{"intermediate"},
|
||||
},
|
||||
"leaf2": {
|
||||
Rule: "Path(`/api/v2`)",
|
||||
Service: "leaf2-service",
|
||||
ParentRefs: []string{"intermediate"},
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"leaf1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"leaf2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/api/v1", statusCode: http.StatusOK},
|
||||
{path: "/api/v2", statusCode: http.StatusOK},
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "all child routers have errors - should return error",
|
||||
childRefs: []string{"child1", "child2"},
|
||||
routers: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api`)",
|
||||
Service: "child1-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
|
||||
},
|
||||
"child2": {
|
||||
Rule: "Path(`/web`)",
|
||||
Service: "child2-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid: non-root router cannot have TLS
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"child2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "no child routers could be added to muxer (2 skipped)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Create runtime routers
|
||||
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||
for name, router := range test.routers {
|
||||
runtimeRouters[name] = &runtime.RouterInfo{
|
||||
Router: router,
|
||||
}
|
||||
}
|
||||
|
||||
// Create runtime services
|
||||
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
||||
for name, service := range test.services {
|
||||
runtimeServices[name] = &runtime.ServiceInfo{
|
||||
Service: service,
|
||||
}
|
||||
}
|
||||
|
||||
// Create runtime middlewares
|
||||
runtimeMiddlewares := make(map[string]*runtime.MiddlewareInfo)
|
||||
for name, middleware := range test.middlewares {
|
||||
runtimeMiddlewares[name] = &runtime.MiddlewareInfo{
|
||||
Middleware: middleware,
|
||||
}
|
||||
}
|
||||
|
||||
conf := &runtime.Configuration{
|
||||
Routers: runtimeRouters,
|
||||
Services: runtimeServices,
|
||||
Middlewares: runtimeMiddlewares,
|
||||
}
|
||||
|
||||
// Set up the manager with mocks
|
||||
serviceManager := &mockServiceManager{}
|
||||
middlewareBuilder := &mockMiddlewareBuilder{}
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
|
||||
// Compute multi-layer routing to populate ChildRefs
|
||||
manager.ParseRouterTree()
|
||||
|
||||
// Build the child routers muxer
|
||||
ctx := t.Context()
|
||||
muxer, err := manager.buildChildRoutersMuxer(ctx, test.childRefs)
|
||||
|
||||
if test.expectedError != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(test.childRefs) == 0 {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, muxer)
|
||||
|
||||
// Test that the muxer routes requests correctly
|
||||
for _, req := range test.expectedRequests {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
||||
muxer.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_buildHTTPHandler_WithChildRouters(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
router *runtime.RouterInfo
|
||||
childRouters map[string]*dynamic.Router
|
||||
services map[string]*dynamic.Service
|
||||
expectedError string
|
||||
expectedRequests []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}
|
||||
}{
|
||||
{
|
||||
desc: "router with child routers",
|
||||
router: &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
ChildRefs: []string{"child1", "child2"},
|
||||
},
|
||||
childRouters: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api/v1`)",
|
||||
Service: "child1-service",
|
||||
},
|
||||
"child2": {
|
||||
Rule: "Path(`/api/v2`)",
|
||||
Service: "child2-service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"child2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "router with service (normal case)",
|
||||
router: &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
Service: "main-service",
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"main-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{},
|
||||
},
|
||||
{
|
||||
desc: "router with neither service nor child routers - error",
|
||||
router: &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
},
|
||||
expectedError: "router must have either a service or child routers",
|
||||
},
|
||||
{
|
||||
desc: "router with child routers but missing child - error",
|
||||
router: &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
ChildRefs: []string{"nonexistent"},
|
||||
},
|
||||
expectedError: "child router \"nonexistent\" does not exist",
|
||||
},
|
||||
{
|
||||
desc: "router with all children having errors - returns empty muxer error",
|
||||
router: &runtime.RouterInfo{
|
||||
Router: &dynamic.Router{
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
ChildRefs: []string{"child1", "child2"},
|
||||
},
|
||||
childRouters: map[string]*dynamic.Router{
|
||||
"child1": {
|
||||
Rule: "Path(`/api/v1`)",
|
||||
Service: "child1-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid for non-root
|
||||
},
|
||||
"child2": {
|
||||
Rule: "Path(`/api/v2`)",
|
||||
Service: "child2-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
TLS: &dynamic.RouterTLSConfig{}, // Invalid for non-root
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"child2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedError: "no child routers could be added to muxer (2 skipped)",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Create runtime routers
|
||||
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||
runtimeRouters["test-router"] = test.router
|
||||
for name, router := range test.childRouters {
|
||||
runtimeRouters[name] = &runtime.RouterInfo{
|
||||
Router: router,
|
||||
}
|
||||
}
|
||||
|
||||
// Create runtime services
|
||||
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
||||
for name, service := range test.services {
|
||||
runtimeServices[name] = &runtime.ServiceInfo{
|
||||
Service: service,
|
||||
}
|
||||
}
|
||||
|
||||
conf := &runtime.Configuration{
|
||||
Routers: runtimeRouters,
|
||||
Services: runtimeServices,
|
||||
}
|
||||
|
||||
// Set up the manager with mocks
|
||||
serviceManager := &mockServiceManager{}
|
||||
middlewareBuilder := &mockMiddlewareBuilder{}
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
|
||||
// Run ParseRouterTree to validate configuration and populate ChildRefs/errors
|
||||
manager.ParseRouterTree()
|
||||
|
||||
// Build the HTTP handler
|
||||
ctx := t.Context()
|
||||
handler, err := manager.buildHTTPHandler(ctx, test.router, "test-router")
|
||||
|
||||
if test.expectedError != "" {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), test.expectedError)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test that the handler routes requests correctly
|
||||
for _, req := range test.expectedRequests {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_BuildHandlers_WithChildRouters(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers map[string]*dynamic.Router
|
||||
services map[string]*dynamic.Service
|
||||
entryPoints []string
|
||||
expectedEntryPoint string
|
||||
expectedRequests []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}
|
||||
}{
|
||||
{
|
||||
desc: "parent router with child routers",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
"child1": {
|
||||
Rule: "Path(`/api/v1`)",
|
||||
Service: "child1-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
"child2": {
|
||||
Rule: "Path(`/api/v2`)",
|
||||
Service: "child2-service",
|
||||
ParentRefs: []string{"parent"},
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"child1-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"child2-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expectedEntryPoint: "web",
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{
|
||||
{path: "/unknown", statusCode: http.StatusNotFound},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "multiple parent routers with children",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"api-parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/api`)",
|
||||
},
|
||||
"web-parent": {
|
||||
EntryPoints: []string{"web"},
|
||||
Rule: "PathPrefix(`/web`)",
|
||||
},
|
||||
"api-child": {
|
||||
Rule: "Path(`/api/v1`)",
|
||||
Service: "api-service",
|
||||
ParentRefs: []string{"api-parent"},
|
||||
},
|
||||
"web-child": {
|
||||
Rule: "Path(`/web/index`)",
|
||||
Service: "web-service",
|
||||
ParentRefs: []string{"web-parent"},
|
||||
},
|
||||
},
|
||||
services: map[string]*dynamic.Service{
|
||||
"api-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8080"}},
|
||||
},
|
||||
},
|
||||
"web-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{{URL: "http://localhost:8081"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
entryPoints: []string{"web"},
|
||||
expectedEntryPoint: "web",
|
||||
expectedRequests: []struct {
|
||||
path string
|
||||
statusCode int
|
||||
}{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
// Create runtime routers
|
||||
runtimeRouters := make(map[string]*runtime.RouterInfo)
|
||||
for name, router := range test.routers {
|
||||
runtimeRouters[name] = &runtime.RouterInfo{
|
||||
Router: router,
|
||||
}
|
||||
}
|
||||
|
||||
// Create runtime services
|
||||
runtimeServices := make(map[string]*runtime.ServiceInfo)
|
||||
for name, service := range test.services {
|
||||
runtimeServices[name] = &runtime.ServiceInfo{
|
||||
Service: service,
|
||||
}
|
||||
}
|
||||
|
||||
conf := &runtime.Configuration{
|
||||
Routers: runtimeRouters,
|
||||
Services: runtimeServices,
|
||||
}
|
||||
|
||||
// Set up the manager with mocks
|
||||
serviceManager := &mockServiceManager{}
|
||||
middlewareBuilder := &mockMiddlewareBuilder{}
|
||||
parser, err := httpmuxer.NewSyntaxParser()
|
||||
require.NoError(t, err)
|
||||
|
||||
manager := NewManager(conf, serviceManager, middlewareBuilder, nil, nil, parser)
|
||||
|
||||
// Compute multi-layer routing to set up parent-child relationships
|
||||
manager.ParseRouterTree()
|
||||
|
||||
// Build handlers
|
||||
ctx := t.Context()
|
||||
handlers := manager.BuildHandlers(ctx, test.entryPoints, false)
|
||||
|
||||
require.Contains(t, handlers, test.expectedEntryPoint)
|
||||
handler := handlers[test.expectedEntryPoint]
|
||||
require.NotNil(t, handler)
|
||||
|
||||
// Test that the handler routes requests correctly
|
||||
for _, req := range test.expectedRequests {
|
||||
recorder := httptest.NewRecorder()
|
||||
request := httptest.NewRequest(http.MethodGet, req.path, nil)
|
||||
request.Host = "test.com"
|
||||
handler.ServeHTTP(recorder, request)
|
||||
|
||||
assert.Equal(t, req.statusCode, recorder.Code, "unexpected status code for path %s", req.path)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Mock implementations for testing
|
||||
|
||||
type mockServiceManager struct{}
|
||||
|
||||
func (m *mockServiceManager) BuildHTTP(_ context.Context, _ string) (http.Handler, error) {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("mock service response"))
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (m *mockServiceManager) LaunchHealthCheck(_ context.Context) {}
|
||||
|
||||
type mockMiddlewareBuilder struct{}
|
||||
|
||||
func (m *mockMiddlewareBuilder) BuildChain(_ context.Context, _ []string) *alice.Chain {
|
||||
chain := alice.New()
|
||||
return &chain
|
||||
}
|
||||
|
||||
type proxyBuilderMock struct{}
|
||||
|
||||
func (p proxyBuilderMock) Build(_ string, _ *url.URL, _, _ bool, _ time.Duration) (http.Handler, error) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue