├── .dockerignore ├── .github └── workflows │ ├── release.yaml │ └── test.yaml ├── .gitignore ├── .goreleaser.yml ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── _images └── example.png ├── counter.yaml ├── go.mod ├── go.sum ├── logger.go ├── main.go ├── renovate.json ├── tail.go ├── version.go └── watch.go /.dockerignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | # Output of the go coverage tool, specifically when used with LiteIDE 31 | *.out 32 | 33 | # external packages folder 34 | vendor/ 35 | 36 | bin/ 37 | dist/ 38 | 39 | .git 40 | .gitignore 41 | README.md 42 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - "v*.*.*" 6 | 7 | jobs: 8 | goreleaser: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 13 | - name: Setup Go 14 | uses: actions/setup-go@v5 15 | with: 16 | go-version: 1.20 17 | - name: Run GoReleaser 18 | uses: goreleaser/goreleaser-action@v6 19 | with: 20 | version: latest 21 | args: release --rm-dist 22 | env: 23 | # To upload Homebrew recipe to dtan4/homebrew-tools, we need a personal token 24 | # instead of Action's temporary token 25 | GITHUB_TOKEN: ${{ secrets.PERSONAL_GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 15 | 16 | - name: Setup Go 17 | uses: actions/setup-go@v5 18 | with: 19 | cache: true 20 | go-version-file: 'go.mod' 21 | 22 | - name: Run tests 23 | run: make ci-test 24 | 25 | - name: Send test coverage to Codecov 26 | uses: codecov/codecov-action@v5 27 | 28 | - name: Run Trivy vulnerability scanner in repo mode 29 | uses: aquasecurity/trivy-action@0.30.0 30 | with: 31 | scan-type: "fs" 32 | ignore-unfixed: true 33 | vuln-type: "os,library" 34 | severity: "CRITICAL,HIGH" 35 | exit-code: "1" 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go 3 | 4 | ### Go ### 5 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 6 | *.o 7 | *.a 8 | *.so 9 | 10 | # Folders 11 | _obj 12 | _test 13 | 14 | # Architecture specific extensions/prefixes 15 | *.[568vq] 16 | [568vq].out 17 | 18 | *.cgo1.go 19 | *.cgo2.c 20 | _cgo_defun.c 21 | _cgo_gotypes.go 22 | _cgo_export.* 23 | 24 | _testmain.go 25 | 26 | *.exe 27 | *.test 28 | *.prof 29 | 30 | # Output of the go coverage tool, specifically when used with LiteIDE 31 | *.out 32 | 33 | # external packages folder 34 | vendor/ 35 | 36 | /bin/ 37 | /dist/ 38 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # This is an example goreleaser.yaml file with some sane defaults. 2 | # Make sure to check the documentation at http://goreleaser.com 3 | before: 4 | hooks: 5 | - make clean 6 | - go mod tidy 7 | builds: 8 | - ldflags: 9 | - "-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}" 10 | env: 11 | - CGO_ENABLED=0 12 | goos: 13 | - darwin 14 | - linux 15 | - windows 16 | goarch: 17 | - 386 18 | - amd64 19 | - arm 20 | - arm64 21 | archives: 22 | - name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' 23 | replacements: 24 | darwin: Darwin 25 | linux: Linux 26 | windows: Windows 27 | 386: i386 28 | amd64: x86_64 29 | format_overrides: 30 | - goos: windows 31 | format: zip 32 | release: 33 | prerelease: auto 34 | brews: 35 | - tap: 36 | owner: dtan4 37 | name: homebrew-tools 38 | folder: Formula 39 | homepage: https://github.com/dtan4/k8stail 40 | description: "`tail -f` experience for Kubernetes Pods" 41 | skip_upload: auto # skip if the version is rc (e.g. v1.0.0-rc1) 42 | test: | 43 | system "#{bin}/k8stail", "-v" 44 | checksum: 45 | name_template: 'checksums.txt' 46 | snapshot: 47 | name_template: "{{ .Tag }}-next" 48 | changelog: 49 | sort: asc 50 | filters: 51 | exclude: 52 | - '^docs:' 53 | - '^test:' 54 | - Merge pull request 55 | - Merge branch 56 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [v0.7.0](https://github.com/dtan4/k8stail/releases/tag/v0.7.0) (2022-03-05) 2 | 3 | - Go 1.17 4 | - Update dependencies 5 | 6 | # [v0.6.0](https://github.com/dtan4/k8stail/releases/tag/v0.6.0) (2018-07-29) 7 | 8 | ## Features 9 | 10 | - Enable external auth provider [#40](https://github.com/dtan4/k8stail/pull/40) 11 | 12 | ## Others 13 | 14 | - Use Go 1.10.3 on Travis CI [#42](https://github.com/dtan4/k8stail/pull/42) 15 | - Upgrade to client-go 8.0.0 [#39](https://github.com/dtan4/k8stail/pull/39) 16 | 17 | # [v0.5.2.rc1](https://github.com/dtan4/k8stail/releases/tag/v0.5.2.rc1) (2017-04-27) 18 | 19 | ## Features 20 | 21 | - Add debug flag to enable pprof [#31](https://github.com/dtan4/k8stail/pull/31) 22 | 23 | ## Fixed 24 | 25 | - Close goroutine immediately if there is no valid object [#30](https://github.com/dtan4/k8stail/pull/30) 26 | 27 | # [v0.5.1](https://github.com/dtan4/k8stail/releases/tag/v0.5.1) (2017-04-27) 28 | 29 | ## Fixed 30 | 31 | - Print selected context correctly [#28](https://github.com/dtan4/k8stail/pull/28) 32 | 33 | # [v0.5.0](https://github.com/dtan4/k8stail/releases/tag/v0.5.0) (2017-04-25) 34 | 35 | ## Features 36 | 37 | - Watch Kubernetes events to detect Pod lifecycle correctly [#26](https://github.com/dtan4/k8stail/pull/26) 38 | - Add more short flags [#25](https://github.com/dtan4/k8stail/pull/25) (thanks @atombender) 39 | 40 | # [v0.4.0](https://github.com/dtan4/k8stail/releases/tag/v0.4.0) (2017-04-11) 41 | 42 | ## Features 43 | 44 | - Use default namespace set in kubecfg [#21](https://github.com/dtan4/k8stail/pull/21) 45 | - Add `--no-halt` flag [#18](https://github.com/dtan4/k8stail/pull/18) 46 | 47 | ## Fixed 48 | 49 | - Detect container recreation [#20](https://github.com/dtan4/k8stail/pull/20) 50 | 51 | # [v0.3.0](https://github.com/dtan4/k8stail/releases/tag/v0.3.0) (2016-12-12) 52 | 53 | ## Backward incompatible changes 54 | 55 | - Deprecate `-flag` style flag, use `--flag` [#11](https://github.com/dtan4/k8stail/pull/11) 56 | 57 | ## Features 58 | 59 | - Support context switch by `--context` flag [#13](https://github.com/dtan4/k8stail/pull/13) (Thanks @apstndb) 60 | 61 | # [v0.2.1](https://github.com/dtan4/k8stail/releases/tag/v0.2.1) (2016-11-16) 62 | 63 | Rebuilt binaries to be statically-linked. 64 | 65 | # [v0.2.0](https://github.com/dtan4/k8stail/releases/tag/v0.2.0) (2016-11-16) 66 | 67 | ## Features 68 | 69 | - Stream logs of all containers in pod [#5](https://github.com/dtan4/k8stail/pull/5) 70 | - Get kubeconfig path from KUBECONFIG [#4](https://github.com/dtan4/k8stail/pull/4) 71 | 72 | # [v0.1.0](https://github.com/dtan4/k8stail/releases/tag/v0.1.0) (2016-11-15) 73 | 74 | Initial release. 75 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24 AS builder 2 | 3 | WORKDIR /go/src/github.com/dtan4/k8stail 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY . . 9 | RUN CGO_ENABLED=0 go build -o /k8stail 10 | 11 | FROM gcr.io/distroless/static:nonroot 12 | 13 | COPY --from=builder /k8stail /k8stail 14 | 15 | ENTRYPOINT ["/k8stail"] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Daisuke Fujita 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME := k8stail 2 | VERSION := $(shell git tag | sort -V -r | head -n1)-next 3 | COMMIT := $(shell git rev-parse HEAD) 4 | DATE := $(shell date "+%Y-%m-%dT%H:%M:%S%z") 5 | 6 | SRCS := $(shell find . -name '*.go' -type f) 7 | LDFLAGS := -ldflags="-s -w -X \"main.version=$(VERSION)\" -X \"main.commit=$(COMMIT)\" -X \"main.date=$(DATE)\"" 8 | 9 | .DEFAULT_GOAL := bin/$(NAME) 10 | 11 | export GO111MODULE=on 12 | 13 | bin/$(NAME): $(SRCS) 14 | go build $(LDFLAGS) -o bin/$(NAME) 15 | 16 | .PHONY: ci-test 17 | ci-test: 18 | go test -coverpkg=./... -coverprofile=coverage.txt -v ./... 19 | 20 | .PHONY: clean 21 | clean: 22 | rm -rf bin/* 23 | 24 | .PHONY: install 25 | install: 26 | go install $(LDFLAGS) 27 | 28 | .PHONY: test 29 | test: 30 | go test -cover -v 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8stail 2 | 3 | [![GitHub Actions](https://github.com/dtan4/k8stail/workflows/Test/badge.svg)](https://github.com/dtan4/k8stail/actions?query=workflow%3ATest+branch%3Amaster) 4 | [![codecov](https://codecov.io/gh/dtan4/k8stail/branch/master/graph/badge.svg)](https://codecov.io/gh/dtan4/k8stail) 5 | [![GitHub release](https://img.shields.io/github/release/dtan4/k8stail.svg)](https://github.com/dtan4/k8stail/releases) 6 | 7 | `tail -f` experience for Kubernetes Pods 8 | 9 | As you know, `kubectl logs` can stream only ONE pod at the same time. `k8stail` enables you to watch __log streams of ALL pods__ in the specified namespace or labels in real time, like `tail -f`. 10 | 11 | ![example](_images/example.png) 12 | 13 | ## Table of Contents 14 | 15 | * [Requirements](#requirements) 16 | * [Installation](#installation) 17 | + [Using Homebrew (OS X only)](#using-homebrew-os-x-only) 18 | + [Precompiled binary](#precompiled-binary) 19 | + [From source](#from-source) 20 | + [Run in a Docker container](#run-in-a-docker-container) 21 | * [Usage](#usage) 22 | + [kubeconfig file](#kubeconfig-file) 23 | + [Options](#options) 24 | * [Development](#development) 25 | * [Author](#author) 26 | * [License](#license) 27 | 28 | ## Requirements 29 | 30 | Kubernetes 1.3 or above 31 | 32 | ## Installation 33 | 34 | ### Using Homebrew (OS X only) 35 | 36 | Formula is available at [dtan4/homebrew-dtan4](https://github.com/dtan4/homebrew-dtan4). 37 | 38 | ```bash 39 | $ brew tap dtan4/dtan4 40 | $ brew install k8stail 41 | ``` 42 | 43 | ### Precompiled binary 44 | 45 | Precompiled binaries for Windows, OS X, Linux are available at [Releases](https://github.com/dtan4/k8stail/releases). 46 | 47 | ### From source 48 | 49 | ```bash 50 | $ go get -d github.com/dtan4/k8stail 51 | $ cd $GOPATH/src/github.com/dtan4/k8stail 52 | $ make deps 53 | $ make install 54 | ``` 55 | 56 | ### Run in a Docker container 57 | 58 | Docker image is no longer provided officially. 59 | If you'd like to run k8sec in Docker image, see [`Dockerfile`](Dockerfile) and build image by yourself. 60 | 61 | ```bash 62 | docker build -t k8stail . 63 | ``` 64 | 65 | ## Usage 66 | 67 | Logs of all pods, all containers in pod in the specified namespace are streaming. When new pod is added, logs of the pod also appears. 68 | To stop streaming and exit, press `Ctrl-C`. 69 | 70 | ```bash 71 | $ k8stail --namespace awesome-app 72 | Namespace: awesome-app 73 | Labels: 74 | ---------- 75 | Pod awesome-app-web-4212725599-67vd4 has detected 76 | Pod awesome-app-web-4212725599-6pduy has detected 77 | Pod awesome-app-web-4212725599-lbuny has detected 78 | Pod awesome-app-web-4212725599-mh3g1 has detected 79 | Pod awesome-app-web-4212725599-pvjsm has detected 80 | [awesome-app-web-4212725599-mh3g1][web] | creating base compositions... 81 | [awesome-app-web-4212725599-zei9h][web] | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) 82 | [awesome-app-web-4212725599-zei9h][web] | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") 83 | [awesome-app-web-4212725599-zei9h][web] | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" 84 | [awesome-app-web-4212725599-zei9h][web] | Migrating to CreatePosts (20160218082522) 85 | ``` 86 | 87 | With `--timestamps` option, log timestamp is printed together. 88 | 89 | 90 | ```bash 91 | $ k8stail --namespace awesome-app --timestamps 92 | Namespace: awesome-app 93 | Labels: 94 | ---------- 95 | Pod awesome-app-web-4212725599-67vd4 has detected 96 | Pod awesome-app-web-4212725599-6pduy has detected 97 | Pod awesome-app-web-4212725599-lbuny has detected 98 | Pod awesome-app-web-4212725599-mh3g1 has detected 99 | Pod awesome-app-web-4212725599-pvjsm has detected 100 | [awesome-app-web-4212725599-mh3g1][web] 2016-11-15T10:57:22.178667425Z | creating base compositions... 101 | [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309011520Z | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) 102 | [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.309053601Z | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") 103 | [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463700110Z | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" 104 | [awesome-app-web-4212725599-zei9h][web] 2016-11-15T10:57:22.463743373Z | Migrating to CreatePosts (20160218082522) 105 | ``` 106 | 107 | With `--labels` option, you can filter pods to watch. 108 | 109 | ```bash 110 | $ k8stail --namespace awesome-app --labels name=awesome-app-web 111 | Namespace: awesome-app 112 | Labels: name=awesome-app-web 113 | ---------- 114 | Pod awesome-app-web-4212725599-67vd4 has detected 115 | Pod awesome-app-web-4212725599-6pduy has detected 116 | Pod awesome-app-web-4212725599-lbuny has detected 117 | Pod awesome-app-web-4212725599-mh3g1 has detected 118 | Pod awesome-app-web-4212725599-pvjsm has detected 119 | [awesome-app-web-4212725599-mh3g1][web] | creating base compositions... 120 | [awesome-app-web-4212725599-zei9h][web] | (47.1ms) CREATE TABLE "schema_migrations" ("version" character varying NOT NULL) 121 | [awesome-app-web-4212725599-zei9h][web] | (45.1ms) CREATE UNIQUE INDEX "unique_schema_migrations" ON "schema_migrations" ("version") 122 | [awesome-app-web-4212725599-zei9h][web] | ActiveRecord::SchemaMigration Load (1.8ms) SELECT "schema_migrations".* FROM "schema_migrations" 123 | [awesome-app-web-4212725599-zei9h][web] | Migrating to CreatePosts (20160218082522) 124 | ``` 125 | 126 | ### kubeconfig file 127 | 128 | `k8stail` uses `~/.kube/config` as default. You can specify another path by `KUBECONFIG` environment variable or `--kubeconfig` option. `--kubeconfig` option always overrides `KUBECONFIG` environment variable. 129 | 130 | ```bash 131 | $ KUBECONFIG=/path/to/kubeconfig k8stail 132 | # or 133 | $ k8stail --kubeconfig=/path/to/kubeconfig 134 | ``` 135 | 136 | ### Options 137 | 138 | |Option|Description|Required|Default| 139 | |---------|-----------|-------|-------| 140 | |`--context=CONTEXT`|Kubernetes context||| 141 | |`--debug`|Debug mode using pprof (http://localhost:6060)||`false`| 142 | |`--kubeconfig=KUBECONFIG`|Path of kubeconfig||`~/.kube/config`| 143 | |`--labels=LABELS`|Label filter query (e.g. `app=APP,role=ROLE`)||| 144 | |`--namespace=NAMESPACE`|Kubernetes namespace||`default`| 145 | |`--timestamps`|Include timestamps on each line||`false`| 146 | |`-h`, `-help`|Print command line usage||| 147 | |`-v`, `-version`|Print version||| 148 | 149 | ## Development 150 | 151 | Go 1.7 or above is required. 152 | 153 | Clone this repository and build using `make`. 154 | 155 | ```bash 156 | $ go get -d github.com/dtan4/k8stail 157 | $ cd $GOPATH/src/github.com/dtan4/k8stail 158 | $ make 159 | ``` 160 | 161 | ## Author 162 | 163 | Daisuke Fujita ([@dtan4](https://github.com/dtan4)) 164 | 165 | ## License 166 | 167 | [![MIT License](http://img.shields.io/badge/license-MIT-blue.svg?style=flat)](LICENSE) 168 | -------------------------------------------------------------------------------- /_images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dtan4/k8stail/4315377bcd5ef024b2ceb3607de636653b303ef8/_images/example.png -------------------------------------------------------------------------------- /counter.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: counter 5 | labels: 6 | app: counter 7 | spec: 8 | replicas: 3 9 | selector: 10 | matchLabels: 11 | app: counter 12 | template: 13 | metadata: 14 | labels: 15 | app: counter 16 | spec: 17 | containers: 18 | - name: counter-a 19 | image: busybox 20 | args: ["/bin/sh", "-c", 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done'] 21 | - name: counter-b 22 | image: busybox 23 | args: ["/bin/sh", "-c", 'i=0; while true; do echo "$i: $(date)"; i=$((i+1)); sleep 1; done'] 24 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/dtan4/k8stail 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/fatih/color v1.18.0 7 | github.com/spf13/pflag v1.0.6 8 | k8s.io/api v0.33.1 9 | k8s.io/apimachinery v0.33.1 10 | k8s.io/client-go v0.33.1 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 15 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 16 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 17 | github.com/go-logr/logr v1.4.2 // indirect 18 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 19 | github.com/go-openapi/jsonreference v0.20.2 // indirect 20 | github.com/go-openapi/swag v0.23.0 // indirect 21 | github.com/gogo/protobuf v1.3.2 // indirect 22 | github.com/google/gnostic-models v0.6.9 // indirect 23 | github.com/google/go-cmp v0.7.0 // indirect 24 | github.com/google/uuid v1.6.0 // indirect 25 | github.com/josharian/intern v1.0.0 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/mailru/easyjson v0.7.7 // indirect 28 | github.com/mattn/go-colorable v0.1.13 // indirect 29 | github.com/mattn/go-isatty v0.0.20 // indirect 30 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 31 | github.com/modern-go/reflect2 v1.0.2 // indirect 32 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 33 | github.com/pkg/errors v0.9.1 // indirect 34 | github.com/x448/float16 v0.8.4 // indirect 35 | golang.org/x/net v0.38.0 // indirect 36 | golang.org/x/oauth2 v0.27.0 // indirect 37 | golang.org/x/sys v0.31.0 // indirect 38 | golang.org/x/term v0.30.0 // indirect 39 | golang.org/x/text v0.23.0 // indirect 40 | golang.org/x/time v0.9.0 // indirect 41 | google.golang.org/protobuf v1.36.5 // indirect 42 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 43 | gopkg.in/inf.v0 v0.9.1 // indirect 44 | gopkg.in/yaml.v3 v3.0.1 // indirect 45 | k8s.io/klog/v2 v2.130.1 // indirect 46 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect 47 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect 48 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect 49 | sigs.k8s.io/randfill v1.0.0 // indirect 50 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect 51 | sigs.k8s.io/yaml v1.4.0 // indirect 52 | ) 53 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 5 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= 7 | github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 8 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 9 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 10 | github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= 11 | github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= 12 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 13 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 14 | github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= 15 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 16 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 17 | github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= 18 | github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= 19 | github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= 20 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 21 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 22 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 23 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 24 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 25 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 26 | github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= 27 | github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= 28 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 29 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 30 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 33 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 37 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 38 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 39 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 40 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 41 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 42 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 43 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 44 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 45 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 46 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 47 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 48 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 49 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 50 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 51 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 52 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 53 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 54 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 55 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 56 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 59 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 60 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 61 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 62 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 63 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 64 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 65 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 66 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 67 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 68 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 69 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 70 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 71 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 72 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 73 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 74 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 75 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 76 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 77 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 78 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 79 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 80 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 81 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 82 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 83 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 84 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 85 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 86 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 87 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 88 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 89 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 90 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 91 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 92 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 93 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 94 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 95 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 96 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 97 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 98 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 99 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 100 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 101 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 102 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 103 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 104 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 105 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 106 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 107 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 108 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 109 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 110 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 111 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 112 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 113 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 114 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 115 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 116 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 117 | golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= 118 | golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= 119 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 120 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 121 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 122 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 123 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 124 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 125 | golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ= 126 | golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= 127 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 128 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 129 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 130 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 131 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 132 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 133 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 134 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 135 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 136 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 137 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 138 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 139 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 140 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 141 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 142 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 143 | k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= 144 | k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= 145 | k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= 146 | k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= 147 | k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= 148 | k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= 149 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 150 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 151 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= 152 | k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= 153 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= 154 | k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 155 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= 156 | sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= 157 | sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 158 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 159 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 160 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= 161 | sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= 162 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 163 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 164 | -------------------------------------------------------------------------------- /logger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "sync" 7 | 8 | "github.com/fatih/color" 9 | ) 10 | 11 | var ( 12 | greenBold = color.New(color.FgGreen, color.Bold) 13 | yellowBold = color.New(color.FgYellow, color.Bold) 14 | redBold = color.New(color.FgRed, color.Bold) 15 | boldFunc = color.New(color.Bold).SprintFunc() 16 | yellowFunc = color.New(color.FgYellow).SprintFunc() 17 | ) 18 | 19 | // Logger represents logger 20 | type Logger struct { 21 | m sync.Mutex 22 | } 23 | 24 | // NewLogger returns new Logger object 25 | func NewLogger() *Logger { 26 | return &Logger{ 27 | m: sync.Mutex{}, 28 | } 29 | } 30 | 31 | // PrintColorizedLog prints log with the given color 32 | func (l *Logger) PrintColorizedLog(c *color.Color, line string) { 33 | l.m.Lock() 34 | defer l.m.Unlock() 35 | 36 | c.Println(line) 37 | } 38 | 39 | // PrintHeader prints header 40 | func (l *Logger) PrintHeader(context, namespace, labels string) { 41 | fmt.Printf("%s %s\n", boldFunc("Context: "), context) 42 | fmt.Printf("%s %s\n", boldFunc("Namespace:"), namespace) 43 | fmt.Printf("%s %s\n", boldFunc("Labels: "), labels) 44 | color.New(color.FgYellow).Println("Press Ctrl-C to exit.") 45 | color.New(color.Bold).Println("----------") 46 | } 47 | 48 | // PrintPlainLog prints log with no cosmetics 49 | func (l *Logger) PrintPlainLog(line string) { 50 | l.m.Lock() 51 | defer l.m.Unlock() 52 | 53 | fmt.Println(line) 54 | } 55 | 56 | // PrintPodDetected prints that Pod was detected 57 | func (l *Logger) PrintPodDetected(pod, container string) { 58 | l.PrintColorizedLog(greenBold, fmt.Sprintf("Pod:%s Container:%s has been detected", pod, container)) 59 | } 60 | 61 | // PrintPodDeleted prints that Pod was finished 62 | func (l *Logger) PrintPodFinished(pod, container string) { 63 | l.PrintColorizedLog(yellowBold, fmt.Sprintf("Pod:%s Container:%s has been finished", pod, container)) 64 | } 65 | 66 | // PrintPodDeleted prints that Pod was deleted 67 | func (l *Logger) PrintPodDeleted(pod, container string) { 68 | l.PrintColorizedLog(redBold, fmt.Sprintf("Pod:%s Container:%s has been deleted", pod, container)) 69 | } 70 | 71 | // PrintPodLog prints Pod log 72 | func (l *Logger) PrintPodLog(pod, container, line string, timestamps bool) { 73 | l.m.Lock() 74 | defer l.m.Unlock() 75 | 76 | if timestamps { 77 | ss := strings.SplitN(line, " ", 2) 78 | fmt.Printf("[%s][%s] %s %s %s \n", boldFunc(pod), boldFunc(container), yellowFunc(ss[0]), boldFunc("|"), ss[1]) 79 | } else { 80 | fmt.Printf("[%s][%s] %s %s\n", boldFunc(pod), boldFunc(container), boldFunc("|"), line) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "math" 8 | "net/http" 9 | _ "net/http/pprof" 10 | "os" 11 | "os/signal" 12 | "time" 13 | 14 | flag "github.com/spf13/pflag" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/client-go/kubernetes" 17 | _ "k8s.io/client-go/plugin/pkg/client/auth" 18 | "k8s.io/client-go/tools/clientcmd" 19 | ) 20 | 21 | const ( 22 | debugAddress = ":6060" 23 | logSecondsOffset = 10 24 | ) 25 | 26 | var ( 27 | sinceSeconds = int64(math.Ceil(float64(logSecondsOffset) / float64(time.Second))) 28 | ) 29 | 30 | func main() { 31 | var ( 32 | debug bool 33 | kubeContext string 34 | kubeconfig string 35 | labels string 36 | namespace string 37 | timestamps bool 38 | version bool 39 | ) 40 | 41 | flags := flag.NewFlagSet("k8stail", flag.ExitOnError) 42 | flags.Usage = func() { 43 | flags.PrintDefaults() 44 | } 45 | 46 | flags.StringVar(&kubeContext, "context", "", "Kubernetes context") 47 | flags.BoolVar(&debug, "debug", false, "Debug mode using pprof (http://localhost:6060)") 48 | flags.StringVar(&kubeconfig, "kubeconfig", "", "Path of kubeconfig") 49 | flags.StringVarP(&labels, "labels", "l", "", "Label filter query") 50 | flags.StringVarP(&namespace, "namespace", "n", "", "Kubernetes namespace") 51 | flags.BoolVarP(×tamps, "timestamps", "t", false, "Include timestamps on each line") 52 | flags.BoolVarP(&version, "version", "v", false, "Print version") 53 | 54 | if err := flags.Parse(os.Args[1:]); err != nil { 55 | fmt.Fprintln(os.Stderr, err) 56 | os.Exit(1) 57 | } 58 | 59 | if kubeconfig == "" { 60 | if os.Getenv("KUBECONFIG") != "" { 61 | kubeconfig = os.Getenv("KUBECONFIG") 62 | } else { 63 | kubeconfig = clientcmd.RecommendedHomeFile 64 | } 65 | } 66 | 67 | if version { 68 | printVersion() 69 | os.Exit(0) 70 | } 71 | 72 | if debug { 73 | go func() { 74 | log.Println(http.ListenAndServe(debugAddress, nil)) 75 | }() 76 | } 77 | 78 | clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( 79 | &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, 80 | &clientcmd.ConfigOverrides{CurrentContext: kubeContext}) 81 | 82 | config, err := clientConfig.ClientConfig() 83 | if err != nil { 84 | fmt.Fprintln(os.Stderr, err) 85 | os.Exit(1) 86 | } 87 | 88 | clientset, err := kubernetes.NewForConfig(config) 89 | if err != nil { 90 | fmt.Fprintln(os.Stderr, err) 91 | os.Exit(1) 92 | } 93 | 94 | rawConfig, err := clientConfig.RawConfig() 95 | if err != nil { 96 | fmt.Fprintln(os.Stderr, err) 97 | os.Exit(1) 98 | } 99 | 100 | var currentContext string 101 | 102 | if kubeContext == "" { 103 | currentContext = rawConfig.CurrentContext 104 | } else { 105 | currentContext = kubeContext 106 | } 107 | 108 | if namespace == "" { 109 | if rawConfig.Contexts[currentContext].Namespace == "" { 110 | namespace = metav1.NamespaceDefault 111 | } else { 112 | namespace = rawConfig.Contexts[currentContext].Namespace 113 | } 114 | } 115 | 116 | sigCh := make(chan os.Signal, 1) 117 | signal.Notify(sigCh, os.Interrupt) 118 | 119 | logger := NewLogger() 120 | logger.PrintHeader(currentContext, namespace, labels) 121 | 122 | ctx, cancel := context.WithCancel(context.Background()) 123 | defer cancel() 124 | 125 | watcher, err := clientset.CoreV1().Pods(namespace).Watch(ctx, metav1.ListOptions{ 126 | LabelSelector: labels, 127 | }) 128 | if err != nil { 129 | fmt.Fprintln(os.Stderr, err) 130 | os.Exit(1) 131 | } 132 | 133 | added, finished, deleted := Watch(ctx, watcher) 134 | 135 | tails := NewTailMap() 136 | 137 | go func() { 138 | for target := range added { 139 | id := target.GetID() 140 | 141 | if _, ok := tails.Get(id); ok { 142 | continue 143 | } 144 | 145 | tail := NewTail(target.Namespace, target.Pod, target.Container, logger, sinceSeconds, timestamps) 146 | tails.Set(id, tail) 147 | tail.Start(ctx, clientset) 148 | } 149 | }() 150 | 151 | go func() { 152 | for target := range finished { 153 | id := target.GetID() 154 | 155 | t, ok := tails.Get(id) 156 | if !ok { 157 | continue 158 | } 159 | 160 | if t.Finished { 161 | continue 162 | } 163 | 164 | t.Finish() 165 | 166 | tails.Delete(id) 167 | } 168 | }() 169 | 170 | go func() { 171 | for target := range deleted { 172 | id := target.GetID() 173 | 174 | t, ok := tails.Get(id) 175 | if !ok { 176 | continue 177 | } 178 | 179 | t.Delete() 180 | 181 | tails.Delete(id) 182 | } 183 | }() 184 | 185 | <-sigCh 186 | cancel() 187 | } 188 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "schedule:weekly" 6 | ], 7 | "postUpdateOptions": [ 8 | "gomodTidy" 9 | ], 10 | "packageRules": [ 11 | { 12 | "matchDatasources": ["golang-version"], 13 | "rangeStrategy": "bump" 14 | }, 15 | { 16 | "groupName": "all non-major Go dependencies", 17 | "groupSlug": "all-minor-patch-gomod", 18 | "matchManagers": [ 19 | "gomod" 20 | ], 21 | "matchUpdateTypes": [ 22 | "minor", 23 | "patch", 24 | "pin", 25 | "digest" 26 | ], 27 | "automerge": true, 28 | "automergeType": "branch", 29 | "matchPackageNames": [ 30 | "!go" 31 | ] 32 | }, 33 | { 34 | "groupName": "all non-major GitHub Actions dependencies", 35 | "groupSlug": "all-minor-patch-github-actions", 36 | "matchManagers": [ 37 | "github-actions" 38 | ], 39 | "matchUpdateTypes": [ 40 | "minor", 41 | "patch", 42 | "pin", 43 | "digest" 44 | ], 45 | "automerge": true, 46 | "automergeType": "branch" 47 | }, 48 | { 49 | "matchUpdateTypes": [ 50 | "minor", 51 | "patch", 52 | "pin", 53 | "digest" 54 | ], 55 | "automerge": true, 56 | "automergeType": "branch" 57 | } 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /tail.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "fmt" 7 | "os" 8 | "sync" 9 | 10 | v1 "k8s.io/api/core/v1" 11 | "k8s.io/client-go/kubernetes" 12 | ) 13 | 14 | type Tail struct { 15 | Finished bool 16 | closed chan struct{} 17 | logger *Logger 18 | namespace string 19 | pod string 20 | container string 21 | sinceSeconds int64 22 | timestamps bool 23 | } 24 | 25 | // NewTail creates new Tail object 26 | func NewTail(namespace, pod, container string, logger *Logger, sinceSeconds int64, timestamps bool) *Tail { 27 | return &Tail{ 28 | Finished: false, 29 | closed: make(chan struct{}), 30 | logger: logger, 31 | namespace: namespace, 32 | pod: pod, 33 | container: container, 34 | sinceSeconds: sinceSeconds, 35 | timestamps: timestamps, 36 | } 37 | } 38 | 39 | // Start starts Pod log streaming 40 | func (t *Tail) Start(ctx context.Context, clientset *kubernetes.Clientset) { 41 | t.logger.PrintPodDetected(t.pod, t.container) 42 | 43 | go func() { 44 | rs, err := clientset.CoreV1().Pods(t.namespace).GetLogs(t.pod, &v1.PodLogOptions{ 45 | Container: t.container, 46 | Follow: true, 47 | SinceSeconds: &t.sinceSeconds, 48 | Timestamps: t.timestamps, 49 | }).Stream(ctx) 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, err) 52 | return 53 | } 54 | defer rs.Close() 55 | 56 | go func() { 57 | <-t.closed 58 | rs.Close() 59 | }() 60 | 61 | sc := bufio.NewScanner(rs) 62 | 63 | for sc.Scan() { 64 | t.logger.PrintPodLog(t.pod, t.container, sc.Text(), t.timestamps) 65 | } 66 | }() 67 | 68 | go func() { 69 | <-ctx.Done() 70 | close(t.closed) 71 | }() 72 | } 73 | 74 | // Finish finishes Pod log streaming with Pod completion 75 | func (t *Tail) Finish() { 76 | t.logger.PrintPodFinished(t.pod, t.container) 77 | t.Finished = true 78 | } 79 | 80 | // Delete finishes Pod log streaming with Pod deletion 81 | func (t *Tail) Delete() { 82 | t.logger.PrintPodDeleted(t.pod, t.container) 83 | close(t.closed) 84 | } 85 | 86 | type TailMap struct { 87 | mu sync.Mutex 88 | 89 | data map[string]*Tail 90 | } 91 | 92 | func NewTailMap() *TailMap { 93 | return &TailMap{ 94 | data: make(map[string]*Tail), 95 | } 96 | } 97 | 98 | func (m *TailMap) Get(k string) (*Tail, bool) { 99 | m.mu.Lock() 100 | defer m.mu.Unlock() 101 | 102 | d, ok := m.data[k] 103 | 104 | return d, ok 105 | } 106 | 107 | func (m *TailMap) Set(k string, v *Tail) { 108 | m.mu.Lock() 109 | defer m.mu.Unlock() 110 | 111 | m.data[k] = v 112 | } 113 | 114 | func (m *TailMap) Delete(k string) { 115 | m.mu.Lock() 116 | defer m.mu.Unlock() 117 | 118 | delete(m.data, k) 119 | } 120 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | var ( 8 | version string 9 | commit string 10 | date string 11 | ) 12 | 13 | func printVersion() { 14 | fmt.Printf("k8stail version: %s, commit: %s, build at: %s\n", version, commit, date) 15 | } 16 | -------------------------------------------------------------------------------- /watch.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "k8s.io/api/core/v1" 7 | "k8s.io/apimachinery/pkg/watch" 8 | ) 9 | 10 | type Target struct { 11 | Namespace string 12 | Pod string 13 | Container string 14 | } 15 | 16 | // NewTarget creates new Target object 17 | func NewTarget(namespace, pod, container string) *Target { 18 | return &Target{ 19 | Namespace: namespace, 20 | Pod: pod, 21 | Container: container, 22 | } 23 | } 24 | 25 | // GetID returns target ID 26 | func (t *Target) GetID() string { 27 | return t.Namespace + "_" + t.Pod + "_" + t.Container 28 | } 29 | 30 | // Watch starts and listens Kubernetes Pod events 31 | func Watch(ctx context.Context, watcher watch.Interface) (chan *Target, chan *Target, chan *Target) { 32 | added := make(chan *Target) 33 | finished := make(chan *Target) 34 | deleted := make(chan *Target) 35 | 36 | go func() { 37 | for { 38 | select { 39 | case e := <-watcher.ResultChan(): 40 | if e.Object == nil { 41 | return 42 | } 43 | 44 | pod := e.Object.(*v1.Pod) 45 | 46 | switch e.Type { 47 | case watch.Added: 48 | if pod.Status.Phase != v1.PodRunning { 49 | continue 50 | } 51 | 52 | for _, container := range pod.Spec.Containers { 53 | added <- NewTarget(pod.Namespace, pod.Name, container.Name) 54 | } 55 | case watch.Modified: 56 | switch pod.Status.Phase { 57 | case v1.PodRunning: 58 | for _, container := range pod.Spec.Containers { 59 | added <- NewTarget(pod.Namespace, pod.Name, container.Name) 60 | } 61 | case v1.PodSucceeded, v1.PodFailed: 62 | for _, container := range pod.Spec.Containers { 63 | finished <- NewTarget(pod.Namespace, pod.Name, container.Name) 64 | } 65 | } 66 | case watch.Deleted: 67 | for _, container := range pod.Spec.Containers { 68 | deleted <- NewTarget(pod.Namespace, pod.Name, container.Name) 69 | } 70 | } 71 | 72 | case <-ctx.Done(): 73 | watcher.Stop() 74 | return 75 | } 76 | } 77 | }() 78 | 79 | return added, finished, deleted 80 | } 81 | --------------------------------------------------------------------------------