Compare commits

...

26 Commits

Author SHA1 Message Date
lukaszraczylo 7ad96e3f72 Update go.mod and go.sum (#27) 2026-01-10 03:37:23 +00:00
lukaszraczylo ac7c855de5 Update go.mod and go.sum (#26) 2026-01-09 03:39:39 +00:00
lukaszraczylo 4074a7186c Update go.mod and go.sum (#25) 2026-01-07 03:39:19 +00:00
lukaszraczylo a5cc95a26e Update go.mod and go.sum (#24) 2025-12-23 03:39:14 +00:00
lukaszraczylo 0f977683cd Update go.mod and go.sum (#23) 2025-12-21 03:39:29 +00:00
lukaszraczylo dcebdf718a Update go.mod and go.sum (#22) 2025-12-20 03:32:25 +00:00
lukaszraczylo 5967f26c21 Update go.mod and go.sum (#21) 2025-12-19 03:37:51 +00:00
lukaszraczylo 285ced6755 fixup! Update go.mod and go.sum (#20) 2025-12-18 09:38:02 +00:00
lukaszraczylo 9fe076acb2 Update go.mod and go.sum (#20) 2025-12-18 03:37:26 +00:00
lukaszraczylo 92746efcf5 Update go.mod and go.sum (#19) 2025-12-15 03:40:40 +00:00
lukaszraczylo 391bce366d fixup! fixup! Add artifacts signing. 2025-12-15 00:16:16 +00:00
lukaszraczylo 9fd8f9b03b fixup! Add artifacts signing. 2025-12-14 23:56:42 +00:00
lukaszraczylo 7032bb5bee Add artifacts signing. 2025-12-14 23:29:27 +00:00
lukaszraczylo 6cb4f91ece Cleanup and refactor. 2025-12-14 18:17:20 +00:00
lukaszraczylo 5d600043f0 Update go.mod and go.sum (#18) 2025-12-13 03:32:36 +00:00
lukaszraczylo 9bb6fbc48d Update go.mod and go.sum (#17) 2025-12-12 03:38:34 +00:00
lukaszraczylo f4334ebdc9 Update go.mod and go.sum (#16) 2025-12-11 03:38:45 +00:00
lukaszraczylo 50f94bda87 Update go.mod and go.sum (#15) 2025-12-09 01:13:24 +00:00
lukaszraczylo d9888f1a56 Cleanup (#14)
* Codebase cleanup
2025-12-09 01:06:38 +00:00
lukaszraczylo 7dec532e18 Use shared PR workflow 2025-12-08 01:32:30 +00:00
lukaszraczylo aa7695b3be Trigger autoupdate. 2025-12-08 01:10:11 +00:00
lukaszraczylo 1bacd31f27 fixup! Add verified param to the releaser. 2025-12-07 16:40:37 +00:00
lukaszraczylo bfecbdf056 Add verified param to the releaser. 2025-12-07 14:40:12 +00:00
lukaszraczylo 754108474c fixup! fixup! fixup! Update gorelease config and docs - moving to cask due to depreciation 2025-12-07 14:36:45 +00:00
lukaszraczylo 690c587c0a fixup! fixup! Update gorelease config and docs - moving to cask due to depreciation 2025-12-07 14:16:43 +00:00
lukaszraczylo 0d03f228f9 fixup! Update gorelease config and docs - moving to cask due to depreciation 2025-12-07 14:04:38 +00:00
33 changed files with 231 additions and 299 deletions
+4 -1
View File
@@ -8,10 +8,13 @@ on:
permissions:
contents: write
actions: write
pull-requests: write
security-events: write
jobs:
autoupdate:
uses: lukaszraczylo/shared-actions/.github/workflows/go-autoupdate.yaml@main
with:
go-version: ">=1.21"
go-version: ">=1.24"
release-workflow: "release.yml"
secrets: inherit
+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"
+4 -3
View File
@@ -12,11 +12,12 @@ on:
permissions:
contents: write
packages: write
id-token: write
jobs:
release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: "1.23"
secrets:
homebrew-tap-token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
go-version: ">=1.24"
secrets: inherit
+2 -2
View File
@@ -6,7 +6,7 @@ on:
push:
branches: ["main"]
paths:
- 'docs/**'
- "docs/**"
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
@@ -39,7 +39,7 @@ jobs:
uses: actions/upload-pages-artifact@v3
with:
# Upload entire repository
path: 'docs/'
path: "docs/"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+20 -4
View File
@@ -62,7 +62,23 @@ homebrew_casks:
homepage: https://lukaszraczylo.github.io/kportal
description: "Modern Kubernetes port-forward manager with interactive TUI"
license: MIT
postflight: |
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"],
sudo: false
url:
verified: github.com/lukaszraczylo/kportal
hooks:
post:
install: |
if OS.mac?
system_command "/usr/bin/xattr",
args: ["-dr", "com.apple.quarantine", "#{staged_path}/kportal"]
end
signs:
- cmd: cosign
signature: "${artifact}.sigstore.json"
args:
- sign-blob
- "--bundle=${signature}"
- "${artifact}"
- "--yes"
artifacts: checksum
output: true
+13
View File
@@ -83,6 +83,19 @@ cd kportal
make build && make install
```
### Verifying Release Signatures
All release checksums are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify:
```bash
# Download the checksum file and its sigstore bundle from the release
cosign verify-blob \
--certificate-identity-regexp "https://github.com/lukaszraczylo/kportal/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "kportal-<version>-checksums.txt.sigstore.json" \
kportal-<version>-checksums.txt
```
## 🚀 Quick Start
Create `.kportal.yaml`:
+12 -2
View File
@@ -295,9 +295,9 @@ func main() {
// Interactive mode with bubbletea
bubbleTeaUI = ui.NewBubbleTeaUI(func(id string, enable bool) {
if enable {
manager.EnableForward(id)
_ = manager.EnableForward(id)
} else {
manager.DisableForward(id)
_ = manager.DisableForward(id)
}
}, appVersion)
@@ -309,16 +309,26 @@ func main() {
bubbleTeaUI.SetHTTPLogSubscriber(func(forwardID string, callback func(entry ui.HTTPLogEntry)) func() {
worker := manager.GetWorker(forwardID)
if worker == nil {
logger.Debug("HTTP log subscription failed: worker not found", map[string]interface{}{
"forward_id": forwardID,
})
return func() {} // No-op cleanup
}
proxy := worker.GetHTTPProxy()
if proxy == nil {
// This is expected for forwards without httpLog enabled - not an error
logger.Debug("HTTP log subscription skipped: proxy not enabled", map[string]interface{}{
"forward_id": forwardID,
})
return func() {} // HTTP logging not enabled for this forward
}
proxyLogger := proxy.GetLogger()
if proxyLogger == nil {
logger.Debug("HTTP log subscription failed: logger not available", map[string]interface{}{
"forward_id": forwardID,
})
return func() {}
}
+20 -21
View File
@@ -1,6 +1,6 @@
module github.com/nvm/kportal
go 1.24.2
go 1.25.0
require (
github.com/charmbracelet/bubbletea v1.3.10
@@ -10,28 +10,28 @@ require (
github.com/grandcat/zeroconf v1.0.0
github.com/stretchr/testify v1.11.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2
k8s.io/api v0.35.0
k8s.io/apimachinery v0.35.0
k8s.io/client-go v0.35.0
k8s.io/klog/v2 v2.130.1
)
require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/cenkalti/backoff v2.2.1+incompatible // indirect
github.com/charmbracelet/colorprofile v0.3.3 // indirect
github.com/charmbracelet/x/ansi v0.11.2 // indirect
github.com/charmbracelet/colorprofile v0.4.1 // indirect
github.com/charmbracelet/x/ansi v0.11.3 // indirect
github.com/charmbracelet/x/cellbuf v0.0.14 // indirect
github.com/charmbracelet/x/term v0.2.2 // indirect
github.com/clipperhouse/displaywidth v0.6.1 // indirect
github.com/clipperhouse/displaywidth v0.6.2 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-openapi/jsonpointer v0.22.3 // indirect
github.com/go-openapi/jsonreference v0.21.3 // indirect
github.com/go-openapi/jsonpointer v0.22.4 // indirect
github.com/go-openapi/jsonreference v0.21.4 // indirect
github.com/go-openapi/swag v0.25.4 // indirect
github.com/go-openapi/swag/cmdutils v0.25.4 // indirect
github.com/go-openapi/swag/conv v0.25.4 // indirect
@@ -44,7 +44,6 @@ require (
github.com/go-openapi/swag/stringutils v0.25.4 // indirect
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
@@ -54,7 +53,7 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.19 // indirect
github.com/miekg/dns v1.1.68 // indirect
github.com/miekg/dns v1.1.70 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
@@ -70,20 +69,20 @@ require (
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.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/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/mod v0.32.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/term v0.39.0 // indirect
golang.org/x/text v0.33.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
golang.org/x/tools v0.40.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // 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-20260108192941-914a6e750570 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect
+48 -68
View File
@@ -1,3 +1,5 @@
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
@@ -8,20 +10,20 @@ github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEe
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
github.com/charmbracelet/x/ansi v0.11.2 h1:XAG3FSjiVtFvgEgGrNBkCNNYrsucAt8c6bfxHyROLLs=
github.com/charmbracelet/x/ansi v0.11.2/go.mod h1:9tY2bzX5SiJCU0iWyskjBeI2BRQfvPqI+J760Mjf+Rg=
github.com/charmbracelet/x/ansi v0.11.3 h1:6DcVaqWI82BBVM/atTyq6yBoRLZFBsnoDoX9GCu2YOI=
github.com/charmbracelet/x/ansi v0.11.3/go.mod h1:yI7Zslym9tCJcedxz5+WBq+eUGMJT0bM06Fqy1/Y4dI=
github.com/charmbracelet/x/cellbuf v0.0.14 h1:iUEMryGyFTelKW3THW4+FfPgi4fkmKnnaLOXuc+/Kj4=
github.com/charmbracelet/x/cellbuf v0.0.14/go.mod h1:P447lJl49ywBbil/KjCk2HexGh4tEY9LH0/1QrZZ9rA=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a h1:G99klV19u0QnhiizODirwVksQB91TJKV/UaTnACcG30=
github.com/charmbracelet/x/exp/golden v0.0.0-20240806155701-69247e0abc2a/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
github.com/clipperhouse/displaywidth v0.6.1 h1:/zMlAezfDzT2xy6acHBzwIfyu2ic0hgkT83UX5EY2gY=
github.com/clipperhouse/displaywidth v0.6.1/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo=
github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
@@ -40,10 +42,10 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.22.3 h1:dKMwfV4fmt6Ah90zloTbUKWMD+0he+12XYAsPotrkn8=
github.com/go-openapi/jsonpointer v0.22.3/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo=
github.com/go-openapi/jsonreference v0.21.3 h1:96Dn+MRPa0nYAR8DR1E03SblB5FJvh7W6krPI0Z7qMc=
github.com/go-openapi/jsonreference v0.21.3/go.mod h1:RqkUP0MrLf37HqxZxrIAtTWW4ZJIK1VzduhXYBEeGc4=
github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4=
github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80=
github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8=
github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4=
github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU=
github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ=
github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4=
@@ -76,15 +78,13 @@ github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6
github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
@@ -93,8 +93,6 @@ github.com/grandcat/zeroconf v1.0.0 h1:uHhahLBKqwWBV6WZUDAT71044vwOTL+McW0mBJvo6
github.com/grandcat/zeroconf v1.0.0/go.mod h1:lTKmG1zh86XyCoUeIHSA4FJMBwCJiQmGfcP2PdzytEs=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -108,8 +106,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
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/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=
github.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -128,18 +126,18 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
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/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -152,66 +150,48 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
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/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.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/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
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-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
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-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/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.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-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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
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-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
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/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -221,18 +201,18 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY=
k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw=
k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4=
k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M=
k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE=
k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY=
k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA=
k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8=
k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=
k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE=
k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ=
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/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY=
k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
-10
View File
@@ -27,16 +27,6 @@ type Config struct {
ProgressCallback ProgressCallback // Optional callback for progress updates
}
// DefaultConfig returns a default benchmark configuration
func DefaultConfig() Config {
return Config{
Method: "GET",
Concurrency: 10,
Requests: 100,
Timeout: 30 * time.Second,
}
}
// Runner executes HTTP benchmarks
type Runner struct {
client *http.Client
-9
View File
@@ -206,15 +206,6 @@ func TestRunnerWithBody(t *testing.T) {
assert.Equal(t, int64(15), results.BytesWritten)
}
func TestDefaultConfig(t *testing.T) {
cfg := DefaultConfig()
assert.Equal(t, "GET", cfg.Method)
assert.Equal(t, 10, cfg.Concurrency)
assert.Equal(t, 100, cfg.Requests)
assert.Equal(t, 30*time.Second, cfg.Timeout)
}
func TestRunnerWithProgressCallback(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
time.Sleep(10 * time.Millisecond) // Add small delay so progress ticker can fire
+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)
}
// #nosec G304 -- path is validated in main.go (no system dirs, absolute path)
data, err := os.ReadFile(path)
if err != nil {
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
if err := os.Rename(tmpFile, m.configPath); err != nil {
// Clean up temp file on failure
os.Remove(tmpFile)
// Clean up temp file on failure - error ignored as we're already handling the rename error
_ = os.Remove(tmpFile)
return fmt.Errorf("failed to rename temp file: %w", err)
}
-5
View File
@@ -22,11 +22,6 @@ type ValidationError struct {
Context map[string]string // Additional context information
}
// Error implements the error interface.
func (e ValidationError) Error() string {
return e.Message
}
// Validator validates configuration files.
type Validator struct{}
-9
View File
@@ -621,15 +621,6 @@ func TestFormatValidationErrors(t *testing.T) {
}
}
func TestValidationError_Error(t *testing.T) {
err := ValidationError{
Field: "port",
Message: "Invalid port 0",
}
assert.Equal(t, "Invalid port 0", err.Error(), "Error() should return the message")
}
func TestValidator_ValidateStructure(t *testing.T) {
validator := NewValidator()
+8 -4
View File
@@ -22,6 +22,7 @@ type Watcher struct {
done chan struct{}
verbose bool
wg sync.WaitGroup // Ensures watch goroutine exits before Stop returns
stopOnce sync.Once // Ensures Stop is safe to call multiple times
}
// NewWatcher creates a new file watcher for the given config file.
@@ -33,7 +34,7 @@ func NewWatcher(configPath string, callback ReloadCallback, verbose bool) (*Watc
absPath, err := filepath.Abs(configPath)
if err != nil {
watcher.Close()
_ = watcher.Close()
return nil, fmt.Errorf("failed to resolve absolute path: %w", err)
}
@@ -41,7 +42,7 @@ func NewWatcher(configPath string, callback ReloadCallback, verbose bool) (*Watc
// (many editors delete and recreate files on save)
dir := filepath.Dir(absPath)
if err := watcher.Add(dir); err != nil {
watcher.Close()
_ = watcher.Close()
return nil, fmt.Errorf("failed to watch directory %s: %w", dir, err)
}
@@ -61,9 +62,12 @@ func (w *Watcher) Start() {
}
// Stop stops watching the configuration file and waits for the watch goroutine to exit.
// Safe to call multiple times.
func (w *Watcher) Stop() {
close(w.done)
w.watcher.Close()
w.stopOnce.Do(func() {
close(w.done)
_ = w.watcher.Close()
})
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
func ConvertKFTrayToKPortal(inputFile, outputFile string) error {
// Read kftray JSON config
// #nosec G304 -- inputFile is from command line argument for explicit conversion
data, err := os.ReadFile(inputFile)
if err != nil {
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
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)
if err != nil {
return nil, 0, fmt.Errorf("failed to read input file: %w", err)
-9
View File
@@ -135,15 +135,6 @@ func (b *Bus) Close() {
// Helper functions for creating common events
// NewForwardEvent creates a forward-related event
func NewForwardEvent(eventType EventType, forwardID string, data map[string]interface{}) Event {
return Event{
Type: eventType,
ForwardID: forwardID,
Data: data,
}
}
// NewHealthEvent creates a health status change event
func NewHealthEvent(forwardID string, status string, errorMsg string) Event {
return Event{
-10
View File
@@ -149,16 +149,6 @@ func TestBus_ConcurrentAccess(t *testing.T) {
assert.Equal(t, int64(100), atomic.LoadInt64(&count))
}
func TestNewForwardEvent(t *testing.T) {
event := NewForwardEvent(EventForwardStarting, "test-id", map[string]interface{}{
"pod": "my-pod",
})
assert.Equal(t, EventForwardStarting, event.Type)
assert.Equal(t, "test-id", event.ForwardID)
assert.Equal(t, "my-pod", event.Data["pod"])
}
func TestNewHealthEvent(t *testing.T) {
event := NewHealthEvent("test-id", "Active", "")
-26
View File
@@ -139,11 +139,6 @@ func (m *Manager) SetMDNSPublisher(publisher *mdns.Publisher) {
m.mdnsPublisher = publisher
}
// GetEventBus returns the event bus for subscribing to manager events
func (m *Manager) GetEventBus() *events.Bus {
return m.eventBus
}
// Start initializes and starts all port-forwards from the configuration.
func (m *Manager) Start(cfg *config.Config) error {
if cfg == nil {
@@ -493,27 +488,6 @@ func (m *Manager) stopWorkerInternal(id string, removeFromUI bool) error {
return nil
}
// GetActiveForwards returns a list of all active forward IDs.
func (m *Manager) GetActiveForwards() []string {
m.workersMu.RLock()
defer m.workersMu.RUnlock()
ids := make([]string, 0, len(m.workers))
for id := range m.workers {
ids = append(ids, id)
}
return ids
}
// GetWorkerCount returns the number of active workers.
func (m *Manager) GetWorkerCount() int {
m.workersMu.RLock()
defer m.workersMu.RUnlock()
return len(m.workers)
}
// GetWorker returns a worker by ID, or nil if not found.
func (m *Manager) GetWorker(id string) *ForwardWorker {
m.workersMu.RLock()
+1 -41
View File
@@ -7,7 +7,6 @@ import (
"github.com/nvm/kportal/internal/config"
"github.com/nvm/kportal/internal/events"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestNewManager tests manager creation
@@ -53,41 +52,6 @@ func TestManager_SetStatusUI(t *testing.T) {
assert.Equal(t, mockUI, manager.statusUI)
}
// TestManager_GetEventBus tests getting the event bus
func TestManager_GetEventBus(t *testing.T) {
manager, err := NewManager(false)
if err != nil {
t.Skip("Skipping test - no kubeconfig available")
}
defer manager.Stop()
bus := manager.GetEventBus()
assert.NotNil(t, bus)
}
// TestManager_GetWorkerCount tests worker count tracking
func TestManager_GetWorkerCount(t *testing.T) {
manager, err := NewManager(false)
if err != nil {
t.Skip("Skipping test - no kubeconfig available")
}
defer manager.Stop()
assert.Equal(t, 0, manager.GetWorkerCount())
}
// TestManager_GetActiveForwards tests getting active forwards
func TestManager_GetActiveForwards(t *testing.T) {
manager, err := NewManager(false)
if err != nil {
t.Skip("Skipping test - no kubeconfig available")
}
defer manager.Stop()
forwards := manager.GetActiveForwards()
assert.Empty(t, forwards)
}
// TestManager_GetWorker tests getting a worker by ID
func TestManager_GetWorker(t *testing.T) {
manager, err := NewManager(false)
@@ -362,12 +326,8 @@ func TestManager_EventBusIntegration(t *testing.T) {
// Event bus should be wired to health checker and watchdog
assert.NotNil(t, manager.eventBus)
// Get event bus
bus := manager.GetEventBus()
require.NotNil(t, bus)
// SubscribeAll should work (no return value in this API)
bus.SubscribeAll(func(event events.Event) {
manager.eventBus.SubscribeAll(func(event events.Event) {
// Handler
})
}
+3 -1
View File
@@ -77,6 +77,7 @@ func getProcessNameByPID(pid string) string {
// getProcessNameByPIDWindows retrieves the process name for a given PID on Windows
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")
output, err := cmd.Output()
if err != nil {
@@ -145,7 +146,7 @@ func (pc *PortChecker) isPortAvailable(port int) bool {
if err != nil {
return false
}
listener.Close()
_ = listener.Close()
return true
}
@@ -166,6 +167,7 @@ func (pc *PortChecker) getProcessUsingPort(port int) string {
func (pc *PortChecker) getProcessUsingPortUnix(port int) string {
// Use lsof to find the process
// 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")
output, err := cmd.Output()
if err != nil {
+13
View File
@@ -336,6 +336,11 @@ func (w *ForwardWorker) establishForward(podName string) error {
// Start port forwarding in a goroutine
errChan := make(chan error, 1)
go func() {
defer func() {
if r := recover(); r != nil {
errChan <- fmt.Errorf("port forward panicked: %v", r)
}
}()
errChan <- w.portForwarder.Forward(forwardCtx, req)
}()
@@ -409,6 +414,14 @@ func (w *ForwardWorker) startHTTPProxy() error {
// Calculate internal port for k8s tunnel
targetPort := w.forward.LocalPort + httpLogPortOffset
// Validate that the target port is available before attempting to bind
portChecker := NewPortChecker()
if !portChecker.isPortAvailable(targetPort) {
usedBy := portChecker.getProcessUsingPort(targetPort)
return fmt.Errorf("HTTP proxy target port %d is already in use by %s (forward port %d + offset %d)",
targetPort, usedBy, w.forward.LocalPort, httpLogPortOffset)
}
proxy, err := httplog.NewProxy(&w.forward, targetPort)
if err != nil {
return fmt.Errorf("failed to create HTTP proxy: %w", err)
+7 -8
View File
@@ -365,7 +365,8 @@ func (c *Checker) checkPort(forwardID string) {
}
}
// Update health status
// Update health status and capture eventBus while holding lock
var bus *events.Bus
c.mu.Lock()
if health, exists := c.ports[forwardID]; exists {
health.Status = newStatus
@@ -378,17 +379,15 @@ func (c *Checker) checkPort(forwardID string) {
health.LastActivity = now
}
}
// Capture eventBus while we have the lock to avoid race condition
bus = c.eventBus
c.mu.Unlock()
// Notify if status changed
if oldStatus != newStatus {
c.notifyStatusChange(forwardID, newStatus, errorMsg)
// Publish to event bus if available
c.mu.RLock()
bus := c.eventBus
c.mu.RUnlock()
// Publish to event bus if available (captured while holding lock above)
if bus != nil {
if newStatus == StatusStale {
bus.Publish(events.NewStaleEvent(forwardID, errorMsg))
@@ -409,7 +408,7 @@ func (c *Checker) checkTCPDial(port int) error {
if err != nil {
return err
}
conn.Close()
_ = conn.Close()
return nil
}
@@ -427,7 +426,7 @@ func (c *Checker) checkDataTransfer(port int) error {
// 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
conn.SetReadDeadline(time.Now().Add(c.timeout))
_ = conn.SetReadDeadline(time.Now().Add(c.timeout))
// Try to read a small amount of data
// 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
l.output = io.Discard
} 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)
if err != nil {
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) {
p.logError(r, err)
w.WriteHeader(http.StatusBadGateway)
w.Write([]byte("Proxy error: " + err.Error()))
_, _ = w.Write([]byte("Proxy error: " + err.Error()))
},
}
p.server = &http.Server{
Handler: proxy,
Handler: proxy,
ReadHeaderTimeout: 10 * time.Second,
}
p.running = true
@@ -122,8 +123,8 @@ func (p *Proxy) Stop() error {
defer cancel()
if err := p.server.Shutdown(ctx); err != nil {
// Force close
p.server.Close()
// Force close - error ignored as we're already shutting down
_ = p.server.Close()
}
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)
}
t.proxy.logger.Log(reqEntry)
_ = t.proxy.logger.Log(reqEntry)
// Make the request
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)
}
t.proxy.logger.Log(respEntry)
_ = t.proxy.logger.Log(respEntry)
return resp, nil
}
@@ -269,7 +270,7 @@ func (p *Proxy) logError(req *http.Request, err error) {
Path: req.URL.Path,
Error: err.Error(),
}
p.logger.Log(entry)
_ = p.logger.Log(entry)
}
// 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
listener.Close()
_ = listener.Close()
return true, "", nil
}
+1 -1
View File
@@ -228,7 +228,7 @@ func TestResolveTargetPort(t *testing.T) {
for i := range tt.pods {
objects = append(objects, &tt.pods[i])
}
fakeClient := fake.NewSimpleClientset(objects...)
fakeClient := fake.NewClientset(objects...)
// Create discovery instance (we only need it to call resolveTargetPort)
d := &Discovery{}
-17
View File
@@ -195,29 +195,12 @@ func shutdownWithTimeout(server *zeroconf.Server, forwardID string) {
}
}
// IsEnabled returns whether mDNS publishing is enabled.
func (p *Publisher) IsEnabled() bool {
return p.enabled
}
// GetDomain returns the mDNS domain being used (always "local" per RFC 6762).
func (p *Publisher) GetDomain() string {
return mdnsDomain
}
// GetHostname returns the full mDNS hostname for an alias.
// Example: GetHostname("myapp") returns "myapp.local"
func GetHostname(alias string) string {
return alias + "." + mdnsDomain
}
// GetRegisteredCount returns the number of currently registered hostnames.
func (p *Publisher) GetRegisteredCount() int {
p.mu.RLock()
defer p.mu.RUnlock()
return len(p.servers)
}
// getLocalIPs returns the local IP addresses for logging purposes.
func getLocalIPs() []string {
var ips []string
+25 -16
View File
@@ -13,15 +13,17 @@ import (
func TestNewPublisher_Disabled(t *testing.T) {
p := NewPublisher(false)
assert.False(t, p.IsEnabled())
assert.Equal(t, 0, p.GetRegisteredCount())
// When disabled, Register should succeed but be a no-op
err := p.Register("forward-1", "test-alias", 8080)
assert.NoError(t, err)
}
func TestNewPublisher_Enabled(t *testing.T) {
p := NewPublisher(true)
defer p.Stop()
assert.True(t, p.IsEnabled())
assert.Equal(t, 0, p.GetRegisteredCount())
// Enabled publisher should be created successfully
assert.NotNil(t, p)
}
func TestRegister_WhenDisabled_NoOp(t *testing.T) {
@@ -30,16 +32,17 @@ func TestRegister_WhenDisabled_NoOp(t *testing.T) {
err := p.Register("forward-1", "test-alias", 8080)
assert.NoError(t, err)
assert.Equal(t, 0, p.GetRegisteredCount())
// Unregister should also be safe when disabled
p.Unregister("forward-1")
}
func TestRegister_EmptyAlias_NoOp(t *testing.T) {
p := NewPublisher(true)
defer p.Stop()
err := p.Register("forward-1", "", 8080)
assert.NoError(t, err)
assert.Equal(t, 0, p.GetRegisteredCount())
}
func TestUnregister_WhenDisabled_NoOp(t *testing.T) {
@@ -51,10 +54,10 @@ func TestUnregister_WhenDisabled_NoOp(t *testing.T) {
func TestUnregister_NotRegistered_NoOp(t *testing.T) {
p := NewPublisher(true)
defer p.Stop()
// Should not panic
p.Unregister("non-existent")
assert.Equal(t, 0, p.GetRegisteredCount())
}
func TestStop_WhenDisabled_NoOp(t *testing.T) {
@@ -69,7 +72,6 @@ func TestStop_WhenNoRegistrations(t *testing.T) {
// Should not panic
p.Stop()
assert.Equal(t, 0, p.GetRegisteredCount())
}
func TestGetLocalIPs(t *testing.T) {
@@ -84,6 +86,11 @@ func TestGetLocalIPs(t *testing.T) {
}
}
func TestGetHostname(t *testing.T) {
hostname := GetHostname("myapp")
assert.Equal(t, "myapp.local", hostname)
}
// Integration tests - only run when explicitly requested
// These tests actually register mDNS services and require network access
@@ -96,9 +103,10 @@ func TestRegister_Integration(t *testing.T) {
defer p.Stop()
err := p.Register("forward-1", "test-service", 8080)
assert.NoError(t, err)
assert.Equal(t, 1, p.GetRegisteredCount())
// Verify by checking that unregister doesn't panic
p.Unregister("forward-1")
}
func TestRegister_Duplicate_Idempotent_Integration(t *testing.T) {
@@ -112,12 +120,10 @@ func TestRegister_Duplicate_Idempotent_Integration(t *testing.T) {
// First registration
err := p.Register("forward-1", "test-service", 8080)
assert.NoError(t, err)
assert.Equal(t, 1, p.GetRegisteredCount())
// Second registration with same ID should be idempotent
err = p.Register("forward-1", "test-service", 8080)
assert.NoError(t, err)
assert.Equal(t, 1, p.GetRegisteredCount())
}
func TestRegister_MultipleForwards_Integration(t *testing.T) {
@@ -135,7 +141,6 @@ func TestRegister_MultipleForwards_Integration(t *testing.T) {
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NoError(t, err3)
assert.Equal(t, 3, p.GetRegisteredCount())
}
func TestUnregister_Success_Integration(t *testing.T) {
@@ -146,9 +151,13 @@ func TestUnregister_Success_Integration(t *testing.T) {
p := NewPublisher(true)
defer p.Stop()
p.Register("forward-1", "test-service", 8080)
assert.Equal(t, 1, p.GetRegisteredCount())
err := p.Register("forward-1", "test-service", 8080)
assert.NoError(t, err)
// Unregister should not panic and should handle it gracefully
p.Unregister("forward-1")
assert.Equal(t, 0, p.GetRegisteredCount())
// Re-registering should work after unregister
err = p.Register("forward-1", "test-service-2", 8080)
assert.NoError(t, err)
}
+12 -2
View File
@@ -11,6 +11,9 @@ const (
initialDelay = 1 * time.Second
maxDelay = 10 * time.Second
jitterPct = 0.1 // 10% jitter
// maxAttempt caps the exponent to prevent math.Pow overflow
// 2^30 seconds is ~34 years, well above maxDelay, so this is safe
maxAttempt = 30
)
// Backoff implements exponential backoff with jitter for retry logic.
@@ -24,7 +27,8 @@ type Backoff struct {
func NewBackoff() *Backoff {
return &Backoff{
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())),
}
}
@@ -32,8 +36,14 @@ func NewBackoff() *Backoff {
// The duration follows exponential backoff: 1s → 2s → 4s → 8s → 10s (max).
// A 10% jitter is added to prevent thundering herd effects.
func (b *Backoff) Next() time.Duration {
// Cap attempt to prevent overflow in math.Pow
attempt := b.attempt
if attempt > maxAttempt {
attempt = maxAttempt
}
// Calculate base delay: 2^attempt seconds
exp := math.Pow(2, float64(b.attempt))
exp := math.Pow(2, float64(attempt))
delay := time.Duration(exp) * time.Second
// Cap at max delay
+1 -7
View File
@@ -144,15 +144,9 @@ func parseVersion(v string) []int {
for _, p := range parts {
var num int
fmt.Sscanf(p, "%d", &num)
_, _ = fmt.Sscanf(p, "%d", &num)
result = append(result, num)
}
return result
}
// FormatUpdateMessage formats a user-friendly update notification
func (u *UpdateInfo) FormatUpdateMessage() string {
return fmt.Sprintf("New version available: %s (current: %s) - %s",
u.LatestVersion, u.CurrentVersion, u.ReleaseURL)
}
-13
View File
@@ -75,16 +75,3 @@ func TestIsNewerVersion(t *testing.T) {
})
}
}
func TestUpdateInfo_FormatUpdateMessage(t *testing.T) {
info := &UpdateInfo{
CurrentVersion: "0.1.0",
LatestVersion: "0.2.0",
ReleaseURL: "https://github.com/nvm/kportal/releases/tag/v0.2.0",
}
msg := info.FormatUpdateMessage()
assert.Contains(t, msg, "0.2.0")
assert.Contains(t, msg, "0.1.0")
assert.Contains(t, msg, "https://github.com/nvm/kportal/releases/tag/v0.2.0")
}