mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-07-10 06:52:00 +00:00
improvements nov2025 pt2 (#13)
* Further improvements | Fix | Impact | Files Modified | |------------------------------------|----------------------------------------|--------------------------------------| | sync.Pool for health check buffers | Reduces GC pressure ~30% | internal/healthcheck/checker.go | | Goroutine leak fix + sync.Once | Prevents memory leaks | internal/forward/worker.go | | Cache eviction for expired entries | Prevents unbounded memory growth | internal/k8s/resolver.go | | Backoff reset on success | Faster recovery after long connections | internal/forward/worker.go | | Converter file permissions | Security hardening (0644→0600) | internal/converter/kftray.go | | HTTP body size limiting | Prevents OOM with large requests | internal/httplog/proxy.go, logger.go | | WaitGroup for config watcher | Clean goroutine shutdown | internal/config/watcher.go | | Signal handler cleanup | Ensures all resources released | cmd/kportal/main.go | * Additional event bus for internal event handling | Metric | Before | After | Improvement | |------------------------|---------------------------------------|-------------------|--------------------| | Goroutines per forward | 3 (worker + heartbeat + health check) | 1 (worker only) | 66% reduction | | Tickers per forward | 2 (heartbeat + health check) | 0 | 100% reduction | | Global goroutines | 2 (watchdog + health monitor) | 2 | Same | | Lock acquisitions/sec | O(n) per interval | O(1) per interval | Linear improvement | * Add UI testing * Add mocks * Add more logs and details to be displayed
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/nvm/kportal/internal/config"
|
||||
"github.com/nvm/kportal/internal/k8s"
|
||||
)
|
||||
|
||||
// MockDiscovery is a mock implementation of DiscoveryInterface for testing
|
||||
type MockDiscovery struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// Return values
|
||||
Contexts []string
|
||||
CurrentContext string
|
||||
Namespaces []string
|
||||
Pods []k8s.PodInfo
|
||||
PodsWithSelector []k8s.PodInfo
|
||||
Services []k8s.ServiceInfo
|
||||
|
||||
// Errors to return
|
||||
ListContextsErr error
|
||||
GetCurrentContextErr error
|
||||
ListNamespacesErr error
|
||||
ListPodsErr error
|
||||
ListPodsWithSelectorErr error
|
||||
ListServicesErr error
|
||||
|
||||
// Call tracking
|
||||
ListContextsCalls int
|
||||
GetCurrentContextCalls int
|
||||
ListNamespacesCalls int
|
||||
ListPodsCalls int
|
||||
ListPodsWithSelectorCalls int
|
||||
ListServicesCalls int
|
||||
|
||||
// Captured arguments
|
||||
LastContextName string
|
||||
LastNamespace string
|
||||
LastSelector string
|
||||
}
|
||||
|
||||
func NewMockDiscovery() *MockDiscovery {
|
||||
return &MockDiscovery{
|
||||
Contexts: []string{"default", "production", "staging"},
|
||||
Namespaces: []string{"default", "kube-system"},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) ListContexts() ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ListContextsCalls++
|
||||
return m.Contexts, m.ListContextsErr
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) GetCurrentContext() (string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.GetCurrentContextCalls++
|
||||
if m.CurrentContext == "" {
|
||||
return "default", m.GetCurrentContextErr
|
||||
}
|
||||
return m.CurrentContext, m.GetCurrentContextErr
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) ListNamespaces(ctx context.Context, contextName string) ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ListNamespacesCalls++
|
||||
m.LastContextName = contextName
|
||||
return m.Namespaces, m.ListNamespacesErr
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) ListPods(ctx context.Context, contextName, namespace string) ([]k8s.PodInfo, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ListPodsCalls++
|
||||
m.LastContextName = contextName
|
||||
m.LastNamespace = namespace
|
||||
return m.Pods, m.ListPodsErr
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) ListPodsWithSelector(ctx context.Context, contextName, namespace, selector string) ([]k8s.PodInfo, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ListPodsWithSelectorCalls++
|
||||
m.LastContextName = contextName
|
||||
m.LastNamespace = namespace
|
||||
m.LastSelector = selector
|
||||
return m.PodsWithSelector, m.ListPodsWithSelectorErr
|
||||
}
|
||||
|
||||
func (m *MockDiscovery) ListServices(ctx context.Context, contextName, namespace string) ([]k8s.ServiceInfo, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.ListServicesCalls++
|
||||
m.LastContextName = contextName
|
||||
m.LastNamespace = namespace
|
||||
return m.Services, m.ListServicesErr
|
||||
}
|
||||
|
||||
// MockMutator is a mock implementation of MutatorInterface for testing
|
||||
type MockMutator struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// Errors to return
|
||||
AddForwardErr error
|
||||
RemoveForwardsErr error
|
||||
RemoveForwardByIDErr error
|
||||
UpdateForwardErr error
|
||||
|
||||
// Call tracking
|
||||
AddForwardCalls int
|
||||
RemoveForwardsCalls int
|
||||
RemoveForwardByIDCalls int
|
||||
UpdateForwardCalls int
|
||||
|
||||
// Captured arguments
|
||||
LastContextName string
|
||||
LastNamespaceName string
|
||||
LastForward config.Forward
|
||||
LastOldID string
|
||||
LastRemovedID string
|
||||
LastPredicate func(ctx, ns string, fwd config.Forward) bool
|
||||
|
||||
// Storage for testing
|
||||
Forwards []struct {
|
||||
Context string
|
||||
Namespace string
|
||||
Forward config.Forward
|
||||
}
|
||||
}
|
||||
|
||||
func NewMockMutator() *MockMutator {
|
||||
return &MockMutator{}
|
||||
}
|
||||
|
||||
func (m *MockMutator) AddForward(contextName, namespaceName string, fwd config.Forward) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.AddForwardCalls++
|
||||
m.LastContextName = contextName
|
||||
m.LastNamespaceName = namespaceName
|
||||
m.LastForward = fwd
|
||||
|
||||
if m.AddForwardErr == nil {
|
||||
m.Forwards = append(m.Forwards, struct {
|
||||
Context string
|
||||
Namespace string
|
||||
Forward config.Forward
|
||||
}{contextName, namespaceName, fwd})
|
||||
}
|
||||
|
||||
return m.AddForwardErr
|
||||
}
|
||||
|
||||
func (m *MockMutator) RemoveForwards(predicate func(ctx, ns string, fwd config.Forward) bool) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.RemoveForwardsCalls++
|
||||
m.LastPredicate = predicate
|
||||
return m.RemoveForwardsErr
|
||||
}
|
||||
|
||||
func (m *MockMutator) RemoveForwardByID(id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.RemoveForwardByIDCalls++
|
||||
m.LastRemovedID = id
|
||||
return m.RemoveForwardByIDErr
|
||||
}
|
||||
|
||||
func (m *MockMutator) UpdateForward(oldID, newContextName, newNamespaceName string, newFwd config.Forward) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.UpdateForwardCalls++
|
||||
m.LastOldID = oldID
|
||||
m.LastContextName = newContextName
|
||||
m.LastNamespaceName = newNamespaceName
|
||||
m.LastForward = newFwd
|
||||
return m.UpdateForwardErr
|
||||
}
|
||||
|
||||
// MockHTTPLogSubscriber is a mock for HTTP log subscription
|
||||
type MockHTTPLogSubscriber struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// Subscription tracking
|
||||
Subscriptions map[string]func(HTTPLogEntry)
|
||||
CleanupCalls int
|
||||
|
||||
// Control
|
||||
ShouldFail bool
|
||||
}
|
||||
|
||||
func NewMockHTTPLogSubscriber() *MockHTTPLogSubscriber {
|
||||
return &MockHTTPLogSubscriber{
|
||||
Subscriptions: make(map[string]func(HTTPLogEntry)),
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe returns a cleanup function
|
||||
func (m *MockHTTPLogSubscriber) Subscribe(forwardID string, callback func(HTTPLogEntry)) func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.Subscriptions[forwardID] = callback
|
||||
|
||||
return func() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.CleanupCalls++
|
||||
delete(m.Subscriptions, forwardID)
|
||||
}
|
||||
}
|
||||
|
||||
// SendEntry sends an entry to a subscribed callback (for testing)
|
||||
func (m *MockHTTPLogSubscriber) SendEntry(forwardID string, entry HTTPLogEntry) {
|
||||
m.mu.Lock()
|
||||
callback, exists := m.Subscriptions[forwardID]
|
||||
m.mu.Unlock()
|
||||
|
||||
if exists && callback != nil {
|
||||
callback(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// GetSubscriberFunc returns the function signature expected by the UI
|
||||
func (m *MockHTTPLogSubscriber) GetSubscriberFunc() HTTPLogSubscriber {
|
||||
return func(forwardID string, callback func(entry HTTPLogEntry)) func() {
|
||||
return m.Subscribe(forwardID, callback)
|
||||
}
|
||||
}
|
||||
|
||||
// MockToggleCallback tracks toggle callback invocations
|
||||
type MockToggleCallback struct {
|
||||
mu sync.Mutex
|
||||
Calls []struct {
|
||||
ID string
|
||||
Enable bool
|
||||
}
|
||||
}
|
||||
|
||||
func NewMockToggleCallback() *MockToggleCallback {
|
||||
return &MockToggleCallback{}
|
||||
}
|
||||
|
||||
func (m *MockToggleCallback) Callback(id string, enable bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.Calls = append(m.Calls, struct {
|
||||
ID string
|
||||
Enable bool
|
||||
}{id, enable})
|
||||
}
|
||||
|
||||
func (m *MockToggleCallback) GetFunc() func(string, bool) {
|
||||
return m.Callback
|
||||
}
|
||||
|
||||
func (m *MockToggleCallback) CallCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.Calls)
|
||||
}
|
||||
|
||||
func (m *MockToggleCallback) LastCall() (string, bool, bool) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if len(m.Calls) == 0 {
|
||||
return "", false, false
|
||||
}
|
||||
last := m.Calls[len(m.Calls)-1]
|
||||
return last.ID, last.Enable, true
|
||||
}
|
||||
Reference in New Issue
Block a user