diff --git a/.golangci.yml b/.golangci.yml index 287764e..9378b2a 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,29 +1,31 @@ # golangci-lint configuration # https://golangci-lint.run/usage/configuration/ +version: "2" run: timeout: 5m tests: true +formatters: + enable: + - gofmt + linters: enable: - errcheck - - gosimple - govet - ineffassign - staticcheck - unused - gosec - gocritic - - gofmt - -linters-settings: - govet: - enable: - - fieldalignment - gosec: - excludes: - - G304 # File path provided as taint input - handled with #nosec comments where needed - gocritic: - disabled-checks: - - ifElseChain # Complex conditionals are clearer as if-else than switch true + settings: + govet: + enable: + - fieldalignment + gosec: + excludes: + - G304 # File path provided as taint input - handled with #nosec comments where needed + gocritic: + disabled-checks: + - ifElseChain # Complex conditionals are clearer as if-else than switch true diff --git a/cmd/kportal/main.go b/cmd/kportal/main.go index db51371..4f2d8a7 100644 --- a/cmd/kportal/main.go +++ b/cmd/kportal/main.go @@ -347,10 +347,11 @@ func main() { } // Populate headers based on direction - if entry.Direction == "request" { + switch entry.Direction { + case "request": uiEntry.RequestHeaders = entry.Headers uiEntry.RequestBody = entry.Body - } else if entry.Direction == "response" { + case "response": uiEntry.ResponseHeaders = entry.Headers uiEntry.ResponseBody = entry.Body } diff --git a/internal/benchmark/runner.go b/internal/benchmark/runner.go index b71803c..7e83530 100644 --- a/internal/benchmark/runner.go +++ b/internal/benchmark/runner.go @@ -201,7 +201,7 @@ func (r *Runner) makeRequest(ctx context.Context, cfg Config) (statusCode int, b if err != nil { return 0, 0, bytesWritten, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Read response body to measure bytes respBody, err := io.ReadAll(resp.Body) diff --git a/internal/config/watcher_test.go b/internal/config/watcher_test.go index 384b458..d9299bc 100644 --- a/internal/config/watcher_test.go +++ b/internal/config/watcher_test.go @@ -423,7 +423,7 @@ func TestWatcher_HandleReload_LoadError(t *testing.T) { defer watcher.Stop() // Delete the config file to cause load error - os.Remove(configPath) + _ = os.Remove(configPath) // Call handleReload directly watcher.handleReload() diff --git a/internal/forward/portcheck_test.go b/internal/forward/portcheck_test.go index 12774e6..8056f5f 100644 --- a/internal/forward/portcheck_test.go +++ b/internal/forward/portcheck_test.go @@ -210,7 +210,7 @@ func TestPortChecker_CheckAvailability_ExcludeMap(t *testing.T) { // #nosec G102 -- test intentionally binds to all interfaces to match production port checking listener, err := net.Listen("tcp", ":0") assert.NoError(t, err, "should create listener") - defer listener.Close() + defer func() { _ = listener.Close() }() // Get the port that's now occupied addr := listener.Addr().(*net.TCPAddr) @@ -236,12 +236,12 @@ func TestPortChecker_CheckAvailability_MultipleSkipPorts(t *testing.T) { // #nosec G102 -- test intentionally binds to all interfaces to match production port checking listener1, err := net.Listen("tcp", ":0") assert.NoError(t, err) - defer listener1.Close() + defer func() { _ = listener1.Close() }() // #nosec G102 -- test intentionally binds to all interfaces to match production port checking listener2, err := net.Listen("tcp", ":0") assert.NoError(t, err) - defer listener2.Close() + defer func() { _ = listener2.Close() }() port1 := listener1.Addr().(*net.TCPAddr).Port port2 := listener2.Addr().(*net.TCPAddr).Port @@ -360,7 +360,7 @@ func TestPortChecker_PortAvailability_Integration(t *testing.T) { // #nosec G102 -- test intentionally binds to all interfaces to match production port checking listener, err := net.Listen("tcp", ":0") assert.NoError(t, err, "should create listener") - defer listener.Close() + defer func() { _ = listener.Close() }() // Get the occupied port occupiedPort := listener.Addr().(*net.TCPAddr).Port @@ -370,7 +370,7 @@ func TestPortChecker_PortAvailability_Integration(t *testing.T) { assert.False(t, available, "occupied port should not be available") // Close the listener - listener.Close() + _ = listener.Close() // The port should now be available (though there might be a brief delay) // We don't assert this to avoid flakiness in CI environments diff --git a/internal/healthcheck/checker.go b/internal/healthcheck/checker.go index 0e05f24..9e0ae4f 100644 --- a/internal/healthcheck/checker.go +++ b/internal/healthcheck/checker.go @@ -439,7 +439,7 @@ func (c *Checker) checkDataTransfer(port int) error { if err != nil { return err } - defer conn.Close() + defer func() { _ = conn.Close() }() // Set a short read deadline to detect hung connections // We don't expect to receive data, but we want to verify the connection isn't hung diff --git a/internal/healthcheck/checker_test.go b/internal/healthcheck/checker_test.go index ba273a8..8542a0f 100644 --- a/internal/healthcheck/checker_test.go +++ b/internal/healthcheck/checker_test.go @@ -46,7 +46,7 @@ func (s *HealthCheckTestSuite) TearDownTest() { s.checker.Stop() } if s.listener != nil { - s.listener.Close() + _ = s.listener.Close() } } @@ -198,17 +198,17 @@ func (s *HealthCheckTestSuite) TestDataTransferMethod() { case "banner": _, _ = conn.Write([]byte("220 Welcome\r\n")) time.Sleep(50 * time.Millisecond) - conn.Close() + _ = conn.Close() case "close": - conn.Close() + _ = conn.Close() case "silent": // Just keep connection open time.Sleep(200 * time.Millisecond) - conn.Close() + _ = conn.Close() } } }() - defer testListener.Close() + defer func() { _ = testListener.Close() }() } else { testPort = 54322 // Unused port } diff --git a/internal/httplog/logger_test.go b/internal/httplog/logger_test.go index 9ea2deb..630da9d 100644 --- a/internal/httplog/logger_test.go +++ b/internal/httplog/logger_test.go @@ -20,7 +20,7 @@ func TestNewLogger_OutputModes(t *testing.T) { t.Run("empty logFile uses io.Discard", func(t *testing.T) { l, err := NewLogger("test-forward", "", 1024) require.NoError(t, err) - defer l.Close() + defer func() { _ = l.Close() }() assert.Nil(t, l.file) assert.Equal(t, io.Discard, l.output) @@ -34,7 +34,7 @@ func TestNewLogger_OutputModes(t *testing.T) { l, err := NewLogger("test-forward", logFile, 2048) require.NoError(t, err) - defer l.Close() + defer func() { _ = l.Close() }() assert.NotNil(t, l.file) assert.NotEqual(t, io.Discard, l.output) @@ -58,7 +58,7 @@ func TestNewLogger_OutputModes(t *testing.T) { err = l.Log(Entry{Direction: "request"}) require.NoError(t, err) - l.Close() + _ = l.Close() // File should have both contents data, _ := os.ReadFile(logFile) diff --git a/internal/httplog/proxy_test.go b/internal/httplog/proxy_test.go index 3703414..55d9a4a 100644 --- a/internal/httplog/proxy_test.go +++ b/internal/httplog/proxy_test.go @@ -160,7 +160,7 @@ func TestNewLogger(t *testing.T) { require.NoError(t, err) require.NotNil(t, l) assert.Nil(t, l.file) // No file when using stdout - l.Close() + _ = l.Close() // Test file logger (using temp file) tmpFile := t.TempDir() + "/test.log" @@ -173,7 +173,7 @@ func TestNewLogger(t *testing.T) { err = l.Log(Entry{Direction: "request", Method: "GET"}) require.NoError(t, err) - l.Close() + _ = l.Close() // Verify file has content data, err := os.ReadFile(tmpFile) diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 6669725..84d8ab4 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -98,13 +98,13 @@ func (l *Logger) log(level Level, msg string, fields map[string]interface{}) { Fields: fields, } data, _ := json.Marshal(entry) - fmt.Fprintln(l.output, string(data)) + _, _ = fmt.Fprintln(l.output, string(data)) } else { // Text format if len(fields) > 0 { - fmt.Fprintf(l.output, "[%s] %s %v\n", levelStr, msg, fields) + _, _ = fmt.Fprintf(l.output, "[%s] %s %v\n", levelStr, msg, fields) } else { - fmt.Fprintf(l.output, "[%s] %s\n", levelStr, msg) + _, _ = fmt.Fprintf(l.output, "[%s] %s\n", levelStr, msg) } } } diff --git a/internal/ui/bubbletea_ui.go b/internal/ui/bubbletea_ui.go index 4097f59..e39d73d 100644 --- a/internal/ui/bubbletea_ui.go +++ b/internal/ui/bubbletea_ui.go @@ -564,6 +564,11 @@ func (m model) buildTableRows() [][]string { statusIcon, statusText := m.getStatusIconAndText(id, fwd) + localPortText := fmt.Sprintf("%d", fwd.LocalPort) + if fwd.Status == "Active" && !m.ui.isForwardDisabled(id) { + localPortText = hyperlink(fmt.Sprintf("http://127.0.0.1:%d", fwd.LocalPort), fmt.Sprintf("%d→", fwd.LocalPort)) + } + rows = append(rows, []string{ truncate(fwd.Context, ColumnWidthContext), truncate(fwd.Namespace, ColumnWidthNamespace), @@ -571,7 +576,7 @@ func (m model) buildTableRows() [][]string { truncate(fwd.Type, ColumnWidthType), truncate(fwd.Resource, ColumnWidthResource), fmt.Sprintf("%d", fwd.RemotePort), - fmt.Sprintf("%d", fwd.LocalPort), + localPortText, statusIcon + " " + statusText, }) } @@ -642,6 +647,7 @@ func (m model) createTableStyleFunc(colors mainViewColors) func(row, col int) li return baseStyle.Foreground(colors.errorColor) } } + } return baseStyle diff --git a/internal/ui/commands_test.go b/internal/ui/commands_test.go index a0b4435..fa24c9c 100644 --- a/internal/ui/commands_test.go +++ b/internal/ui/commands_test.go @@ -368,7 +368,7 @@ func TestHTTPLogEntry(t *testing.T) { func TestHTTPLogSubscriberType(t *testing.T) { // Test that our mock matches the type mock := NewMockHTTPLogSubscriber() - var subscriber HTTPLogSubscriber = mock.GetSubscriberFunc() + subscriber := mock.GetSubscriberFunc() // Test subscription callCount := 0 diff --git a/internal/ui/handlers_test.go b/internal/ui/handlers_test.go index a24abf1..43a5637 100644 --- a/internal/ui/handlers_test.go +++ b/internal/ui/handlers_test.go @@ -856,11 +856,12 @@ func TestModel_Update_ViewModeRouting(t *testing.T) { ui := NewBubbleTeaUI(nil, "1.0.0") ui.mu.Lock() ui.viewMode = tt.viewMode - if tt.viewMode == ViewModeAddWizard { + switch tt.viewMode { + case ViewModeAddWizard: ui.addWizard = newAddWizardState() - } else if tt.viewMode == ViewModeBenchmark { + case ViewModeBenchmark: ui.benchmarkState = newBenchmarkState("id", "alias", 8080) - } else if tt.viewMode == ViewModeHTTPLog { + case ViewModeHTTPLog: ui.httpLogState = newHTTPLogState("id", "alias") } ui.mu.Unlock() diff --git a/internal/ui/table.go b/internal/ui/table.go index 9a64ae7..0c389fb 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -187,6 +187,13 @@ func (t *TableUI) Remove(id string) { delete(t.forwards, id) } +// hyperlink wraps text in an OSC 8 terminal hyperlink escape sequence. +// Clicking the text opens the URL in terminals that support it (Ghostty, iTerm2, +// Windows Terminal, Kitty, WezTerm, etc.). Unsupported terminals show plain text. +func hyperlink(url, text string) string { + return fmt.Sprintf("\x1b]8;;%s\x1b\\%s\x1b]8;;\x1b\\", url, text) +} + // truncate truncates a string to maxLen, adding "..." if needed func truncate(s string, maxLen int) string { if len(s) <= maxLen { diff --git a/internal/ui/wizard_handlers.go b/internal/ui/wizard_handlers.go index 7ff8fb4..448e47c 100644 --- a/internal/ui/wizard_handlers.go +++ b/internal/ui/wizard_handlers.go @@ -661,12 +661,13 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) { Alias: wizard.alias, } - if wizard.selectedResourceType == ResourceTypePodPrefix { + switch wizard.selectedResourceType { + case ResourceTypePodPrefix: fwd.Resource = "pod/" + wizard.resourceValue - } else if wizard.selectedResourceType == ResourceTypePodSelector { + case ResourceTypePodSelector: fwd.Resource = wizard.resourceValue fwd.Selector = wizard.selector - } else if wizard.selectedResourceType == ResourceTypeService { + case ResourceTypeService: fwd.Resource = "service/" + wizard.resourceValue } diff --git a/internal/ui/wizard_views.go b/internal/ui/wizard_views.go index 2db93ef..ea9229d 100644 --- a/internal/ui/wizard_views.go +++ b/internal/ui/wizard_views.go @@ -1304,10 +1304,10 @@ func decompressContent(content string, headers map[string]string) string { if err != nil { return content // Return original on error } - defer reader.Close() + defer func() { _ = reader.Close() }() case "deflate": reader = flate.NewReader(bytes.NewReader(data)) - defer reader.Close() + defer func() { _ = reader.Close() }() default: // br (brotli), compress, zstd - not in stdlib, return original return content diff --git a/internal/version/checker.go b/internal/version/checker.go index 54d7878..0ac925f 100644 --- a/internal/version/checker.go +++ b/internal/version/checker.go @@ -101,7 +101,7 @@ func (c *Checker) fetchLatestRelease(ctx context.Context) (*ReleaseInfo, error) if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)