init pgpf cli tool

This commit is contained in:
Денис Балабанов 2025-05-02 21:13:52 +03:00
parent d25ca0a536
commit c27953f744
13 changed files with 1115 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
bin/

150
.golangci.yml Normal file
View File

@ -0,0 +1,150 @@
run:
timeout: 15m
allow-parallel-runners: true
linters:
disable-all: true
enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- copyloopvar
- containedctx
- dogsled
- dupword
- durationcheck
- errcheck
- errchkjson
- gci
- goconst
- gocritic
- godot
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- importas
- ineffassign
- misspell
- nakedret
- nilerr
- noctx
- nolintlint
- nosprintfhostport
- prealloc
- predeclared
- revive
- rowserrcheck
- staticcheck
- stylecheck
- thelper
- typecheck
- unconvert
- unparam
- unused
- usestdlibvars
- whitespace
linters-settings:
gci:
sections:
- "standard"
- "default"
- "prefix(github.com/balabanovds)"
custom-order: true
importas:
no-unaliased: true
goconst:
min-len: 5
min-occurrences: 99
lll:
line-length: 140
revive:
rules:
# The following rules are recommended https://github.com/mgechev/revive#recommended-configuration
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: error-return
- name: error-strings
- name: error-naming
# - name: exported
- name: if-return
- name: increment-decrement
- name: var-naming
- name: var-declaration
# - name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
- name: empty-block
- name: superfluous-else
- name: unused-parameter
- name: unreachable-code
- name: redefines-builtin-id
#
# Rules in addition to the recommended configuration above.
#
- name: bool-literal-in-expr
- name: constant-logical-expr
gosec:
excludes:
- G307 # Deferring unsafe method "Close" on type "\*os.File"
- G108 # Profiling endpoint is automatically exposed on /debug/pprof
- G115 # integer overflow conversion int -> uint32
gocritic:
enabled-tags:
- diagnostic
- experimental
- performance
disabled-checks:
- appendAssign
- dupImport # https://github.com/go-critic/go-critic/issues/845
- evalOrder
- ifElseChain
- octalLiteral
- regexpSimplify
- singleCaseSwitch
- sloppyReassign
- truncateCmp
- typeDefFirst
- unnamedResult
- unnecessaryDefer
- whyNoLint
- wrapperFunc
- rangeValCopy
- hugeParam
issues:
exclude-rules:
- text: "at least one file in a package should have a package comment"
linters:
- stylecheck
- text: "should have a package comment, unless it's in another file for this package"
linters:
- golint
- text: "should have comment or be unexported"
linters:
- golint
- text: "don't use an underscore in package name"
linters:
- revive
- text: "should not use underscores in package names"
linters:
- stylecheck
- path: _test\.go
linters:
- gosec
- dupl
exclude-use-default: false
exclude-files:
- \.pb\.go$
- \.pb\.validate\.go$
- \.pb\.gw\.go$

19
Makefile Normal file
View File

@ -0,0 +1,19 @@
OUT_DIR=bin
PROG_NAME_PGPF=pgpf
build-mac-arm64-pgpf:
CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -o ./$(OUT_DIR)/$(PROG_NAME_PGPF)_macos_arm64 main.go
build-mac-amd64-pgpf:
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -o ./$(OUT_DIR)/$(PROG_NAME_PGPF)_macos_amd64 main.go
build-linux-amd64-pgpf:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ./$(OUT_DIR)/$(PROG_NAME_PGPF)_linux_amd64 main.go
build-windows-amd64-pgpf:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ./$(OUT_DIR)/$(PROG_NAME_PGPF)_windows_amd64.exe main.go
build-all: build-mac-arm64-pgpf build-mac-amd64-pgpf build-linux-amd64-pgpf build-windows-amd64-pgpf
lint:
golangci-lint run --max-issues-per-linter 0 --max-same-issues 0 -v ./...

74
cmd/pgpf/README.md Normal file
View File

@ -0,0 +1,74 @@
# pgpf
## Назначение
Программа предназначена для проброса портов (port-forwarding) для пода на котором развернут инстанс БД Postgres.
Опционально: запуск терминальных клиентов `psql` или `pgcli`.
Расшифровывается pgpf как PostGres Port Forward
## Аргументы
- `--config` - файл с конфигурацией в формате yaml (поля конфига описаны ниже. По дефолту `$HOME/.pgpf/config.yaml`)
- `--kubeconfig` - файл с кубконфигом (по дефолту `$HOME/.kube/config`)
- `--ns` - неймспейс пода - обязательное поле, может использоваться часть
### Конфиг приложения
```yaml
app:
port-forward-only:
enabled: false # использовать только проброс портов
local-host: IP.AD.DR.ES # IPv4 адрес куда пробрасывать трафик (если не указано, то по умолчанию 127.0.0.1)
local-port: 0 # локальный TCP порт куда пробрасывать трафик (если не указано, то выбирается любой свободный)
use-app:
pgcli: false # использовать терминальное приложение pgcli
psql: true # использовать терминальное приложение psql
kube:
db:
pod-substring: db-cluster-pooler # подстрока в имени пода с БД для фильтрации
secret:
pod-substring: db-cluster-app # подстрока в имени секрета для фильтрации
data-fields:
password: password # имя поля, в котором лежит пароль
username: user # имя поля, в которм лежит имя пользователя
dbname: dbname # имя поля, в котором лежит название базы данных
port: port # имя поля, в котором лежит порт на которм слешает инстанс БД
```
Примечания:
1. Если используется только проброс портов `app.port-forward-only.enabled = true`, то использование терминальных приложений игнорируется. В выводе будет указан хост и порт которые можно будет использовать для подключения.
При этом в файл `$HOME/.pgpass` добавляется временная запись с кредами подключения. При завершении работы эта запись удаляется.
2. При использовании терминальных приложений, всегда используется локальный хост и любой свободный порт. Креды нигде не сохраняются, а передаются в терминальное приложение напрямую. Соответственно, терминальное приложение должно находиться в $PATH.
3. Приложение ищет под в неймспейсе или если это часть неймспейса, то делается запрос на получение подов из всех неймспейсов, Из общего списка отфильтровывается список содержащий часть неймспейса. И в последнем уже ищется под с именем, содержащим в названии значение `kube.db.pod-substring`
Если найдено более одного пода, то приложение завершается с ошибкой (в будущем добавлю интерактив)
4. Данные для подключения берутся из секрета. Секрет берется из неймспейса найденного пода и отфильровывается по имени, содержащем `kube.secret.pod-substring`. Если секрет не найден, приложение завершается с ошибкой.
### Пример использования
Терминальное приложение pgcli
```bash
$ pgpf --kubeconfig /Users/dsbalabanov/.kube/kubeconfig_dev2_sso.config --ns backbone
Forwarding from 127.0.0.1:50159 -> 5432
Forwarding from [::1]:50159 -> 5432
Handling connection for 50159
Handling connection for 50159
Server: PostgreSQL 16.3 (Debian 16.3-1.pgdg110+1)
Version: 4.2.0
Home: http://pgcli.com
backbone-api>
```
Только проброс портов
```bash
$ pgpf --kubeconfig /Users/dsbalabanov/.kube/kubeconfig_dev2_sso.config --ns backbone
Forwarding from 127.0.0.1:50222 -> 5432
Forwarding from [::1]:50222 -> 5432
Connection to database backbone-api is ready, temporary data added to .pgpass. Use 127.0.0.1:50222 for connection...
```

199
cmd/pgpf/main.go Normal file
View File

@ -0,0 +1,199 @@
package main
import (
"context"
"fmt"
"net"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"github.com/spf13/pflag"
"golang.org/x/sync/errgroup"
"github.com/balabanovds/pgcli/internal/config"
"github.com/balabanovds/pgcli/internal/core"
"github.com/balabanovds/pgcli/internal/kube"
"github.com/balabanovds/pgcli/internal/pg"
)
const (
appName = "pgpf"
)
func main() {
ctx := context.Background()
if err := run(ctx); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func run(ctx context.Context) error {
var defaultAppConfig string
if home := core.Homedir(); home != "" {
defaultAppConfig = filepath.Join(home, "."+appName, "config.yaml")
}
var (
appconfig string
kubeconfig string
namespace string
)
pflag.StringVar(&appconfig, "config", defaultAppConfig, "Application configuration file path")
pflag.StringVar(&kubeconfig, "kubeconfig", "", "Absolute path to the kubeconfig file")
pflag.StringVar(&namespace, "ns", "", "Namespace (required)")
pflag.Parse()
if namespace == "" {
pflag.Usage()
os.Exit(1)
}
appCfg, err := config.InitConfig(appconfig)
if err != nil {
return fmt.Errorf("failed to initiate application config %s: %w", appconfig, err)
}
cl, err := kube.NewClient(kubeconfig, &appCfg.Kube)
if err != nil {
return fmt.Errorf("failed to create Kubernetes client: %w", err)
}
pods, err := cl.PodNames(ctx, namespace)
if err != nil {
return fmt.Errorf("failed to get Pods: %w", err)
}
if len(pods) == 0 {
return fmt.Errorf("pods not found")
}
if len(pods) != 1 {
return fmt.Errorf("expected only one pod")
}
pod := pods[0]
secret, err := cl.GetSecret(ctx, pod.Namespace)
if err != nil {
return fmt.Errorf("failed to get secret for namespace %s: %w", pod.Namespace, err)
}
// stopCh control the port forwarding lifecycle. When it gets closed the
// port forward will terminate
stopCh := make(chan struct{}, 1)
// readyCh communicate when the port forward is ready to get traffic
readyCh := make(chan struct{})
// managing termination signal from the terminal. As you can see the stopCh
// gets closed to gracefully handle its termination.
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigs
fmt.Println("Bye...")
close(stopCh)
}()
var localport int
if appCfg.App.PortForwardOnly.Enabled && appCfg.App.PortForwardOnly.LocalPort != 0 {
localport = appCfg.App.PortForwardOnly.LocalPort
} else {
localport, err = freeTCPPort()
if err != nil {
return fmt.Errorf("failed to get free local TCP port: %w", err)
}
}
localhost := "127.0.0.1"
if appCfg.App.PortForwardOnly.Enabled && appCfg.App.PortForwardOnly.LocalHost != "" {
localhost = appCfg.App.PortForwardOnly.LocalHost
}
var wgerr errgroup.Group
wgerr.Go(func() error {
return cl.ForwardPort(&kube.ForwardPodRequest{
Pod: pod,
LocalPort: localport,
PodPort: secret.Port,
Stream: core.Stream{
In: os.Stdin,
Out: os.Stdout,
Err: os.Stderr,
},
StopCh: stopCh,
ReadyCh: readyCh,
})
})
<-readyCh
if appCfg.App.PortForwardOnly.Enabled {
pgpass, err := pg.NewPgPassHandler()
if err != nil {
return fmt.Errorf("failed to create pgpass handler: %w", err)
}
defer func() {
if err = pgpass.Close(); err != nil {
fmt.Println("error occurred during closing pgpass handler", err)
}
}()
err = pgpass.Append(secret, localhost, localport)
if err != nil {
return fmt.Errorf("failed to add new data to pgpass file: %w", err)
}
fmt.Printf("Connection to database %s is ready, temporary data added to .pgpass. Use %s:%d for connection...",
secret.DBName, localhost, localport)
return wgerr.Wait()
}
wgerr.Go(func() error {
defer close(stopCh)
dsn := dsn(secret, localport)
var cmd *exec.Cmd
switch {
case appCfg.App.UseApp.PgCLI:
cmd = exec.CommandContext(ctx, "pgcli", dsn) //nolint:gosec
case appCfg.App.UseApp.Psql:
cmd = exec.CommandContext(ctx, "psql", dsn) //nolint:gosec
default:
panic("can not run unknown command")
}
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
})
return wgerr.Wait()
}
func freeTCPPort() (int, error) {
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
if err != nil {
return 0, err
}
l, err := net.ListenTCP("tcp", addr)
if err != nil {
return 0, err
}
defer func() {
if cerr := l.Close(); cerr != nil {
fmt.Printf("failed to close socket: %s", cerr)
}
}()
return l.Addr().(*net.TCPAddr).Port, nil
}
func dsn(secret *kube.Secret, localport int) string {
return fmt.Sprintf("postgresql://%s:%s@localhost:%d/%s", secret.User, secret.Password, localport, secret.DBName)
}

21
config.yaml Normal file
View File

@ -0,0 +1,21 @@
app:
port-forward-only:
enabled: false
local-host: 127.0.0.1
local-port: 0 # zero - will be selected any free TCP port
use-app:
pgcli: false
psql: true
kube:
db:
pod-substring: db-cluster-pooler
secret:
pod-substring: db-cluster-app
data-fields:
password: password
username: user
dbname: dbname
port: port

69
go.mod Normal file
View File

@ -0,0 +1,69 @@
module github.com/balabanovds/pgcli
go 1.24.1
require (
github.com/spf13/pflag v1.0.5
golang.org/x/sync v0.12.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/cli-runtime v0.33.0
k8s.io/client-go v0.33.0
)
require (
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/btree v1.1.3 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/spf13/cobra v1.8.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
golang.org/x/net v0.38.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sys v0.31.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/text v0.23.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/kustomize/api v0.19.0 // indirect
sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

205
go.sum Normal file
View File

@ -0,0 +1,205 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
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/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
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/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
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=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=
github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
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/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
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/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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
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/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
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/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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-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.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
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-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
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-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.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
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-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.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
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.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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.33.0 h1:yTgZVn1XEe6opVpP1FylmNrIFWuDqe2H0V8CT5gxfIU=
k8s.io/api v0.33.0/go.mod h1:CTO61ECK/KU7haa3qq8sarQ0biLq2ju405IZAd9zsiM=
k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ=
k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/cli-runtime v0.33.0 h1:Lbl/pq/1o8BaIuyn+aVLdEPHVN665tBAXUePs8wjX7c=
k8s.io/cli-runtime v0.33.0/go.mod h1:QcA+r43HeUM9jXFJx7A+yiTPfCooau/iCcP1wQh4NFw=
k8s.io/client-go v0.33.0 h1:UASR0sAYVUzs2kYuKn/ZakZlcs2bEHaizrrHUZg0G98=
k8s.io/client-go v0.33.0/go.mod h1:kGkd+l/gNGg8GYWAPr0xF1rRKvVWvzh9vmZAMXtaKOg=
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-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

62
internal/config/config.go Normal file
View File

@ -0,0 +1,62 @@
package config
import (
"bytes"
"os"
"path/filepath"
"gopkg.in/yaml.v3"
)
type Config struct {
Kube KubeConfig `yaml:"kube"`
App AppConfig `yaml:"app"`
}
type KubeConfig struct {
PodDB KubeSecretDBConfig `yaml:"db"`
Secret KubeSecretDBConfig `yaml:"secret"`
}
type KubePodDBConfig struct {
Substring string `yaml:"pod-substring"`
}
type KubeSecretDBConfig struct {
Substring string `yaml:"pod-substring"`
DataFields struct {
Password string `yaml:"password"`
Username string `yaml:"username"`
DBName string `yaml:"dbname"`
Port string `yaml:"port"`
} `yaml:"data-fields"`
}
type AppConfig struct {
PortForwardOnly struct {
Enabled bool `yaml:"enabled"`
LocalHost string `yaml:"local-host"`
LocalPort int `yaml:"local-port"`
} `yaml:"port-forward-only"`
UseApp struct {
PgCLI bool `yaml:"pgcli"`
Psql bool `yaml:"psql"`
} `yaml:"use-app"`
}
func InitConfig(config string) (*Config, error) {
f, err := os.ReadFile(filepath.Clean(config))
if err != nil {
return nil, err
}
buf := bytes.NewReader(f)
var cfg Config
err = yaml.NewDecoder(buf).Decode(&cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

10
internal/core/os.go Normal file
View File

@ -0,0 +1,10 @@
package core
import "os"
func Homedir() string {
if h := os.Getenv("HOME"); h != "" {
return h
}
return os.Getenv("USERPROFILE") // windows
}

9
internal/core/stream.go Normal file
View File

@ -0,0 +1,9 @@
package core
import "io"
type Stream struct {
In io.Reader
Out io.Writer
Err io.Writer
}

176
internal/kube/client.go Normal file
View File

@ -0,0 +1,176 @@
package kube
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/tools/portforward"
"k8s.io/client-go/transport/spdy"
"github.com/balabanovds/pgcli/internal/config"
"github.com/balabanovds/pgcli/internal/core"
)
type Client struct {
config *rest.Config
appConfig *config.KubeConfig
client *kubernetes.Clientset
}
func NewClient(kubeconfig string, appConfig *config.KubeConfig) (*Client, error) {
config, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
if err != nil {
config, err = defaultConfig()
if err != nil {
return nil, fmt.Errorf("failed to read Kubernetes config: %w", err)
}
}
client, err := kubernetes.NewForConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to create Kubernetes client: %w", err)
}
return &Client{
config: config,
appConfig: appConfig,
client: client,
}, nil
}
type NamespacedName struct {
Namespace string
Name string
}
func (c *Client) PodNames(ctx context.Context, namespace string) ([]NamespacedName, error) {
podsList, err := c.client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get Pods for namespace %s: %w", namespace, err)
}
pods := podsList.Items
if len(pods) == 0 {
allpods, err := c.client.CoreV1().Pods("").List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get Pods for namespace %s: %w", namespace, err)
}
for _, p := range allpods.Items {
if strings.Contains(p.Namespace, namespace) {
pods = append(pods, p)
}
}
}
res := make([]NamespacedName, 0, 1)
for _, p := range pods {
if strings.Contains(p.Name, c.appConfig.PodDB.Substring) {
res = append(res, NamespacedName{
Namespace: p.Namespace,
Name: p.Name,
})
}
}
return res, nil
}
type Secret struct {
User string
Password string
DBName string
Port int
}
func (c *Client) GetSecret(ctx context.Context, namespace string) (*Secret, error) {
secrets, err := c.client.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get Pods for namespace %s: %w", namespace, err)
}
var scr *v1.Secret
for _, s := range secrets.Items {
if strings.Contains(s.Name, c.appConfig.Secret.Substring) {
scr = &s
break
}
}
if scr == nil {
return nil, fmt.Errorf("secret for namespace %q containing substring %q not found", namespace, c.appConfig.Secret.Substring)
}
port, err := strconv.Atoi(string(scr.Data[c.appConfig.Secret.DataFields.Port]))
if err != nil {
port = 5432
}
return &Secret{
User: string(scr.Data[c.appConfig.Secret.DataFields.Username]),
Password: string(scr.Data[c.appConfig.Secret.DataFields.Password]),
DBName: string(scr.Data[c.appConfig.Secret.DataFields.DBName]),
Port: port,
}, nil
}
type ForwardPodRequest struct {
// Pod is the selected pod for this port forwarding
Pod NamespacedName
// LocalPort is the local port that will be selected to expose the PodPort
LocalPort int
// PodPort is the target port for the pod
PodPort int
// Steam configures where to write or read input from
Stream core.Stream
// StopCh is the channel used to manage the port forward lifecycle
StopCh <-chan struct{}
// ReadyCh communicates when the tunnel is ready to receive traffic
ReadyCh chan struct{}
}
func (c *Client) ForwardPort(req *ForwardPodRequest) error {
// stream is used to tell the port forwarder where to place its output or
// where to expect input if needed. For the port forwarding we just need
// the output eventually
iostream := genericclioptions.IOStreams{
In: req.Stream.In,
Out: req.Stream.Out,
ErrOut: req.Stream.Err,
}
path := fmt.Sprintf("/api/v1/namespaces/%s/pods/%s/portforward",
req.Pod.Namespace, req.Pod.Name)
hostIP := strings.TrimLeft(c.config.Host, "htps:/")
transport, upgrader, err := spdy.RoundTripperFor(c.config)
if err != nil {
return err
}
dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, http.MethodPost, &url.URL{Scheme: "https", Path: path, Host: hostIP})
fw, err := portforward.New(dialer, []string{fmt.Sprintf("%d:%d", req.LocalPort, req.PodPort)}, req.StopCh, req.ReadyCh, iostream.Out, iostream.ErrOut)
if err != nil {
return err
}
return fw.ForwardPorts()
}
func defaultConfig() (*rest.Config, error) {
pathOptions := clientcmd.NewDefaultPathOptions()
config, err := clientcmd.BuildConfigFromKubeconfigGetter("", pathOptions.GetStartingConfig)
if err != nil {
return nil, fmt.Errorf("error loading REST configuration using default path options: %w", err)
}
return config, nil
}

120
internal/pg/pgpass.go Normal file
View File

@ -0,0 +1,120 @@
package pg
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"time"
"github.com/balabanovds/pgcli/internal/core"
"github.com/balabanovds/pgcli/internal/kube"
)
type PgPassHandler struct {
orig string
bckp string
deleteOriginal bool
}
func NewPgPassHandler() (*PgPassHandler, error) {
if core.Homedir() == "" {
return nil, fmt.Errorf("unable to figure out your home dir")
}
h := &PgPassHandler{}
h.orig = filepath.Join(core.Homedir(), ".pgpass")
h.bckp = fmt.Sprintf("%s-%d", h.orig, time.Now().UnixMilli())
if !h.fileExists(h.orig) {
_, err := os.Create(h.orig)
if err != nil {
return nil, fmt.Errorf("failed to create .pgpass file for portforward: %w", err)
}
h.deleteOriginal = true
} else {
err := h.copyFile(h.orig, h.bckp)
if err != nil {
return nil, fmt.Errorf("failed to copy original file %s to backup %s: %w", h.orig, h.bckp, err)
}
}
return h, nil
}
func (h *PgPassHandler) Close() error {
if err := h.deleteFile(h.orig); err != nil {
return fmt.Errorf("failed to delete file %s: %w", h.orig, err)
}
if err := h.copyFile(h.bckp, h.orig); err != nil {
return fmt.Errorf("failed to copy file from %s to %s: %w", h.bckp, h.orig, err)
}
if h.deleteOriginal {
if err := h.deleteFile(h.orig); err != nil {
return fmt.Errorf("failed to delete file %s: %w", h.orig, err)
}
}
if err := h.deleteFile(h.bckp); err != nil {
return fmt.Errorf("failed to delete file %s: %w", h.bckp, err)
}
return nil
}
func (h *PgPassHandler) Append(sec *kube.Secret, localhost string, localport int) error {
f, err := os.OpenFile(h.orig, os.O_APPEND|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("failed to append data to file %s: %w", h.orig, err)
}
defer func() {
if cerr := f.Close(); cerr != nil {
fmt.Printf("failed to close file %s during appendL %s", h.orig, cerr)
}
}()
_, err = fmt.Fprintf(f, "# added by programm pgpf\n%s:%d:%s:%s:%s\n", localhost, localport, sec.DBName, sec.User, sec.Password)
return err
}
func (h *PgPassHandler) copyFile(src, dst string) error {
src = filepath.Clean(src)
fsrc, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", src, err)
}
defer func() {
if cerr := fsrc.Close(); cerr != nil {
fmt.Printf("failed to close source file %s during copy: %s\n", src, cerr)
}
}()
dst = filepath.Clean(dst)
fdst, err := os.OpenFile(dst, os.O_CREATE|os.O_WRONLY, 0o600)
if err != nil {
return fmt.Errorf("failed to open file %s: %w", dst, err)
}
defer func() {
if cerr := fdst.Close(); cerr != nil {
fmt.Printf("faled to close destination file %s during copy: %s\n", dst, cerr)
}
}()
_, err = io.Copy(fdst, fsrc)
if err != nil {
return fmt.Errorf("failed to copy file from %s to %s: %w", src, dst, err)
}
return nil
}
func (h *PgPassHandler) fileExists(src string) bool {
_, err := os.Stat(src)
return err == nil || errors.Is(err, os.ErrNotExist)
}
func (h *PgPassHandler) deleteFile(src string) error {
return os.Remove(src)
}