feat: Add reverse proxy functionality with enhanced routing capabilities
- Introduced IgnoreRequestPath option in proxy configuration to allow exact match routing. - Implemented proxy_pass directive in routing extension to handle backend requests. - Enhanced error handling for backend unavailability and timeouts. - Added integration tests for reverse proxy, including basic requests, exact match routes, regex routes, header forwarding, and query string preservation. - Created helper functions for setting up test servers and backends, along with assertion utilities for response validation. - Updated server initialization to support extension management and middleware chaining. - Improved logging for debugging purposes during request handling.
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
package extension
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/konduktor/konduktor/internal/logging"
|
||||
)
|
||||
|
||||
// Manager manages all loaded extensions
|
||||
type Manager struct {
|
||||
extensions []Extension
|
||||
registry map[string]ExtensionFactory
|
||||
logger *logging.Logger
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewManager creates a new extension manager
|
||||
func NewManager(logger *logging.Logger) *Manager {
|
||||
m := &Manager{
|
||||
extensions: make([]Extension, 0),
|
||||
registry: make(map[string]ExtensionFactory),
|
||||
logger: logger,
|
||||
}
|
||||
|
||||
// Register built-in extensions
|
||||
m.RegisterFactory("routing", NewRoutingExtension)
|
||||
m.RegisterFactory("security", NewSecurityExtension)
|
||||
m.RegisterFactory("caching", NewCachingExtension)
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
// RegisterFactory registers an extension factory
|
||||
func (m *Manager) RegisterFactory(name string, factory ExtensionFactory) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.registry[name] = factory
|
||||
}
|
||||
|
||||
// LoadExtension loads an extension by type and config
|
||||
func (m *Manager) LoadExtension(extType string, config map[string]interface{}) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
factory, ok := m.registry[extType]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown extension type: %s", extType)
|
||||
}
|
||||
|
||||
ext, err := factory(config, m.logger)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create extension %s: %w", extType, err)
|
||||
}
|
||||
|
||||
if err := ext.Initialize(); err != nil {
|
||||
return fmt.Errorf("failed to initialize extension %s: %w", extType, err)
|
||||
}
|
||||
|
||||
m.extensions = append(m.extensions, ext)
|
||||
|
||||
// Sort by priority (lower first)
|
||||
sort.Slice(m.extensions, func(i, j int) bool {
|
||||
return m.extensions[i].Priority() < m.extensions[j].Priority()
|
||||
})
|
||||
|
||||
m.logger.Info("Loaded extension", "type", extType, "name", ext.Name(), "priority", ext.Priority())
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddExtension adds a pre-created extension
|
||||
func (m *Manager) AddExtension(ext Extension) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if err := ext.Initialize(); err != nil {
|
||||
return fmt.Errorf("failed to initialize extension %s: %w", ext.Name(), err)
|
||||
}
|
||||
|
||||
m.extensions = append(m.extensions, ext)
|
||||
|
||||
// Sort by priority
|
||||
sort.Slice(m.extensions, func(i, j int) bool {
|
||||
return m.extensions[i].Priority() < m.extensions[j].Priority()
|
||||
})
|
||||
|
||||
m.logger.Info("Added extension", "name", ext.Name(), "priority", ext.Priority())
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessRequest runs all extensions' ProcessRequest in order
|
||||
// Returns true if any extension handled the request
|
||||
func (m *Manager) ProcessRequest(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
|
||||
m.mu.RLock()
|
||||
extensions := m.extensions
|
||||
m.mu.RUnlock()
|
||||
|
||||
for _, ext := range extensions {
|
||||
if !ext.Enabled() {
|
||||
continue
|
||||
}
|
||||
|
||||
handled, err := ext.ProcessRequest(ctx, w, r)
|
||||
if err != nil {
|
||||
m.logger.Error("Extension error", "extension", ext.Name(), "error", err)
|
||||
// Continue to next extension on error
|
||||
continue
|
||||
}
|
||||
|
||||
if handled {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// ProcessResponse runs all extensions' ProcessResponse in reverse order
|
||||
func (m *Manager) ProcessResponse(ctx context.Context, w http.ResponseWriter, r *http.Request) {
|
||||
m.mu.RLock()
|
||||
extensions := m.extensions
|
||||
m.mu.RUnlock()
|
||||
|
||||
// Process in reverse order for response
|
||||
for i := len(extensions) - 1; i >= 0; i-- {
|
||||
ext := extensions[i]
|
||||
if !ext.Enabled() {
|
||||
continue
|
||||
}
|
||||
|
||||
ext.ProcessResponse(ctx, w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup cleans up all extensions
|
||||
func (m *Manager) Cleanup() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
for _, ext := range m.extensions {
|
||||
if err := ext.Cleanup(); err != nil {
|
||||
m.logger.Error("Extension cleanup error", "extension", ext.Name(), "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
m.extensions = nil
|
||||
}
|
||||
|
||||
// GetExtension returns an extension by name
|
||||
func (m *Manager) GetExtension(name string) Extension {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, ext := range m.extensions {
|
||||
if ext.Name() == name {
|
||||
return ext
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extensions returns all loaded extensions
|
||||
func (m *Manager) Extensions() []Extension {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
result := make([]Extension, len(m.extensions))
|
||||
copy(result, m.extensions)
|
||||
return result
|
||||
}
|
||||
|
||||
// Handler returns an http.Handler that processes requests through all extensions
|
||||
func (m *Manager) Handler(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Create response wrapper to capture response for ProcessResponse
|
||||
wrapper := newResponseWrapper(w)
|
||||
|
||||
// Process request through extensions
|
||||
handled, err := m.ProcessRequest(ctx, wrapper, r)
|
||||
if err != nil {
|
||||
m.logger.Error("Error processing request", "error", err)
|
||||
}
|
||||
|
||||
if handled {
|
||||
// Extension handled the request, process response
|
||||
m.ProcessResponse(ctx, wrapper, r)
|
||||
return
|
||||
}
|
||||
|
||||
// No extension handled, pass to next handler
|
||||
next.ServeHTTP(wrapper, r)
|
||||
|
||||
// Process response
|
||||
m.ProcessResponse(ctx, wrapper, r)
|
||||
})
|
||||
}
|
||||
|
||||
// responseWrapper wraps http.ResponseWriter to allow response modification
|
||||
type responseWrapper struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
written bool
|
||||
}
|
||||
|
||||
func newResponseWrapper(w http.ResponseWriter) *responseWrapper {
|
||||
return &responseWrapper{
|
||||
ResponseWriter: w,
|
||||
statusCode: http.StatusOK,
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWrapper) WriteHeader(code int) {
|
||||
if !rw.written {
|
||||
rw.statusCode = code
|
||||
rw.ResponseWriter.WriteHeader(code)
|
||||
rw.written = true
|
||||
}
|
||||
}
|
||||
|
||||
func (rw *responseWrapper) Write(b []byte) (int, error) {
|
||||
if !rw.written {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return rw.ResponseWriter.Write(b)
|
||||
}
|
||||
|
||||
func (rw *responseWrapper) StatusCode() int {
|
||||
return rw.statusCode
|
||||
}
|
||||
Reference in New Issue
Block a user