Compare commits

...

14 Commits

20 changed files with 90 additions and 194 deletions
+8 -71
View File
@@ -5,79 +5,16 @@ on:
schedule: schedule:
- cron: "0 3 * * *" - cron: "0 3 * * *"
env:
GO_VERSION: ">=1.21"
permissions: permissions:
contents: write contents: write
actions: write actions: write
pull-requests: write
security-events: write
jobs: jobs:
# This job is responsible for preparation of the build autoupdate:
# environment variables. uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
prepare: with:
name: Preparing build context go-version: ">=1.24"
runs-on: ubuntu-latest release-workflow: "release.yml"
secrets: inherit
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Install Go
uses: actions/setup-go@v5
id: cache
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Go get dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: |
go get ./...
# This job is responsible for running tests and linting the codebase
test:
name: "Unit testing"
runs-on: ubuntu-latest
container: golang:1
needs: [prepare]
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Ensure full history is checked out
- name: Install Go
uses: actions/setup-go@v5
with:
go-version: ${{env.GO_VERSION}}
cache-dependency-path: "**/*.sum"
- name: Install dependencies
run: |
apt-get update
apt-get install ca-certificates make -y
update-ca-certificates
go mod tidy
go get -u -v ./...
go mod tidy -v
- name: Run unit tests
run: |
CI_RUN=${CI} make test
git config --global --add safe.directory /__w/kportal/kportal
- name: Commit changes
id: auto-commit
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Update go.mod and go.sum"
commit_options: "--no-verify --signoff"
file_pattern: "go.mod go.sum"
- name: Trigger release workflow
if: steps.auto-commit.outputs.changes_detected == 'true'
run: gh workflow run release.yml
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+22
View File
@@ -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"
+7 -80
View File
@@ -5,90 +5,17 @@ on:
branches: branches:
- main - main
paths: paths:
- '**.go' - "**.go"
- 'go.mod' - "go.mod"
- 'go.sum' - "go.sum"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
contents: write contents: write
jobs: jobs:
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Run tests with race detector
run: go test -race -v ./...
version:
name: Calculate Version
needs: test
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version_formatted.outputs.version }}
version_tag: ${{ steps.version_formatted.outputs.version_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Calculate semantic version
id: semver
uses: lukaszraczylo/semver-generator@v1
with:
config_file: semver.yaml
repository_local: true
- name: Format version
id: version_formatted
run: |
VERSION="${{ steps.semver.outputs.semantic_version }}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "version_tag=v${VERSION}" >> $GITHUB_OUTPUT
- name: Display version
run: |
echo "Calculated version: ${{ steps.version_formatted.outputs.version }}"
echo "Version tag: ${{ steps.version_formatted.outputs.version_tag }}"
release: release:
name: Release with GoReleaser uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
needs: version with:
runs-on: ubuntu-latest go-version: ">=1.24"
steps: secrets: inherit
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Create and push tag
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a ${{ needs.version.outputs.version_tag }} -m "Release ${{ needs.version.outputs.version_tag }}"
git push origin ${{ needs.version.outputs.version_tag }}
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --clean
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
+2 -2
View File
@@ -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
+13 -9
View File
@@ -23,11 +23,11 @@ builds:
archives: archives:
- id: kportal - id: kportal
format: tar.gz formats: [tar.gz]
name_template: "kportal-{{ .Version }}-{{ .Os }}-{{ .Arch }}" name_template: "kportal-{{ .Version }}-{{ .Os }}-{{ .Arch }}"
format_overrides: format_overrides:
- goos: windows - goos: windows
format: zip formats: [zip]
files: files:
- LICENSE - LICENSE
- README.md - README.md
@@ -53,17 +53,21 @@ release:
draft: false draft: false
prerelease: auto prerelease: auto
brews: homebrew_casks:
- repository: - repository:
owner: lukaszraczylo owner: lukaszraczylo
name: homebrew-taps name: homebrew-taps
token: "{{ .Env.HOMEBREW_TAP_TOKEN }}" token: "{{ .Env.HOMEBREW_TAP_TOKEN }}"
directory: Formula directory: 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
test: | url:
system "#{bin}/kportal", "--version" verified: github.com/lukaszraczylo/kportal
dependencies: hooks:
- name: kubernetes-cli post:
type: optional install: |
if OS.mac?
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"]
end
+7 -2
View File
@@ -54,12 +54,17 @@ kportal manages multiple Kubernetes port-forwards with an interactive terminal i
## 📦 Installation ## 📦 Installation
### Homebrew (macOS/Linux) ### Homebrew (macOS)
```bash ```bash
brew install lukaszraczylo/taps/kportal brew install --cask lukaszraczylo/taps/kportal
``` ```
> **Note**: If you previously installed via `brew install lukaszraczylo/taps/kportal` (formula), uninstall first:
> ```bash
> brew uninstall kportal
> ```
### Quick Install ### Quick Install
```bash ```bash
+2 -2
View File
@@ -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)
+3 -3
View File
@@ -556,11 +556,11 @@
<i class="fas fa-beer text-orange-500 dark:text-orange-400 text-2xl mr-3"></i> <i class="fas fa-beer text-orange-500 dark:text-orange-400 text-2xl mr-3"></i>
<div> <div>
<h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Homebrew</h3> <h3 class="text-xl font-semibold text-gray-900 dark:text-gray-100">Homebrew</h3>
<p class="text-gray-600 dark:text-gray-400 text-sm">macOS & Linux</p> <p class="text-gray-600 dark:text-gray-400 text-sm">macOS</p>
</div> </div>
</div> </div>
<div onclick="copyToClipboard('brew install lukaszraczylo/taps/kportal', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 dark:from-gray-950 dark:to-black text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-orange-500 transition-all duration-300"> <div onclick="copyToClipboard('brew install --cask lukaszraczylo/taps/kportal', this)" class="relative bg-gradient-to-br from-gray-900 to-gray-800 dark:from-gray-950 dark:to-black text-gray-100 p-4 rounded-lg text-sm cursor-pointer group overflow-x-auto border border-gray-700 hover:border-orange-500 transition-all duration-300">
<code class="block whitespace-nowrap font-mono">brew install lukaszraczylo/taps/kportal</code> <code class="block whitespace-nowrap font-mono">brew install --cask lukaszraczylo/taps/kportal</code>
<div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-orange-400 transition-colors duration-300"></i></div> <div class="absolute top-3 right-3"><i class="fas fa-copy text-gray-500 group-hover:text-orange-400 transition-colors duration-300"></i></div>
</div> </div>
</div> </div>
-7
View File
@@ -221,10 +221,3 @@ func (r *Runner) makeRequest(ctx context.Context, cfg Config) (statusCode int, b
return resp.StatusCode, int64(len(respBody)), bytesWritten, nil return resp.StatusCode, int64(len(respBody)), bytesWritten, nil
} }
// Progress represents the current progress of a benchmark run
type Progress struct {
Completed int
Total int
Elapsed time.Duration
}
+1
View File
@@ -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)
+2 -2
View File
@@ -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)
} }
+3 -3
View File
@@ -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
} }
+2
View File
@@ -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)
+3 -1
View File
@@ -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 {
+2 -2
View File
@@ -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:
+1
View File
@@ -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
+8 -7
View File
@@ -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
+1 -1
View File
@@ -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
} }
+2 -1
View File
@@ -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())),
} }
} }
+1 -1
View File
@@ -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)
} }