mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-27 04:13:11 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d9888f1a56 | |||
| 7dec532e18 | |||
| aa7695b3be | |||
| 1bacd31f27 | |||
| bfecbdf056 | |||
| 754108474c | |||
| 690c587c0a |
@@ -8,10 +8,13 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
actions: write
|
actions: write
|
||||||
|
pull-requests: write
|
||||||
|
security-events: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
autoupdate:
|
autoupdate:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: ">=1.21"
|
go-version: ">=1.24"
|
||||||
release-workflow: "release.yml"
|
release-workflow: "release.yml"
|
||||||
|
secrets: inherit
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
name: Pull Request
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "**"
|
||||||
|
- "!main"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
actions: write
|
||||||
|
pull-requests: write
|
||||||
|
security-events: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pr-checks:
|
||||||
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-pr.yaml@main
|
||||||
|
with:
|
||||||
|
go-version: ">=1.24"
|
||||||
@@ -17,6 +17,5 @@ jobs:
|
|||||||
release:
|
release:
|
||||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||||
with:
|
with:
|
||||||
go-version: "1.23"
|
go-version: ">=1.24"
|
||||||
secrets:
|
secrets: inherit
|
||||||
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
paths:
|
paths:
|
||||||
- 'docs/**'
|
- "docs/**"
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
@@ -39,7 +39,7 @@ jobs:
|
|||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
# Upload entire repository
|
# Upload entire repository
|
||||||
path: 'docs/'
|
path: "docs/"
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@v4
|
uses: actions/deploy-pages@v4
|
||||||
|
|||||||
+8
-6
@@ -62,10 +62,12 @@ homebrew_casks:
|
|||||||
homepage: https://lukaszraczylo.github.io/kportal
|
homepage: https://lukaszraczylo.github.io/kportal
|
||||||
description: "Modern Kubernetes port-forward manager with interactive TUI"
|
description: "Modern Kubernetes port-forward manager with interactive TUI"
|
||||||
license: MIT
|
license: MIT
|
||||||
|
url:
|
||||||
|
verified: github.com/lukaszraczylo/kportal
|
||||||
hooks:
|
hooks:
|
||||||
post.install: |
|
post:
|
||||||
if OS.mac?
|
install: |
|
||||||
system_command "/usr/bin/xattr",
|
if OS.mac?
|
||||||
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"],
|
system_command "/usr/bin/xattr",
|
||||||
sudo: false
|
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"]
|
||||||
end
|
end
|
||||||
|
|||||||
+2
-2
@@ -295,9 +295,9 @@ func main() {
|
|||||||
// Interactive mode with bubbletea
|
// Interactive mode with bubbletea
|
||||||
bubbleTeaUI = ui.NewBubbleTeaUI(func(id string, enable bool) {
|
bubbleTeaUI = ui.NewBubbleTeaUI(func(id string, enable bool) {
|
||||||
if enable {
|
if enable {
|
||||||
manager.EnableForward(id)
|
_ = manager.EnableForward(id)
|
||||||
} else {
|
} else {
|
||||||
manager.DisableForward(id)
|
_ = manager.DisableForward(id)
|
||||||
}
|
}
|
||||||
}, appVersion)
|
}, appVersion)
|
||||||
|
|
||||||
|
|||||||
@@ -296,6 +296,7 @@ func LoadConfig(path string) (*Config, error) {
|
|||||||
return nil, fmt.Errorf("config file too large: %d bytes (max %d)", fileInfo.Size(), maxConfigSize)
|
return nil, fmt.Errorf("config file too large: %d bytes (max %d)", fileInfo.Size(), maxConfigSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// #nosec G304 -- path is validated in main.go (no system dirs, absolute path)
|
||||||
data, err := os.ReadFile(path)
|
data, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
|||||||
@@ -264,8 +264,8 @@ func (m *Mutator) writeAtomic(cfg *Config) error {
|
|||||||
|
|
||||||
// Atomic rename
|
// Atomic rename
|
||||||
if err := os.Rename(tmpFile, m.configPath); err != nil {
|
if err := os.Rename(tmpFile, m.configPath); err != nil {
|
||||||
// Clean up temp file on failure
|
// Clean up temp file on failure - error ignored as we're already handling the rename error
|
||||||
os.Remove(tmpFile)
|
_ = os.Remove(tmpFile)
|
||||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
return fmt.Errorf("failed to rename temp file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func NewWatcher(configPath string, callback ReloadCallback, verbose bool) (*Watc
|
|||||||
|
|
||||||
absPath, err := filepath.Abs(configPath)
|
absPath, err := filepath.Abs(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
watcher.Close()
|
_ = watcher.Close()
|
||||||
return nil, fmt.Errorf("failed to resolve absolute path: %w", err)
|
return nil, fmt.Errorf("failed to resolve absolute path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ func NewWatcher(configPath string, callback ReloadCallback, verbose bool) (*Watc
|
|||||||
// (many editors delete and recreate files on save)
|
// (many editors delete and recreate files on save)
|
||||||
dir := filepath.Dir(absPath)
|
dir := filepath.Dir(absPath)
|
||||||
if err := watcher.Add(dir); err != nil {
|
if err := watcher.Add(dir); err != nil {
|
||||||
watcher.Close()
|
_ = watcher.Close()
|
||||||
return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err)
|
return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ 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.
|
||||||
func (w *Watcher) Stop() {
|
func (w *Watcher) Stop() {
|
||||||
close(w.done)
|
close(w.done)
|
||||||
w.watcher.Close()
|
_ = w.watcher.Close()
|
||||||
w.wg.Wait() // Wait for watch goroutine to exit
|
w.wg.Wait() // Wait for watch goroutine to exit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type KFTrayConfig struct {
|
|||||||
// ConvertKFTrayToKPortal converts kftray JSON configuration to kportal YAML format
|
// ConvertKFTrayToKPortal converts kftray JSON configuration to kportal YAML format
|
||||||
func ConvertKFTrayToKPortal(inputFile, outputFile string) error {
|
func ConvertKFTrayToKPortal(inputFile, outputFile string) error {
|
||||||
// Read kftray JSON config
|
// Read kftray JSON config
|
||||||
|
// #nosec G304 -- inputFile is from command line argument for explicit conversion
|
||||||
data, err := os.ReadFile(inputFile)
|
data, err := os.ReadFile(inputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read input file: %w", err)
|
return fmt.Errorf("failed to read input file: %w", err)
|
||||||
@@ -57,6 +58,7 @@ func ConvertKFTrayToKPortal(inputFile, outputFile string) error {
|
|||||||
|
|
||||||
// GetConversionSummary returns statistics about the kftray configuration
|
// GetConversionSummary returns statistics about the kftray configuration
|
||||||
func GetConversionSummary(inputFile string) (map[string]map[string]int, int, error) {
|
func GetConversionSummary(inputFile string) (map[string]map[string]int, int, error) {
|
||||||
|
// #nosec G304 -- inputFile is from command line argument for explicit conversion
|
||||||
data, err := os.ReadFile(inputFile)
|
data, err := os.ReadFile(inputFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fmt.Errorf("failed to read input file: %w", err)
|
return nil, 0, fmt.Errorf("failed to read input file: %w", err)
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ func getProcessNameByPID(pid string) string {
|
|||||||
|
|
||||||
// getProcessNameByPIDWindows retrieves the process name for a given PID on Windows
|
// getProcessNameByPIDWindows retrieves the process name for a given PID on Windows
|
||||||
func getProcessNameByPIDWindows(pid string) string {
|
func getProcessNameByPIDWindows(pid string) string {
|
||||||
|
// #nosec G204 -- pid is validated by isValidPID() to contain only digits
|
||||||
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %s", pid), "/FO", "CSV", "/NH")
|
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %s", pid), "/FO", "CSV", "/NH")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -145,7 +146,7 @@ func (pc *PortChecker) isPortAvailable(port int) bool {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
listener.Close()
|
_ = listener.Close()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +167,7 @@ func (pc *PortChecker) getProcessUsingPort(port int) string {
|
|||||||
func (pc *PortChecker) getProcessUsingPortUnix(port int) string {
|
func (pc *PortChecker) getProcessUsingPortUnix(port int) string {
|
||||||
// Use lsof to find the process
|
// Use lsof to find the process
|
||||||
// lsof -i :PORT -sTCP:LISTEN -t returns PIDs
|
// lsof -i :PORT -sTCP:LISTEN -t returns PIDs
|
||||||
|
// #nosec G204 -- port is an integer from config validation, not user input
|
||||||
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN", "-t")
|
cmd := exec.Command("lsof", "-i", fmt.Sprintf(":%d", port), "-sTCP:LISTEN", "-t")
|
||||||
output, err := cmd.Output()
|
output, err := cmd.Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ func (c *Checker) checkTCPDial(port int) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
conn.Close()
|
_ = conn.Close()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ func (c *Checker) checkDataTransfer(port int) error {
|
|||||||
|
|
||||||
// Set a short read deadline to detect hung connections
|
// 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
|
// We don't expect to receive data, but we want to verify the connection isn't hung
|
||||||
conn.SetReadDeadline(time.Now().Add(c.timeout))
|
_ = conn.SetReadDeadline(time.Now().Add(c.timeout))
|
||||||
|
|
||||||
// Try to read a small amount of data
|
// Try to read a small amount of data
|
||||||
// Most servers will either:
|
// Most servers will either:
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ func NewLogger(forwardID, logFile string, maxBodyLen int) (*Logger, error) {
|
|||||||
// Log entries are delivered via callbacks to the UI
|
// Log entries are delivered via callbacks to the UI
|
||||||
l.output = io.Discard
|
l.output = io.Discard
|
||||||
} else {
|
} else {
|
||||||
|
// #nosec G304 -- logFile is from config validation, not arbitrary user input
|
||||||
f, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
f, err := os.OpenFile(logFile, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0600)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -85,12 +85,13 @@ func (p *Proxy) Start() error {
|
|||||||
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
|
||||||
p.logError(r, err)
|
p.logError(r, err)
|
||||||
w.WriteHeader(http.StatusBadGateway)
|
w.WriteHeader(http.StatusBadGateway)
|
||||||
w.Write([]byte("Proxy error: " + err.Error()))
|
_, _ = w.Write([]byte("Proxy error: " + err.Error()))
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
p.server = &http.Server{
|
p.server = &http.Server{
|
||||||
Handler: proxy,
|
Handler: proxy,
|
||||||
|
ReadHeaderTimeout: 10 * time.Second,
|
||||||
}
|
}
|
||||||
|
|
||||||
p.running = true
|
p.running = true
|
||||||
@@ -122,8 +123,8 @@ func (p *Proxy) Stop() error {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := p.server.Shutdown(ctx); err != nil {
|
if err := p.server.Shutdown(ctx); err != nil {
|
||||||
// Force close
|
// Force close - error ignored as we're already shutting down
|
||||||
p.server.Close()
|
_ = p.server.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.logger.Close(); err != nil {
|
if err := p.logger.Close(); err != nil {
|
||||||
@@ -173,7 +174,7 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
|||||||
reqEntry.Headers = flattenHeaders(req.Header)
|
reqEntry.Headers = flattenHeaders(req.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.proxy.logger.Log(reqEntry)
|
_ = t.proxy.logger.Log(reqEntry)
|
||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
resp, err := t.transport.RoundTrip(req)
|
resp, err := t.transport.RoundTrip(req)
|
||||||
@@ -207,7 +208,7 @@ func (t *loggingTransport) RoundTrip(req *http.Request) (*http.Response, error)
|
|||||||
respEntry.Headers = flattenHeaders(resp.Header)
|
respEntry.Headers = flattenHeaders(resp.Header)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.proxy.logger.Log(respEntry)
|
_ = t.proxy.logger.Log(respEntry)
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -269,7 +270,7 @@ func (p *Proxy) logError(req *http.Request, err error) {
|
|||||||
Path: req.URL.Path,
|
Path: req.URL.Path,
|
||||||
Error: err.Error(),
|
Error: err.Error(),
|
||||||
}
|
}
|
||||||
p.logger.Log(entry)
|
_ = p.logger.Log(entry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// flattenHeaders converts http.Header to map[string]string
|
// flattenHeaders converts http.Header to map[string]string
|
||||||
|
|||||||
@@ -356,6 +356,6 @@ func CheckPortAvailability(port int) (bool, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Port is available, close the listener
|
// Port is available, close the listener
|
||||||
listener.Close()
|
_ = listener.Close()
|
||||||
return true, "", nil
|
return true, "", nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ type Backoff struct {
|
|||||||
func NewBackoff() *Backoff {
|
func NewBackoff() *Backoff {
|
||||||
return &Backoff{
|
return &Backoff{
|
||||||
attempt: 0,
|
attempt: 0,
|
||||||
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
// #nosec G404 -- math/rand is appropriate for backoff jitter; cryptographic randomness not needed
|
||||||
|
rng: rand.New(rand.NewSource(time.Now().UnixNano())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ func parseVersion(v string) []int {
|
|||||||
|
|
||||||
for _, p := range parts {
|
for _, p := range parts {
|
||||||
var num int
|
var num int
|
||||||
fmt.Sscanf(p, "%d", &num)
|
_, _ = fmt.Sscanf(p, "%d", &num)
|
||||||
result = append(result, num)
|
result = append(result, num)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user