mirror of
https://github.com/lukaszraczylo/kportal.git
synced 2026-06-29 05:32:38 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 035b1cdd01 | |||
| 32e88efd9a | |||
| 6d8677026f | |||
| b7a32e4aab |
@@ -0,0 +1,2 @@
|
|||||||
|
github: [lukaszraczylo]
|
||||||
|
custom: [monzo.me/lukaszraczylo]
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
name: Autoupdate go.mod and go.sum
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
schedule:
|
||||||
|
- cron: "0 3 * * *"
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: ">=1.21"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# This job is responsible for preparation of the build
|
||||||
|
# environment variables.
|
||||||
|
prepare:
|
||||||
|
name: Preparing build context
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
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
|
||||||
|
token: ${{ secrets.GHCR_TOKEN }}
|
||||||
|
|
||||||
|
- 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
|
||||||
|
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"
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
# Release Infrastructure
|
|
||||||
|
|
||||||
Documentation for kportal's release automation and distribution.
|
|
||||||
|
|
||||||
## 🔄 CI/CD Pipeline
|
|
||||||
|
|
||||||
**File**: `.github/workflows/release.yml`
|
|
||||||
|
|
||||||
The pipeline builds multi-platform binaries, creates GitHub releases, and updates Homebrew on version tags.
|
|
||||||
|
|
||||||
### Trigger a Release
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag -a v0.2.0 -m "Release v0.2.0"
|
|
||||||
git push origin v0.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
The pipeline will:
|
|
||||||
1. Build binaries for all platforms
|
|
||||||
2. Create GitHub release with binaries and checksums
|
|
||||||
3. Update Homebrew tap formula
|
|
||||||
|
|
||||||
## 📦 Installation Methods
|
|
||||||
|
|
||||||
### Homebrew
|
|
||||||
|
|
||||||
**File**: `Formula/kportal.rb`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install lukaszraczylo/tap/kportal
|
|
||||||
```
|
|
||||||
|
|
||||||
Formula is automatically updated by CI/CD. Requires:
|
|
||||||
- Tap repository: `https://github.com/lukaszraczylo/brew-taps`
|
|
||||||
- Secret: `HOMEBREW_TAP_TOKEN` with `repo` scope
|
|
||||||
|
|
||||||
### Install Script
|
|
||||||
|
|
||||||
**File**: `install.sh`
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -fsSL https://raw.githubusercontent.com/lukaszraczylo/kportal/main/install.sh | bash
|
|
||||||
```
|
|
||||||
|
|
||||||
Auto-detects OS/architecture and installs to `/usr/local/bin`.
|
|
||||||
|
|
||||||
### Manual Download
|
|
||||||
|
|
||||||
Download from [releases page](https://github.com/lukaszraczylo/kportal/releases).
|
|
||||||
|
|
||||||
## Platform Support
|
|
||||||
|
|
||||||
| OS | Architecture | Format |
|
|
||||||
|----|--------------|--------|
|
|
||||||
| Linux | amd64, arm64 | tar.gz |
|
|
||||||
| macOS | amd64, arm64 | tar.gz |
|
|
||||||
| Windows | amd64, arm64 | zip |
|
|
||||||
|
|
||||||
## 🚀 Release Process
|
|
||||||
|
|
||||||
1. **Make changes and test**
|
|
||||||
```bash
|
|
||||||
make test && make all
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Update CHANGELOG.md**
|
|
||||||
|
|
||||||
3. **Tag and push**
|
|
||||||
```bash
|
|
||||||
git tag -a v0.2.0 -m "Release v0.2.0"
|
|
||||||
git push origin main
|
|
||||||
git push origin v0.2.0
|
|
||||||
```
|
|
||||||
|
|
||||||
## Version Bumping
|
|
||||||
|
|
||||||
Version determined by commit message keywords:
|
|
||||||
|
|
||||||
| Bump | Keywords |
|
|
||||||
|------|----------|
|
|
||||||
| Patch (0.0.X) | `fix`, `bugfix`, `docs`, `test`, `refactor` |
|
|
||||||
| Minor (0.X.0) | `feat`, `feature`, `add`, `enhance`, `update` |
|
|
||||||
| Major (X.0.0) | `breaking`, `major`, `BREAKING CHANGE` |
|
|
||||||
|
|
||||||
## Required Secrets
|
|
||||||
|
|
||||||
| Secret | Purpose |
|
|
||||||
|--------|---------|
|
|
||||||
| `GITHUB_TOKEN` | Provided by GitHub Actions |
|
|
||||||
| `HOMEBREW_TAP_TOKEN` | Personal access token with `repo` scope |
|
|
||||||
|
|
||||||
## ⚙️ Initial Setup
|
|
||||||
|
|
||||||
### 1. Enable GitHub Pages
|
|
||||||
|
|
||||||
Repository Settings → Pages → Source: main branch, /docs folder
|
|
||||||
|
|
||||||
### 2. Create Homebrew Tap
|
|
||||||
|
|
||||||
```bash
|
|
||||||
gh repo create lukaszraczylo/brew-taps --public
|
|
||||||
cd brew-taps
|
|
||||||
mkdir Formula
|
|
||||||
# Formula will be auto-updated by CI
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Add Token Secret
|
|
||||||
|
|
||||||
Repository Settings → Secrets → Actions → New secret:
|
|
||||||
- Name: `HOMEBREW_TAP_TOKEN`
|
|
||||||
- Value: Personal access token with `repo` scope
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Release workflow fails
|
|
||||||
- Check GitHub Actions logs
|
|
||||||
- Verify secrets are configured
|
|
||||||
- Ensure tag follows `v\d+.\d+.\d+` format
|
|
||||||
|
|
||||||
### Homebrew not updating
|
|
||||||
- Verify `HOMEBREW_TAP_TOKEN` is valid
|
|
||||||
- Check tap repository permissions
|
|
||||||
|
|
||||||
### Install script fails
|
|
||||||
- Verify release binaries are attached
|
|
||||||
- Check binary naming matches script expectations
|
|
||||||
@@ -6,8 +6,9 @@ require (
|
|||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
|
github.com/go-logr/logr v1.4.3
|
||||||
|
github.com/grandcat/zeroconf v1.0.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/term v0.37.0
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
k8s.io/api v0.34.2
|
k8s.io/api v0.34.2
|
||||||
k8s.io/apimachinery v0.34.2
|
k8s.io/apimachinery v0.34.2
|
||||||
@@ -26,11 +27,9 @@ require (
|
|||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
|
||||||
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
github.com/go-openapi/jsonpointer v0.22.3 // indirect
|
||||||
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
github.com/go-openapi/jsonreference v0.21.3 // indirect
|
||||||
github.com/go-openapi/swag v0.25.3 // indirect
|
github.com/go-openapi/swag v0.25.3 // indirect
|
||||||
@@ -49,13 +48,12 @@ require (
|
|||||||
github.com/google/gnostic-models v0.7.1 // indirect
|
github.com/google/gnostic-models v0.7.1 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
|
||||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||||
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.27 // indirect
|
github.com/miekg/dns v1.1.68 // 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
|
||||||
@@ -71,16 +69,19 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/crypto v0.44.0 // indirect
|
golang.org/x/mod v0.30.0 // indirect
|
||||||
golang.org/x/net v0.47.0 // indirect
|
golang.org/x/net v0.47.0 // indirect
|
||||||
golang.org/x/oauth2 v0.33.0 // indirect
|
golang.org/x/oauth2 v0.33.0 // indirect
|
||||||
|
golang.org/x/sync v0.18.0 // indirect
|
||||||
golang.org/x/sys v0.38.0 // indirect
|
golang.org/x/sys v0.38.0 // indirect
|
||||||
|
golang.org/x/term v0.37.0 // indirect
|
||||||
golang.org/x/text v0.31.0 // indirect
|
golang.org/x/text v0.31.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.14.0 // indirect
|
||||||
|
golang.org/x/tools v0.39.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.10 // indirect
|
google.golang.org/protobuf v1.36.10 // 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-20251121143641-b6aabc6c6745 // indirect
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||||
|
|||||||
@@ -25,8 +25,6 @@ github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsV
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
@@ -104,8 +102,9 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J
|
|||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
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 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM=
|
|
||||||
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.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||||
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=
|
||||||
@@ -128,6 +127,7 @@ github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM
|
|||||||
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||||
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
|
||||||
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
@@ -156,13 +156,13 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU=
|
|
||||||
golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc=
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||||
|
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
@@ -176,6 +176,8 @@ golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwE
|
|||||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||||
|
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -197,8 +199,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn
|
|||||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/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-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=
|
||||||
@@ -222,8 +224,8 @@ k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
|
|||||||
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
|
||||||
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
|
||||||
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
|
||||||
k8s.io/kube-openapi v0.0.0-20251121143641-b6aabc6c6745 h1:c3rI/4s8ibM4vV5UOIlbgkBpwkylI5I9YiPlOtf2g4Q=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
|
||||||
k8s.io/kube-openapi v0.0.0-20251121143641-b6aabc6c6745/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
|
||||||
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||||
|
|||||||
@@ -154,12 +154,21 @@ func (s *WatchdogTestSuite) TestMultipleWorkers() {
|
|||||||
s.watchdog.RegisterWorker("worker-3", makeCallback("worker-3"))
|
s.watchdog.RegisterWorker("worker-3", makeCallback("worker-3"))
|
||||||
|
|
||||||
// worker-1: Keep sending heartbeats (healthy)
|
// worker-1: Keep sending heartbeats (healthy)
|
||||||
|
// Use a done channel to ensure goroutine exits before test ends
|
||||||
ticker1 := time.NewTicker(50 * time.Millisecond)
|
ticker1 := time.NewTicker(50 * time.Millisecond)
|
||||||
defer ticker1.Stop()
|
done := make(chan struct{})
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
defer ticker1.Stop()
|
||||||
for i := 0; i < 10; i++ {
|
for i := 0; i < 10; i++ {
|
||||||
<-ticker1.C
|
select {
|
||||||
s.watchdog.Heartbeat("worker-1")
|
case <-ticker1.C:
|
||||||
|
s.watchdog.Heartbeat("worker-1")
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -172,6 +181,10 @@ func (s *WatchdogTestSuite) TestMultipleWorkers() {
|
|||||||
// Wait for hung workers to be detected
|
// Wait for hung workers to be detected
|
||||||
time.Sleep(600 * time.Millisecond)
|
time.Sleep(600 * time.Millisecond)
|
||||||
|
|
||||||
|
// Signal goroutine to stop and wait for it
|
||||||
|
close(done)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
// Check results
|
// Check results
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Discovery provides cluster introspection capabilities for the UI wizards.
|
// Discovery provides cluster introspection capabilities for the UI wizards.
|
||||||
@@ -41,9 +43,10 @@ type ContainerInfo struct {
|
|||||||
|
|
||||||
// PortInfo describes a port exposed by a container or service.
|
// PortInfo describes a port exposed by a container or service.
|
||||||
type PortInfo struct {
|
type PortInfo struct {
|
||||||
Name string
|
Name string
|
||||||
Port int32
|
Port int32
|
||||||
Protocol string
|
TargetPort int32 // For services: the actual pod port to forward to
|
||||||
|
Protocol string
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServiceInfo contains information about a service.
|
// ServiceInfo contains information about a service.
|
||||||
@@ -205,7 +208,60 @@ func (d *Discovery) ListPodsWithSelector(ctx context.Context, contextName, names
|
|||||||
return pods, nil
|
return pods, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// resolveTargetPort resolves a service's targetPort to an actual port number.
|
||||||
|
// If targetPort is numeric, it returns that number directly.
|
||||||
|
// If targetPort is a named port, it looks up the port number from the backing pods.
|
||||||
|
// Falls back to the service port if resolution fails.
|
||||||
|
func (d *Discovery) resolveTargetPort(ctx context.Context, client kubernetes.Interface, namespace string, svc *corev1.Service, port *corev1.ServicePort) int32 {
|
||||||
|
// If targetPort is not set, Kubernetes defaults to the service port
|
||||||
|
if port.TargetPort.Type == intstr.Int && port.TargetPort.IntVal == 0 {
|
||||||
|
return port.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// If targetPort is numeric, use it directly
|
||||||
|
if port.TargetPort.Type == intstr.Int {
|
||||||
|
return port.TargetPort.IntVal
|
||||||
|
}
|
||||||
|
|
||||||
|
// targetPort is a named port - need to look up from pods
|
||||||
|
namedPort := port.TargetPort.StrVal
|
||||||
|
if namedPort == "" {
|
||||||
|
return port.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get a backing pod to resolve the named port
|
||||||
|
if len(svc.Spec.Selector) == 0 {
|
||||||
|
// No selector, can't resolve - fall back to service port
|
||||||
|
return port.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
selector := metav1.FormatLabelSelector(&metav1.LabelSelector{MatchLabels: svc.Spec.Selector})
|
||||||
|
pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
|
||||||
|
LabelSelector: selector,
|
||||||
|
Limit: 1, // We only need one pod to resolve the port name
|
||||||
|
})
|
||||||
|
if err != nil || len(pods.Items) == 0 {
|
||||||
|
// Can't get pods - fall back to service port
|
||||||
|
return port.Port
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up the named port in the pod's containers
|
||||||
|
pod := &pods.Items[0]
|
||||||
|
for _, container := range pod.Spec.Containers {
|
||||||
|
for _, containerPort := range container.Ports {
|
||||||
|
if containerPort.Name == namedPort {
|
||||||
|
return containerPort.ContainerPort
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named port not found - fall back to service port
|
||||||
|
return port.Port
|
||||||
|
}
|
||||||
|
|
||||||
// ListServices returns all services in the given namespace.
|
// ListServices returns all services in the given namespace.
|
||||||
|
// For each service port, it resolves the targetPort to an actual port number
|
||||||
|
// by looking up the backing pods when the targetPort is a named port.
|
||||||
func (d *Discovery) ListServices(ctx context.Context, contextName, namespace string) ([]ServiceInfo, error) {
|
func (d *Discovery) ListServices(ctx context.Context, contextName, namespace string) ([]ServiceInfo, error) {
|
||||||
client, err := d.pool.GetClient(contextName)
|
client, err := d.pool.GetClient(contextName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -221,10 +277,13 @@ func (d *Discovery) ListServices(ctx context.Context, contextName, namespace str
|
|||||||
for _, svc := range svcList.Items {
|
for _, svc := range svcList.Items {
|
||||||
ports := make([]PortInfo, 0, len(svc.Spec.Ports))
|
ports := make([]PortInfo, 0, len(svc.Spec.Ports))
|
||||||
for _, port := range svc.Spec.Ports {
|
for _, port := range svc.Spec.Ports {
|
||||||
|
targetPort := d.resolveTargetPort(ctx, client, namespace, &svc, &port)
|
||||||
|
|
||||||
ports = append(ports, PortInfo{
|
ports = append(ports, PortInfo{
|
||||||
Name: port.Name,
|
Name: port.Name,
|
||||||
Port: port.Port,
|
Port: port.Port,
|
||||||
Protocol: string(port.Protocol),
|
TargetPort: targetPort,
|
||||||
|
Protocol: string(port.Protocol),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
package k8s
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/intstr"
|
||||||
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveTargetPort(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
servicePort corev1.ServicePort
|
||||||
|
service *corev1.Service
|
||||||
|
pods []corev1.Pod
|
||||||
|
expectedPort int32
|
||||||
|
description string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "numeric targetPort",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromInt(8000),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: nil, // No pods needed for numeric targetPort
|
||||||
|
expectedPort: 8000,
|
||||||
|
description: "should use numeric targetPort directly",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "named targetPort resolved from pod",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Name: "http",
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromString("http"),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: []corev1.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
Labels: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "main",
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{Name: "http", ContainerPort: 8000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedPort: 8000,
|
||||||
|
description: "should resolve named port from pod container",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "targetPort not set - defaults to service port",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromInt(0), // Not set
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: nil,
|
||||||
|
expectedPort: 80,
|
||||||
|
description: "should fall back to service port when targetPort is not set",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "named targetPort with no matching pod",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Name: "http",
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromString("http"),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: nil, // No pods available
|
||||||
|
expectedPort: 80,
|
||||||
|
description: "should fall back to service port when no pods found",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "service without selector",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Name: "http",
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromString("http"),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: nil, // No selector
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: nil,
|
||||||
|
expectedPort: 80,
|
||||||
|
description: "should fall back to service port when service has no selector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "named targetPort not found in pod containers",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Name: "http",
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: intstr.FromString("nonexistent"),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: []corev1.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
Labels: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "main",
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{Name: "http", ContainerPort: 8000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedPort: 80,
|
||||||
|
description: "should fall back to service port when named port not found in pod",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "multiple containers with named port in second container",
|
||||||
|
servicePort: corev1.ServicePort{
|
||||||
|
Name: "metrics",
|
||||||
|
Port: 9090,
|
||||||
|
TargetPort: intstr.FromString("metrics"),
|
||||||
|
},
|
||||||
|
service: &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-svc",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: corev1.ServiceSpec{
|
||||||
|
Selector: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pods: []corev1.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-pod",
|
||||||
|
Namespace: "default",
|
||||||
|
Labels: map[string]string{"app": "test"},
|
||||||
|
},
|
||||||
|
Spec: corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{
|
||||||
|
{
|
||||||
|
Name: "main",
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{Name: "http", ContainerPort: 8000},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "sidecar",
|
||||||
|
Ports: []corev1.ContainerPort{
|
||||||
|
{Name: "metrics", ContainerPort: 9100},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedPort: 9100,
|
||||||
|
description: "should find named port in any container",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Create fake client with pods
|
||||||
|
var objects []runtime.Object
|
||||||
|
for i := range tt.pods {
|
||||||
|
objects = append(objects, &tt.pods[i])
|
||||||
|
}
|
||||||
|
fakeClient := fake.NewSimpleClientset(objects...)
|
||||||
|
|
||||||
|
// Create discovery instance (we only need it to call resolveTargetPort)
|
||||||
|
d := &Discovery{}
|
||||||
|
|
||||||
|
// Call resolveTargetPort
|
||||||
|
result := d.resolveTargetPort(
|
||||||
|
context.Background(),
|
||||||
|
fakeClient,
|
||||||
|
"default",
|
||||||
|
tt.service,
|
||||||
|
&tt.servicePort,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert.Equal(t, tt.expectedPort, result, tt.description)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPortInfoTargetPort(t *testing.T) {
|
||||||
|
// Test that PortInfo correctly stores TargetPort
|
||||||
|
portInfo := PortInfo{
|
||||||
|
Name: "http",
|
||||||
|
Port: 80,
|
||||||
|
TargetPort: 8000,
|
||||||
|
Protocol: "TCP",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, int32(80), portInfo.Port)
|
||||||
|
assert.Equal(t, int32(8000), portInfo.TargetPort)
|
||||||
|
assert.Equal(t, "http", portInfo.Name)
|
||||||
|
assert.Equal(t, "TCP", portInfo.Protocol)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetUniquePorts(t *testing.T) {
|
||||||
|
// Test GetUniquePorts still works with the new PortInfo struct
|
||||||
|
pods := []PodInfo{
|
||||||
|
{
|
||||||
|
Name: "pod1",
|
||||||
|
Containers: []ContainerInfo{
|
||||||
|
{
|
||||||
|
Name: "main",
|
||||||
|
Ports: []PortInfo{
|
||||||
|
{Name: "http", Port: 8080},
|
||||||
|
{Name: "metrics", Port: 9090},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "pod2",
|
||||||
|
Containers: []ContainerInfo{
|
||||||
|
{
|
||||||
|
Name: "main",
|
||||||
|
Ports: []PortInfo{
|
||||||
|
{Name: "http", Port: 8080}, // Duplicate
|
||||||
|
{Name: "grpc", Port: 50051},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ports := GetUniquePorts(pods)
|
||||||
|
|
||||||
|
// Should have 3 unique ports
|
||||||
|
assert.Len(t, ports, 3)
|
||||||
|
|
||||||
|
// Should be sorted by port number
|
||||||
|
assert.Equal(t, int32(8080), ports[0].Port)
|
||||||
|
assert.Equal(t, int32(9090), ports[1].Port)
|
||||||
|
assert.Equal(t, int32(50051), ports[2].Port)
|
||||||
|
|
||||||
|
// Names should be preserved
|
||||||
|
assert.Equal(t, "http", ports[0].Name)
|
||||||
|
assert.Equal(t, "metrics", ports[1].Name)
|
||||||
|
assert.Equal(t, "grpc", ports[2].Name)
|
||||||
|
}
|
||||||
@@ -151,6 +151,13 @@ func (p *Publisher) Stop() {
|
|||||||
logger.Info("mDNS publisher stopped", nil)
|
logger.Info("mDNS publisher stopped", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// shutdownSettleTime is a small delay after zeroconf shutdown to allow internal
|
||||||
|
// goroutines to exit cleanly. This works around a race condition in the
|
||||||
|
// grandcat/zeroconf library where recv4() can access ipv4conn after shutdown()
|
||||||
|
// sets it to nil. See: https://github.com/grandcat/zeroconf/issues/95
|
||||||
|
// Note: 100ms is needed for CI environments where timing can be more variable.
|
||||||
|
const shutdownSettleTime = 100 * time.Millisecond
|
||||||
|
|
||||||
// shutdownWithTimeout attempts to shutdown a zeroconf server with a timeout.
|
// shutdownWithTimeout attempts to shutdown a zeroconf server with a timeout.
|
||||||
// If shutdown hangs, it logs a warning and returns anyway.
|
// If shutdown hangs, it logs a warning and returns anyway.
|
||||||
func shutdownWithTimeout(server *zeroconf.Server, forwardID string) {
|
func shutdownWithTimeout(server *zeroconf.Server, forwardID string) {
|
||||||
@@ -164,6 +171,10 @@ func shutdownWithTimeout(server *zeroconf.Server, forwardID string) {
|
|||||||
select {
|
select {
|
||||||
case <-done:
|
case <-done:
|
||||||
// Shutdown completed successfully
|
// Shutdown completed successfully
|
||||||
|
// Add a small settle time to let internal goroutines exit cleanly.
|
||||||
|
// This works around a race condition in zeroconf where recv4() can
|
||||||
|
// access ipv4conn after shutdown() sets it to nil.
|
||||||
|
time.Sleep(shutdownSettleTime)
|
||||||
case <-time.After(shutdownTimeout):
|
case <-time.After(shutdownTimeout):
|
||||||
logger.Warn("mDNS shutdown timed out, continuing anyway", map[string]interface{}{
|
logger.Warn("mDNS shutdown timed out, continuing anyway", map[string]interface{}{
|
||||||
"forward_id": forwardID,
|
"forward_id": forwardID,
|
||||||
|
|||||||
@@ -468,7 +468,14 @@ func (m model) handleAddWizardEnter() (tea.Model, tea.Cmd) {
|
|||||||
wizard.clearTextInput()
|
wizard.clearTextInput()
|
||||||
} else if wizard.cursor >= 0 && wizard.cursor < len(wizard.detectedPorts) {
|
} else if wizard.cursor >= 0 && wizard.cursor < len(wizard.detectedPorts) {
|
||||||
// Selected a detected port
|
// Selected a detected port
|
||||||
wizard.remotePort = int(wizard.detectedPorts[wizard.cursor].Port)
|
// For services, use TargetPort (actual pod port) if available
|
||||||
|
// For pods, TargetPort is 0, so use Port (container port)
|
||||||
|
selectedPort := wizard.detectedPorts[wizard.cursor]
|
||||||
|
if selectedPort.TargetPort > 0 {
|
||||||
|
wizard.remotePort = int(selectedPort.TargetPort)
|
||||||
|
} else {
|
||||||
|
wizard.remotePort = int(selectedPort.Port)
|
||||||
|
}
|
||||||
wizard.step = StepEnterLocalPort
|
wizard.step = StepEnterLocalPort
|
||||||
wizard.clearTextInput()
|
wizard.clearTextInput()
|
||||||
wizard.inputMode = InputModeText
|
wizard.inputMode = InputModeText
|
||||||
|
|||||||
@@ -349,9 +349,20 @@ func (m model) renderEnterRemotePort() string {
|
|||||||
// Render detected ports within viewport
|
// Render detected ports within viewport
|
||||||
for i := start; i < end && i < len(wizard.detectedPorts); i++ {
|
for i := start; i < end && i < len(wizard.detectedPorts); i++ {
|
||||||
port := wizard.detectedPorts[i]
|
port := wizard.detectedPorts[i]
|
||||||
portDesc := fmt.Sprintf("%d", port.Port)
|
// For services, show both service port and target port if they differ
|
||||||
if port.Name != "" {
|
var portDesc string
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||||
|
// Service with different target port: "80 → 8000 (http)"
|
||||||
|
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||||
|
if port.Name != "" {
|
||||||
|
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Pod port or service with same port
|
||||||
|
portDesc = fmt.Sprintf("%d", port.Port)
|
||||||
|
if port.Name != "" {
|
||||||
|
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prefix := " "
|
prefix := " "
|
||||||
@@ -390,9 +401,17 @@ func (m model) renderEnterRemotePort() string {
|
|||||||
if len(wizard.detectedPorts) > 0 {
|
if len(wizard.detectedPorts) > 0 {
|
||||||
b.WriteString(mutedStyle.Render("Detected ports:\n"))
|
b.WriteString(mutedStyle.Render("Detected ports:\n"))
|
||||||
for _, port := range wizard.detectedPorts {
|
for _, port := range wizard.detectedPorts {
|
||||||
portDesc := fmt.Sprintf("%d", port.Port)
|
var portDesc string
|
||||||
if port.Name != "" {
|
if port.TargetPort > 0 && port.TargetPort != port.Port {
|
||||||
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
portDesc = fmt.Sprintf("%d → %d", port.Port, port.TargetPort)
|
||||||
|
if port.Name != "" {
|
||||||
|
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
portDesc = fmt.Sprintf("%d", port.Port)
|
||||||
|
if port.Name != "" {
|
||||||
|
portDesc += fmt.Sprintf(" (%s)", port.Name)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", portDesc)))
|
b.WriteString(mutedStyle.Render(fmt.Sprintf(" • %s\n", portDesc)))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user