mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-28 05:26:27 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7032bb5bee | |||
| 6cb4f91ece | |||
| 5d600043f0 | |||
| 9bb6fbc48d |
@@ -71,3 +71,21 @@ homebrew_casks:
|
|||||||
system_command "/usr/bin/xattr",
|
system_command "/usr/bin/xattr",
|
||||||
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"]
|
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
signs:
|
||||||
|
- cmd: cosign
|
||||||
|
env:
|
||||||
|
- COSIGN_PASSWORD={{ .Env.COSIGN_PASSWORD }}
|
||||||
|
certificate: "${artifact}.pem"
|
||||||
|
args:
|
||||||
|
- sign-blob
|
||||||
|
- "--key"
|
||||||
|
- "env://COSIGN_KEY"
|
||||||
|
- "--output-signature"
|
||||||
|
- "${signature}"
|
||||||
|
- "--output-certificate"
|
||||||
|
- "${certificate}"
|
||||||
|
- "${artifact}"
|
||||||
|
- "--yes"
|
||||||
|
artifacts: checksum
|
||||||
|
output: true
|
||||||
|
|||||||
@@ -309,16 +309,26 @@ func main() {
|
|||||||
bubbleTeaUI.SetHTTPLogSubscriber(func(forwardID string, callback func(entry ui.HTTPLogEntry)) func() {
|
bubbleTeaUI.SetHTTPLogSubscriber(func(forwardID string, callback func(entry ui.HTTPLogEntry)) func() {
|
||||||
worker := manager.GetWorker(forwardID)
|
worker := manager.GetWorker(forwardID)
|
||||||
if worker == nil {
|
if worker == nil {
|
||||||
|
logger.Debug("HTTP log subscription failed: worker not found", map[string]interface{}{
|
||||||
|
"forward_id": forwardID,
|
||||||
|
})
|
||||||
return func() {} // No-op cleanup
|
return func() {} // No-op cleanup
|
||||||
}
|
}
|
||||||
|
|
||||||
proxy := worker.GetHTTPProxy()
|
proxy := worker.GetHTTPProxy()
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
|
// This is expected for forwards without httpLog enabled - not an error
|
||||||
|
logger.Debug("HTTP log subscription skipped: proxy not enabled", map[string]interface{}{
|
||||||
|
"forward_id": forwardID,
|
||||||
|
})
|
||||||
return func() {} // HTTP logging not enabled for this forward
|
return func() {} // HTTP logging not enabled for this forward
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyLogger := proxy.GetLogger()
|
proxyLogger := proxy.GetLogger()
|
||||||
if proxyLogger == nil {
|
if proxyLogger == nil {
|
||||||
|
logger.Debug("HTTP log subscription failed: logger not available", map[string]interface{}{
|
||||||
|
"forward_id": forwardID,
|
||||||
|
})
|
||||||
return func() {}
|
return func() {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ require (
|
|||||||
require (
|
require (
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 // indirect
|
github.com/charmbracelet/x/ansi v0.11.3 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
|
||||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.6.1 // indirect
|
github.com/clipperhouse/displaywidth v0.6.1 // indirect
|
||||||
@@ -54,7 +54,7 @@ require (
|
|||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||||
github.com/miekg/dns v1.1.68 // indirect
|
github.com/miekg/dns v1.1.69 // indirect
|
||||||
github.com/moby/spdystream v0.5.0 // indirect
|
github.com/moby/spdystream v0.5.0 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||||
@@ -79,7 +79,7 @@ require (
|
|||||||
golang.org/x/text v0.32.0 // indirect
|
golang.org/x/text v0.32.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
golang.org/x/tools v0.40.0 // indirect
|
golang.org/x/tools v0.40.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||||
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
|
|||||||
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
||||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
|
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
|
||||||
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
|
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
|
||||||
@@ -108,8 +108,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
|
|||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
github.com/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=
|
||||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
github.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=
|
||||||
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
|
||||||
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
@@ -210,8 +210,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|||||||
@@ -27,16 +27,6 @@ type Config struct {
|
|||||||
ProgressCallback ProgressCallback // Optional callback for progress updates
|
ProgressCallback ProgressCallback // Optional callback for progress updates
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultConfig returns a default benchmark configuration
|
|
||||||
func DefaultConfig() Config {
|
|
||||||
return Config{
|
|
||||||
Method: "GET",
|
|
||||||
Concurrency: 10,
|
|
||||||
Requests: 100,
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runner executes HTTP benchmarks
|
// Runner executes HTTP benchmarks
|
||||||
type Runner struct {
|
type Runner struct {
|
||||||
client *http.Client
|
client *http.Client
|
||||||
|
|||||||
@@ -206,15 +206,6 @@ func TestRunnerWithBody(t *testing.T) {
|
|||||||
assert.Equal(t, int64(15), results.BytesWritten)
|
assert.Equal(t, int64(15), results.BytesWritten)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDefaultConfig(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
|
||||||
|
|
||||||
assert.Equal(t, "GET", cfg.Method)
|
|
||||||
assert.Equal(t, 10, cfg.Concurrency)
|
|
||||||
assert.Equal(t, 100, cfg.Requests)
|
|
||||||
assert.Equal(t, 30*time.Second, cfg.Timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnerWithProgressCallback(t *testing.T) {
|
func TestRunnerWithProgressCallback(t *testing.T) {
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
time.Sleep(10 * time.Millisecond) // Add small delay so progress ticker can fire
|
time.Sleep(10 * time.Millisecond) // Add small delay so progress ticker can fire
|
||||||
|
|||||||
@@ -22,11 +22,6 @@ type ValidationError struct {
|
|||||||
Context map[string]string // Additional context information
|
Context map[string]string // Additional context information
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error implements the error interface.
|
|
||||||
func (e ValidationError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validator validates configuration files.
|
// Validator validates configuration files.
|
||||||
type Validator struct{}
|
type Validator struct{}
|
||||||
|
|
||||||
|
|||||||
@@ -621,15 +621,6 @@ func TestFormatValidationErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidationError_Error(t *testing.T) {
|
|
||||||
err := ValidationError{
|
|
||||||
Field: "port",
|
|
||||||
Message: "Invalid port 0",
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, "Invalid port 0", err.Error(), "Error() should return the message")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidator_ValidateStructure(t *testing.T) {
|
func TestValidator_ValidateStructure(t *testing.T) {
|
||||||
validator := NewValidator()
|
validator := NewValidator()
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Watcher struct {
|
|||||||
done chan struct{}
|
done chan struct{}
|
||||||
verbose bool
|
verbose bool
|
||||||
wg sync.WaitGroup // Ensures watch goroutine exits before Stop returns
|
wg sync.WaitGroup // Ensures watch goroutine exits before Stop returns
|
||||||
|
stopOnce sync.Once // Ensures Stop is safe to call multiple times
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewWatcher creates a new file watcher for the given config file.
|
// NewWatcher creates a new file watcher for the given config file.
|
||||||
@@ -61,9 +62,12 @@ func (w *Watcher) Start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stop stops watching the configuration file and waits for the watch goroutine to exit.
|
// Stop stops watching the configuration file and waits for the watch goroutine to exit.
|
||||||
|
// Safe to call multiple times.
|
||||||
func (w *Watcher) Stop() {
|
func (w *Watcher) Stop() {
|
||||||
close(w.done)
|
w.stopOnce.Do(func() {
|
||||||
_ = w.watcher.Close()
|
close(w.done)
|
||||||
|
_ = w.watcher.Close()
|
||||||
|
})
|
||||||
w.wg.Wait() // Wait for watch goroutine to exit
|
w.wg.Wait() // Wait for watch goroutine to exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,15 +135,6 @@ func (b *Bus) Close() {
|
|||||||
|
|
||||||
// Helper functions for creating common events
|
// Helper functions for creating common events
|
||||||
|
|
||||||
// NewForwardEvent creates a forward-related event
|
|
||||||
func NewForwardEvent(eventType EventType, forwardID string, data map[string]interface{}) Event {
|
|
||||||
return Event{
|
|
||||||
Type: eventType,
|
|
||||||
ForwardID: forwardID,
|
|
||||||
Data: data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewHealthEvent creates a health status change event
|
// NewHealthEvent creates a health status change event
|
||||||
func NewHealthEvent(forwardID string, status string, errorMsg string) Event {
|
func NewHealthEvent(forwardID string, status string, errorMsg string) Event {
|
||||||
return Event{
|
return Event{
|
||||||
|
|||||||
@@ -149,16 +149,6 @@ func TestBus_ConcurrentAccess(t *testing.T) {
|
|||||||
assert.Equal(t, int64(100), atomic.LoadInt64(&count))
|
assert.Equal(t, int64(100), atomic.LoadInt64(&count))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewForwardEvent(t *testing.T) {
|
|
||||||
event := NewForwardEvent(EventForwardStarting, "test-id", map[string]interface{}{
|
|
||||||
"pod": "my-pod",
|
|
||||||
})
|
|
||||||
|
|
||||||
assert.Equal(t, EventForwardStarting, event.Type)
|
|
||||||
assert.Equal(t, "test-id", event.ForwardID)
|
|
||||||
assert.Equal(t, "my-pod", event.Data["pod"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewHealthEvent(t *testing.T) {
|
func TestNewHealthEvent(t *testing.T) {
|
||||||
event := NewHealthEvent("test-id", "Active", "")
|
event := NewHealthEvent("test-id", "Active", "")
|
||||||
|
|
||||||
|
|||||||
@@ -139,11 +139,6 @@ func (m *Manager) SetMDNSPublisher(publisher *mdns.Publisher) {
|
|||||||
m.mdnsPublisher = publisher
|
m.mdnsPublisher = publisher
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEventBus returns the event bus for subscribing to manager events
|
|
||||||
func (m *Manager) GetEventBus() *events.Bus {
|
|
||||||
return m.eventBus
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start initializes and starts all port-forwards from the configuration.
|
// Start initializes and starts all port-forwards from the configuration.
|
||||||
func (m *Manager) Start(cfg *config.Config) error {
|
func (m *Manager) Start(cfg *config.Config) error {
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
@@ -493,27 +488,6 @@ func (m *Manager) stopWorkerInternal(id string, removeFromUI bool) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetActiveForwards returns a list of all active forward IDs.
|
|
||||||
func (m *Manager) GetActiveForwards() []string {
|
|
||||||
m.workersMu.RLock()
|
|
||||||
defer m.workersMu.RUnlock()
|
|
||||||
|
|
||||||
ids := make([]string, 0, len(m.workers))
|
|
||||||
for id := range m.workers {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWorkerCount returns the number of active workers.
|
|
||||||
func (m *Manager) GetWorkerCount() int {
|
|
||||||
m.workersMu.RLock()
|
|
||||||
defer m.workersMu.RUnlock()
|
|
||||||
|
|
||||||
return len(m.workers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWorker returns a worker by ID, or nil if not found.
|
// GetWorker returns a worker by ID, or nil if not found.
|
||||||
func (m *Manager) GetWorker(id string) *ForwardWorker {
|
func (m *Manager) GetWorker(id string) *ForwardWorker {
|
||||||
m.workersMu.RLock()
|
m.workersMu.RLock()
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/nvm/kportal/internal/config"
|
"github.com/nvm/kportal/internal/config"
|
||||||
"github.com/nvm/kportal/internal/events"
|
"github.com/nvm/kportal/internal/events"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestNewManager tests manager creation
|
// TestNewManager tests manager creation
|
||||||
@@ -53,41 +52,6 @@ func TestManager_SetStatusUI(t *testing.T) {
|
|||||||
assert.Equal(t, mockUI, manager.statusUI)
|
assert.Equal(t, mockUI, manager.statusUI)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestManager_GetEventBus tests getting the event bus
|
|
||||||
func TestManager_GetEventBus(t *testing.T) {
|
|
||||||
manager, err := NewManager(false)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip("Skipping test - no kubeconfig available")
|
|
||||||
}
|
|
||||||
defer manager.Stop()
|
|
||||||
|
|
||||||
bus := manager.GetEventBus()
|
|
||||||
assert.NotNil(t, bus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestManager_GetWorkerCount tests worker count tracking
|
|
||||||
func TestManager_GetWorkerCount(t *testing.T) {
|
|
||||||
manager, err := NewManager(false)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip("Skipping test - no kubeconfig available")
|
|
||||||
}
|
|
||||||
defer manager.Stop()
|
|
||||||
|
|
||||||
assert.Equal(t, 0, manager.GetWorkerCount())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestManager_GetActiveForwards tests getting active forwards
|
|
||||||
func TestManager_GetActiveForwards(t *testing.T) {
|
|
||||||
manager, err := NewManager(false)
|
|
||||||
if err != nil {
|
|
||||||
t.Skip("Skipping test - no kubeconfig available")
|
|
||||||
}
|
|
||||||
defer manager.Stop()
|
|
||||||
|
|
||||||
forwards := manager.GetActiveForwards()
|
|
||||||
assert.Empty(t, forwards)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestManager_GetWorker tests getting a worker by ID
|
// TestManager_GetWorker tests getting a worker by ID
|
||||||
func TestManager_GetWorker(t *testing.T) {
|
func TestManager_GetWorker(t *testing.T) {
|
||||||
manager, err := NewManager(false)
|
manager, err := NewManager(false)
|
||||||
@@ -362,12 +326,8 @@ func TestManager_EventBusIntegration(t *testing.T) {
|
|||||||
// Event bus should be wired to health checker and watchdog
|
// Event bus should be wired to health checker and watchdog
|
||||||
assert.NotNil(t, manager.eventBus)
|
assert.NotNil(t, manager.eventBus)
|
||||||
|
|
||||||
// Get event bus
|
|
||||||
bus := manager.GetEventBus()
|
|
||||||
require.NotNil(t, bus)
|
|
||||||
|
|
||||||
// SubscribeAll should work (no return value in this API)
|
// SubscribeAll should work (no return value in this API)
|
||||||
bus.SubscribeAll(func(event events.Event) {
|
manager.eventBus.SubscribeAll(func(event events.Event) {
|
||||||
// Handler
|
// Handler
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,6 +336,11 @@ func (w *ForwardWorker) establishForward(podName string) error {
|
|||||||
// Start port forwarding in a goroutine
|
// Start port forwarding in a goroutine
|
||||||
errChan := make(chan error, 1)
|
errChan := make(chan error, 1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
errChan <- fmt.Errorf("port forward panicked: %v", r)
|
||||||
|
}
|
||||||
|
}()
|
||||||
errChan <- w.portForwarder.Forward(forwardCtx, req)
|
errChan <- w.portForwarder.Forward(forwardCtx, req)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -409,6 +414,14 @@ func (w *ForwardWorker) startHTTPProxy() error {
|
|||||||
// Calculate internal port for k8s tunnel
|
// Calculate internal port for k8s tunnel
|
||||||
targetPort := w.forward.LocalPort + httpLogPortOffset
|
targetPort := w.forward.LocalPort + httpLogPortOffset
|
||||||
|
|
||||||
|
// Validate that the target port is available before attempting to bind
|
||||||
|
portChecker := NewPortChecker()
|
||||||
|
if !portChecker.isPortAvailable(targetPort) {
|
||||||
|
usedBy := portChecker.getProcessUsingPort(targetPort)
|
||||||
|
return fmt.Errorf("HTTP proxy target port %d is already in use by %s (forward port %d + offset %d)",
|
||||||
|
targetPort, usedBy, w.forward.LocalPort, httpLogPortOffset)
|
||||||
|
}
|
||||||
|
|
||||||
proxy, err := httplog.NewProxy(&w.forward, targetPort)
|
proxy, err := httplog.NewProxy(&w.forward, targetPort)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create HTTP proxy: %w", err)
|
return fmt.Errorf("failed to create HTTP proxy: %w", err)
|
||||||
|
|||||||
@@ -365,7 +365,8 @@ func (c *Checker) checkPort(forwardID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update health status
|
// Update health status and capture eventBus while holding lock
|
||||||
|
var bus *events.Bus
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if health, exists := c.ports[forwardID]; exists {
|
if health, exists := c.ports[forwardID]; exists {
|
||||||
health.Status = newStatus
|
health.Status = newStatus
|
||||||
@@ -378,17 +379,15 @@ func (c *Checker) checkPort(forwardID string) {
|
|||||||
health.LastActivity = now
|
health.LastActivity = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Capture eventBus while we have the lock to avoid race condition
|
||||||
|
bus = c.eventBus
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
// Notify if status changed
|
// Notify if status changed
|
||||||
if oldStatus != newStatus {
|
if oldStatus != newStatus {
|
||||||
c.notifyStatusChange(forwardID, newStatus, errorMsg)
|
c.notifyStatusChange(forwardID, newStatus, errorMsg)
|
||||||
|
|
||||||
// Publish to event bus if available
|
// Publish to event bus if available (captured while holding lock above)
|
||||||
c.mu.RLock()
|
|
||||||
bus := c.eventBus
|
|
||||||
c.mu.RUnlock()
|
|
||||||
|
|
||||||
if bus != nil {
|
if bus != nil {
|
||||||
if newStatus == StatusStale {
|
if newStatus == StatusStale {
|
||||||
bus.Publish(events.NewStaleEvent(forwardID, errorMsg))
|
bus.Publish(events.NewStaleEvent(forwardID, errorMsg))
|
||||||
|
|||||||
@@ -195,29 +195,12 @@ func shutdownWithTimeout(server *zeroconf.Server, forwardID string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsEnabled returns whether mDNS publishing is enabled.
|
|
||||||
func (p *Publisher) IsEnabled() bool {
|
|
||||||
return p.enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDomain returns the mDNS domain being used (always "local" per RFC 6762).
|
|
||||||
func (p *Publisher) GetDomain() string {
|
|
||||||
return mdnsDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetHostname returns the full mDNS hostname for an alias.
|
// GetHostname returns the full mDNS hostname for an alias.
|
||||||
// Example: GetHostname("myapp") returns "myapp.local"
|
// Example: GetHostname("myapp") returns "myapp.local"
|
||||||
func GetHostname(alias string) string {
|
func GetHostname(alias string) string {
|
||||||
return alias + "." + mdnsDomain
|
return alias + "." + mdnsDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRegisteredCount returns the number of currently registered hostnames.
|
|
||||||
func (p *Publisher) GetRegisteredCount() int {
|
|
||||||
p.mu.RLock()
|
|
||||||
defer p.mu.RUnlock()
|
|
||||||
return len(p.servers)
|
|
||||||
}
|
|
||||||
|
|
||||||
// getLocalIPs returns the local IP addresses for logging purposes.
|
// getLocalIPs returns the local IP addresses for logging purposes.
|
||||||
func getLocalIPs() []string {
|
func getLocalIPs() []string {
|
||||||
var ips []string
|
var ips []string
|
||||||
|
|||||||
@@ -13,15 +13,17 @@ import (
|
|||||||
func TestNewPublisher_Disabled(t *testing.T) {
|
func TestNewPublisher_Disabled(t *testing.T) {
|
||||||
p := NewPublisher(false)
|
p := NewPublisher(false)
|
||||||
|
|
||||||
assert.False(t, p.IsEnabled())
|
// When disabled, Register should succeed but be a no-op
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
err := p.Register("forward-1", "test-alias", 8080)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewPublisher_Enabled(t *testing.T) {
|
func TestNewPublisher_Enabled(t *testing.T) {
|
||||||
p := NewPublisher(true)
|
p := NewPublisher(true)
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
assert.True(t, p.IsEnabled())
|
// Enabled publisher should be created successfully
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
assert.NotNil(t, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegister_WhenDisabled_NoOp(t *testing.T) {
|
func TestRegister_WhenDisabled_NoOp(t *testing.T) {
|
||||||
@@ -30,16 +32,17 @@ func TestRegister_WhenDisabled_NoOp(t *testing.T) {
|
|||||||
err := p.Register("forward-1", "test-alias", 8080)
|
err := p.Register("forward-1", "test-alias", 8080)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
// Unregister should also be safe when disabled
|
||||||
|
p.Unregister("forward-1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegister_EmptyAlias_NoOp(t *testing.T) {
|
func TestRegister_EmptyAlias_NoOp(t *testing.T) {
|
||||||
p := NewPublisher(true)
|
p := NewPublisher(true)
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
err := p.Register("forward-1", "", 8080)
|
err := p.Register("forward-1", "", 8080)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnregister_WhenDisabled_NoOp(t *testing.T) {
|
func TestUnregister_WhenDisabled_NoOp(t *testing.T) {
|
||||||
@@ -51,10 +54,10 @@ func TestUnregister_WhenDisabled_NoOp(t *testing.T) {
|
|||||||
|
|
||||||
func TestUnregister_NotRegistered_NoOp(t *testing.T) {
|
func TestUnregister_NotRegistered_NoOp(t *testing.T) {
|
||||||
p := NewPublisher(true)
|
p := NewPublisher(true)
|
||||||
|
defer p.Stop()
|
||||||
|
|
||||||
// Should not panic
|
// Should not panic
|
||||||
p.Unregister("non-existent")
|
p.Unregister("non-existent")
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStop_WhenDisabled_NoOp(t *testing.T) {
|
func TestStop_WhenDisabled_NoOp(t *testing.T) {
|
||||||
@@ -69,7 +72,6 @@ func TestStop_WhenNoRegistrations(t *testing.T) {
|
|||||||
|
|
||||||
// Should not panic
|
// Should not panic
|
||||||
p.Stop()
|
p.Stop()
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetLocalIPs(t *testing.T) {
|
func TestGetLocalIPs(t *testing.T) {
|
||||||
@@ -84,6 +86,11 @@ func TestGetLocalIPs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetHostname(t *testing.T) {
|
||||||
|
hostname := GetHostname("myapp")
|
||||||
|
assert.Equal(t, "myapp.local", hostname)
|
||||||
|
}
|
||||||
|
|
||||||
// Integration tests - only run when explicitly requested
|
// Integration tests - only run when explicitly requested
|
||||||
// These tests actually register mDNS services and require network access
|
// These tests actually register mDNS services and require network access
|
||||||
|
|
||||||
@@ -96,9 +103,10 @@ func TestRegister_Integration(t *testing.T) {
|
|||||||
defer p.Stop()
|
defer p.Stop()
|
||||||
|
|
||||||
err := p.Register("forward-1", "test-service", 8080)
|
err := p.Register("forward-1", "test-service", 8080)
|
||||||
|
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, p.GetRegisteredCount())
|
|
||||||
|
// Verify by checking that unregister doesn't panic
|
||||||
|
p.Unregister("forward-1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegister_Duplicate_Idempotent_Integration(t *testing.T) {
|
func TestRegister_Duplicate_Idempotent_Integration(t *testing.T) {
|
||||||
@@ -112,12 +120,10 @@ func TestRegister_Duplicate_Idempotent_Integration(t *testing.T) {
|
|||||||
// First registration
|
// First registration
|
||||||
err := p.Register("forward-1", "test-service", 8080)
|
err := p.Register("forward-1", "test-service", 8080)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, p.GetRegisteredCount())
|
|
||||||
|
|
||||||
// Second registration with same ID should be idempotent
|
// Second registration with same ID should be idempotent
|
||||||
err = p.Register("forward-1", "test-service", 8080)
|
err = p.Register("forward-1", "test-service", 8080)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Equal(t, 1, p.GetRegisteredCount())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegister_MultipleForwards_Integration(t *testing.T) {
|
func TestRegister_MultipleForwards_Integration(t *testing.T) {
|
||||||
@@ -135,7 +141,6 @@ func TestRegister_MultipleForwards_Integration(t *testing.T) {
|
|||||||
assert.NoError(t, err1)
|
assert.NoError(t, err1)
|
||||||
assert.NoError(t, err2)
|
assert.NoError(t, err2)
|
||||||
assert.NoError(t, err3)
|
assert.NoError(t, err3)
|
||||||
assert.Equal(t, 3, p.GetRegisteredCount())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUnregister_Success_Integration(t *testing.T) {
|
func TestUnregister_Success_Integration(t *testing.T) {
|
||||||
@@ -146,9 +151,13 @@ func TestUnregister_Success_Integration(t *testing.T) {
|
|||||||
p := NewPublisher(true)
|
p := NewPublisher(true)
|
||||||
defer p.Stop()
|
defer p.Stop()
|
||||||
|
|
||||||
p.Register("forward-1", "test-service", 8080)
|
err := p.Register("forward-1", "test-service", 8080)
|
||||||
assert.Equal(t, 1, p.GetRegisteredCount())
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Unregister should not panic and should handle it gracefully
|
||||||
p.Unregister("forward-1")
|
p.Unregister("forward-1")
|
||||||
assert.Equal(t, 0, p.GetRegisteredCount())
|
|
||||||
|
// Re-registering should work after unregister
|
||||||
|
err = p.Register("forward-1", "test-service-2", 8080)
|
||||||
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ const (
|
|||||||
initialDelay = 1 * time.Second
|
initialDelay = 1 * time.Second
|
||||||
maxDelay = 10 * time.Second
|
maxDelay = 10 * time.Second
|
||||||
jitterPct = 0.1 // 10% jitter
|
jitterPct = 0.1 // 10% jitter
|
||||||
|
// maxAttempt caps the exponent to prevent math.Pow overflow
|
||||||
|
// 2^30 seconds is ~34 years, well above maxDelay, so this is safe
|
||||||
|
maxAttempt = 30
|
||||||
)
|
)
|
||||||
|
|
||||||
// Backoff implements exponential backoff with jitter for retry logic.
|
// Backoff implements exponential backoff with jitter for retry logic.
|
||||||
@@ -33,8 +36,14 @@ func NewBackoff() *Backoff {
|
|||||||
// The duration follows exponential backoff: 1s → 2s → 4s → 8s → 10s (max).
|
// The duration follows exponential backoff: 1s → 2s → 4s → 8s → 10s (max).
|
||||||
// A 10% jitter is added to prevent thundering herd effects.
|
// A 10% jitter is added to prevent thundering herd effects.
|
||||||
func (b *Backoff) Next() time.Duration {
|
func (b *Backoff) Next() time.Duration {
|
||||||
|
// Cap attempt to prevent overflow in math.Pow
|
||||||
|
attempt := b.attempt
|
||||||
|
if attempt > maxAttempt {
|
||||||
|
attempt = maxAttempt
|
||||||
|
}
|
||||||
|
|
||||||
// Calculate base delay: 2^attempt seconds
|
// Calculate base delay: 2^attempt seconds
|
||||||
exp := math.Pow(2, float64(b.attempt))
|
exp := math.Pow(2, float64(attempt))
|
||||||
delay := time.Duration(exp) * time.Second
|
delay := time.Duration(exp) * time.Second
|
||||||
|
|
||||||
// Cap at max delay
|
// Cap at max delay
|
||||||
|
|||||||
@@ -150,9 +150,3 @@ func parseVersion(v string) []int {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
// FormatUpdateMessage formats a user-friendly update notification
|
|
||||||
func (u *UpdateInfo) FormatUpdateMessage() string {
|
|
||||||
return fmt.Sprintf("New version available: %s (current: %s) - %s",
|
|
||||||
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -75,16 +75,3 @@ func TestIsNewerVersion(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateInfo_FormatUpdateMessage(t *testing.T) {
|
|
||||||
info := &UpdateInfo{
|
|
||||||
CurrentVersion: "0.1.0",
|
|
||||||
LatestVersion: "0.2.0",
|
|
||||||
ReleaseURL: "https://github.com/nvm/kportal/releases/tag/v0.2.0",
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := info.FormatUpdateMessage()
|
|
||||||
assert.Contains(t, msg, "0.2.0")
|
|
||||||
assert.Contains(t, msg, "0.1.0")
|
|
||||||
assert.Contains(t, msg, "https://github.com/nvm/kportal/releases/tag/v0.2.0")
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user