Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Factory:Rebuild
stern
stern-1.30.0.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File stern-1.30.0.obscpio of Package stern
07070100000000000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001500000000stern-1.30.0/.github07070100000001000081A4000000000000000000000001664F523B0000002B000000000000000000000000000000000000002000000000stern-1.30.0/.github/CODEOWNERS* @superbrothers @floryut @rkmathi @tksm 07070100000002000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000002400000000stern-1.30.0/.github/ISSUE_TEMPLATE07070100000003000081A4000000000000000000000001664F523B00000183000000000000000000000000000000000000003200000000stern-1.30.0/.github/ISSUE_TEMPLATE/bug-report.md--- name: Bug Report about: Report a bug encountered while using stern labels: kind/bug --- **What happened**: **What you expected to happen**: **How to reproduce it (as minimally and precisely as possible)**: **Anything else we need to know?**: **Environment**: - stern version (use `stern --version`): - OS (e.g: `cat /etc/os-release`): - Install tools (e.g: Homebrew) - Others: 07070100000004000081A4000000000000000000000001664F523B00000094000000000000000000000000000000000000003300000000stern-1.30.0/.github/ISSUE_TEMPLATE/enhancement.md--- name: Enhancement Request about: Suggest an enhancement labels: kind/feature --- **What would you like to be added**: **Why is this needed**: 07070100000005000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001F00000000stern-1.30.0/.github/workflows07070100000006000081A4000000000000000000000001664F523B000002E8000000000000000000000000000000000000002700000000stern-1.30.0/.github/workflows/ci.yamlname: CI on: # Must keep in sync with ci_for_skipped.yaml push: branches: [master] paths-ignore: ['**.md'] pull_request: types: [opened, synchronize] paths-ignore: ['**.md'] jobs: run: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Ensure go.mod is already tidied run: go mod tidy && git diff -s --exit-code go.sum - name: Run verify-readme run: make verify-readme - name: Run tests run: make test - name: Build binary run: make build - name: Make dist file run: make dist - name: Validate a krew plugin manifest file run: make validate-krew-manifest 07070100000007000081A4000000000000000000000001664F523B000000F9000000000000000000000000000000000000003300000000stern-1.30.0/.github/workflows/ci_for_skipped.yaml# CI for skipped files name: CI on: push: branches: [master] paths: ['**.md'] pull_request: types: [opened, synchronize] paths: ['**.md'] jobs: run: runs-on: ubuntu-latest steps: - run: 'echo "No check required"' 07070100000008000081A4000000000000000000000001664F523B0000029C000000000000000000000000000000000000002C00000000stern-1.30.0/.github/workflows/release.yamlname: Release on: push: tags: ["v*"] jobs: run: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Login to GitHub Container Registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Release run: make release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Update new version in krew-index uses: rajatjindal/krew-release-bot@v0.0.46 with: krew_template_file: dist/krew/stern.yaml 07070100000009000081A4000000000000000000000001664F523B0000002B000000000000000000000000000000000000001800000000stern-1.30.0/.gitignore/dist /hack/tools/bin vendor .idea .vscode 0707010000000A000081A4000000000000000000000001664F523B000000A1000000000000000000000000000000000000001B00000000stern-1.30.0/.golangci.ymlrun: timeout: 5m linters: disable-all: true enable: - errcheck - gofmt - gosimple - govet - ineffassign - staticcheck - typecheck - unused 0707010000000B000081A4000000000000000000000001664F523B00000620000000000000000000000000000000000000001E00000000stern-1.30.0/.goreleaser.yamlbuilds: - env: - CGO_ENABLED=0 ldflags: - -s - -w - -X github.com/stern/stern/cmd.version={{.Version}} - -X github.com/stern/stern/cmd.commit={{.Commit}} - -X github.com/stern/stern/cmd.date={{.Date}} goos: - linux - windows - darwin goarch: - amd64 - arm - arm64 archives: - builds: - stern name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" format: tar.gz files: - LICENSE wrap_in_directory: false checksum: name_template: 'checksums.txt' changelog: sort: asc dockers: - image_templates: - "ghcr.io/stern/stern:latest" - "ghcr.io/stern/stern:{{ .Major }}" - "ghcr.io/stern/stern:{{ .Major }}.{{ .Minor }}" - "ghcr.io/stern/stern:{{ .Major }}.{{ .Minor }}.{{ .Patch }}" krews: - skip_upload: true homepage: https://github.com/stern/stern description: | Stern allows you to `tail` multiple pods on Kubernetes and multiple containers within the pod. Each result is color coded for quicker debugging. The query is a regular expression so the pod name can easily be filtered and you don't need to specify the exact id (for instance omitting the deployment id). If a pod is deleted it gets removed from tail and if a new pod is added it automatically gets tailed. When a pod contains multiple containers Stern can tail all of them too without having to do this manually for each one. Simply specify the `container` flag to limit what containers to show. By default all containers are listened to. short_description: Multi pod and container log tailing 0707010000000C000081A4000000000000000000000001664F523B00004D5F000000000000000000000000000000000000001A00000000stern-1.30.0/CHANGELOG.md# v1.30.0 ## :zap: Notable Changes ### Add support for configuring colors for pods and containers You can now configure highlight colors for pods and containers in [the config file](https://github.com/stern/stern/blob/master/README.md#config-file) using a comma-separated list of [SGR (Select Graphic Rendition) sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters). See the ["Customize highlight colors" section](https://github.com/stern/stern/blob/master/README.md#customize-highlight-colors) for details. Example configuration: ```yaml # Green, Yellow, Blue, Magenta, Cyan, White pod-colors: "32,33,34,35,36,37" # Colors with underline (4) # If empty, the pod colors will be used as container colors container-colors: "32;4,33;4,34;4,35;4,36;4,37;4" ``` ### Display different colors for different containers A new `--diff-container` flag allows displaying different colors for different containers. This is useful when you want to debug logs for multiple containers in the same pod. You can also enable this feature in [the config file](https://github.com/stern/stern/blob/master/README.md#config-file). ```yaml diff-container: true ``` ### Changes * Add support to configure colors for pods and containers ([#306](https://github.com/stern/stern/pull/306)) [f4b2edc](https://github.com/stern/stern/commit/f4b2edc) (Takashi Kusumi) * Display different colors for different containers ([#305](https://github.com/stern/stern/pull/305)) [d1b5d74](https://github.com/stern/stern/commit/d1b5d74) (Se7en) * Support an array value in the config file ([#303](https://github.com/stern/stern/pull/303)) [6afabde](https://github.com/stern/stern/commit/6afabde) (Takashi Kusumi) # v1.29.0 ## :zap: Notable Changes ### A new `--stdin` flag for parsing logs from stdin A new `--stdin` flag has been added, allowing parsing logs from stdin. This flag is helpful when applying the same template to local logs. ``` stern --stdin --template \ '{{with $msg := .Message | tryParseJSON}}{{toTimestamp $msg.ts "01-02 15:04:05" "Asia/Tokyo"}} {{$msg.msg}}{{"\n"}}{{end}}' \ <etcd.log ``` Additionally, this feature helps test your template with arbitrary logs. ``` stern --stdin --template \ '{{with $msg := .Message | tryParseJSON}}{{levelColor $msg.level}} {{$msg.msg}}{{"\n"}}{{end}}' <<EOF {"level":"info","msg":"info message"} {"level":"error","msg":"error message"} EOF ``` ### Add support for UNIX time with nanoseconds to template functions The following template functions now support UNIX time seconds with nanoseconds (e.g., `1136171056.02`). - `toRFC3339Nano` - `toTUC` - `toTimestamp` ## Changes * Add support for UNIX time with nanoseconds to template functions ([#300](https://github.com/stern/stern/pull/300)) 0d580ff (Takashi Kusumi) * Clarify that '=' cannot be omitted in --timestamps ([#296](https://github.com/stern/stern/pull/296)) ac36420 (Takashi Kusumi) * Added example to README ([#295](https://github.com/stern/stern/pull/295)) c1649ca (Thomas Güttler) * Update dependencies for Kubernetes 1.30 ([#293](https://github.com/stern/stern/pull/293)) d82cc9f (Kazuki Suda) * Add `--stdin` for `stdin` log parsing ([#292](https://github.com/stern/stern/pull/292)) 53fc746 (Jimmie Högklint) # v1.28.0 ## :zap: Notable Changes ### Highlight matched strings in the log lines with the highlight option Some part of a log line can be highlighted while still displaying all other logs lines. `--highlight` flag now highlight matched strings in the log lines. ``` stern --highlight "\[error\]" . ``` # v1.27.0 ## :zap: Notable Changes ### Add new template function: `toTimestamp` The `toTimestamp` function takes in an object, a layout, and optionally a timezone. This allows for more custom time parsing, for instance, if a user doesn't care about seeing the date of the log and only the time (in their own timezone) they can use a template such as: ``` --template '{{ with $msg := .Message | tryParseJSON }}[{{ toTimestamp $msg.time "15:04:05" "Local" }}] {{ $msg.msg }}{{ end }}{{ "\n" }}' ``` ### Add generic kubectl options stern now has the generic options that kubectl has, and a new `--show-hidden-options` option. ``` $ stern --show-hidden-options The following options can also be used in stern: --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. --as-uid string UID to impersonate for the operation. --cache-dir string Default cache directory (default "/home/ksuda/.kube/cache") --certificate-authority string Path to a cert file for the certificate authority --client-certificate string Path to a client certificate file for TLS --client-key string Path to a client key file for TLS --cluster string The name of the kubeconfig cluster to use --disable-compression If true, opt-out of response compression for all requests to the server --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server --user string The name of the kubeconfig user to use ``` The number of kubectl generic options is so large that it makes it difficult to see stern's own list of options, so we usually hide them. Use `--show-hidden-options` if you want to list. ## Changes * Add generic cli options (#283) f315819 (Kazuki Suda) * 281: Support toTimestamp template function (#282) 5445cd5 (Will Anderson) # v1.26.0 ## :zap: Notable Changes ### Add new template functions The following template functions have been added in v1.26.0: - `extractJSONParts`: Parse string as JSON and concatenate the given keys - `tryExtractJSONParts`: Attempt to parse string as JSON and concatenate the given keys, returning text on failure ## Changes * Fix the release workflow ([#275](https://github.com/stern/stern/pull/275)) [91d4cd6](https://github.com/stern/stern/commit/91d4cd6) (Kazuki Suda) * Update dependencies and tools ([#273](https://github.com/stern/stern/pull/273)) [cb94677](https://github.com/stern/stern/commit/cb94677) (Takashi Kusumi) * Possibility to extract parts of a json-message. ([#271](https://github.com/stern/stern/pull/271)) [d49142c](https://github.com/stern/stern/commit/d49142c) (Niels) * Fix potential panic in stern.Run() ([#267](https://github.com/stern/stern/pull/267)) [dcba2dd](https://github.com/stern/stern/commit/dcba2dd) (Takashi Kusumi) * Add log level color keys and handle default ([#264](https://github.com/stern/stern/pull/264)) [65204cc](https://github.com/stern/stern/commit/65204cc) (Jimmie Högklint) * Fix typo in README.md ([#261](https://github.com/stern/stern/pull/261)) [d7d5a4f](https://github.com/stern/stern/commit/d7d5a4f) (Will May) * Integrate fmt and vet checks into golangci-lint ([#260](https://github.com/stern/stern/pull/260)) [1d242bc](https://github.com/stern/stern/commit/1d242bc) (Takashi Kusumi) * Update Github Actions dependencies ([#259](https://github.com/stern/stern/pull/259)) [9e833da](https://github.com/stern/stern/commit/9e833da) (Takashi Kusumi) # v1.25.0 ## :zap: Notable Changes ### Add support for the config file You can now use the config file to change the default values of stern options. The default config file path is `~/.config/stern/config.yaml`. ```yaml # <flag name>: <value> tail: 10 max-log-requests: 999 timestamps: short ``` You can change the config file path with `--config` flag or `STERNCONFIG` environment variable. ## Changes * Fix the heading level in README.md ([#257](https://github.com/stern/stern/pull/257)) [c2290b4](https://github.com/stern/stern/commit/c2290b4) (Kazuki Suda) * Update dependencies and tools ([#256](https://github.com/stern/stern/pull/256)) [531f869](https://github.com/stern/stern/commit/531f869) (Kazuki Suda) * Allow an empty config file ([#255](https://github.com/stern/stern/pull/255)) [c76ea87](https://github.com/stern/stern/commit/c76ea87) (Takashi Kusumi) * Add support for the config file ([#254](https://github.com/stern/stern/pull/254)) [2fdc298](https://github.com/stern/stern/commit/2fdc298) (Kazuki Suda) * Make setup-go get Go version from go.mod ([#253](https://github.com/stern/stern/pull/253)) [23feff7](https://github.com/stern/stern/commit/23feff7) (Takashi Kusumi) # v1.24.0 ## :zap: Notable Changes ### Add a short format for timestamps `--timestamps` flag now accepts a format, one of `default` or `short`. - `default`: the original format `2006-01-02T15:04:05.000000000Z07:00` (RFC3339Nano with trailing zeros) - `short`: the new format `01-02 15:04:05` (time.DateTime without year). If `--timestamps` is specified but without value, `default` is used to maintain backward compatibility. ``` $ stern --timestamps=short -n kube-system ds/kindnet --no-follow --tail 1 --only-log-lines kindnet-hqn2k kindnet-cni 03-12 09:29:53 I0312 00:29:53.620499 1 main.go:250] Node kind-worker3 has CIDR [10.244.1.0/24] kindnet-5f4ms kindnet-cni 03-12 09:29:53 I0312 00:29:53.374482 1 main.go:250] Node kind-worker3 has CIDR [10.244.1.0/24] ``` ### Add `--node` flag to filter on a specific node New `--node` flag allows you to filter pods on a specific node. This flag will be helpful when we debug pods on the specific node. ``` # Print a DaemonSet pod on the specific node stern --node <NODE_NAME> daemonsets/<DS_NAME> # Print all pods on the specific node stern --node <NODE_NAME> --all-namespaces --no-follow --max-log-requests 1 . ``` ### Highlight matched strings in the log lines with the include option `--include` flag now highlight matched strings in the log lines. ``` stern --include "\[error\]" . ``` ### Add `all` option to `--container-state` flag `--container-state` flag now accepts `all` that is the same with specifying `running,waiting,terminated`. This change is helpful when we debug CrashLoopBackoff containers. ``` # Before stern --container-state running,terminated,running <QUERY> # After stern --container-state all <QUERY>` ``` ## :warning: Breaking Changes ### Add `--max-log-requests` flag to limit concurrent requests New `--max-log-requests` flag allows you to limit concurrent requests to prevent unintentional load to a cluster. The behavior and the default are different depending on the presence of the `--no-follow` flag. | `--no-follow` | default | behavior | |---------------|---------|------------------| | specified | 5 | limits the number of concurrent logs to request | | not specified | 50 | exits with an error when if it reaches the concurrent limit | If you want to change to the same behavior as before, specify a sufficiently large value for `--max-log-requests`. ### Change the default of `--container-state` flag to `all` The default value of `--container-state` has been changed to `all` from `running`. With this change, stern will now show logs of completed (`terminated`) and CrashLoopBackoff (`waiting`) pods in addition to running pods by default. If you want to change to the same behavior as before, explicitly specify `--container-state` to `running`. ## Changes * Upgrade golang.org/x/net to fix a dependabot alert ([#250](https://github.com/stern/stern/pull/250)) [e26d049](https://github.com/stern/stern/commit/e26d049) (Kazuki Suda) * Add a short format for timestamps ([#249](https://github.com/stern/stern/pull/249)) [43ab3f1](https://github.com/stern/stern/commit/43ab3f1) (Takashi Kusumi) * Bump golangci-lint to v1.51.2 ([#248](https://github.com/stern/stern/pull/248)) [079d158](https://github.com/stern/stern/commit/079d158) (Takashi Kusumi) * Add dynamic completion for --node flag ([#244](https://github.com/stern/stern/pull/244)) [59d4453](https://github.com/stern/stern/commit/59d4453) (Takashi Kusumi) * Add --node flag to filter on a specific node ([#243](https://github.com/stern/stern/pull/243)) [f90f70f](https://github.com/stern/stern/commit/f90f70f) (Takashi Kusumi) * allow flexible log parsing and formatting ([#239](https://github.com/stern/stern/pull/239)) [12a55fa](https://github.com/stern/stern/commit/12a55fa) (Dmytro Milinevskyi) * Documenting how to get Bash completion in Krew mode ([#240](https://github.com/stern/stern/pull/240)) [24c8716](https://github.com/stern/stern/commit/24c8716) (Jesse Glick) * Add CI for skipped files ([#241](https://github.com/stern/stern/pull/241)) [7131af2](https://github.com/stern/stern/commit/7131af2) (Takashi Kusumi) * Replace actions/cache with setup-go's cache ([#238](https://github.com/stern/stern/pull/238)) [74952fd](https://github.com/stern/stern/commit/74952fd) (Takashi Kusumi) * Make CI jobs faster ([#237](https://github.com/stern/stern/pull/237)) [4bb340d](https://github.com/stern/stern/commit/4bb340d) (Kazuki Suda) * Refactor options.sternConfig() ([#236](https://github.com/stern/stern/pull/236)) [2315b23](https://github.com/stern/stern/commit/2315b23) (Takashi Kusumi) * Return error when output option is invalid ([#235](https://github.com/stern/stern/pull/235)) [1c5aa2b](https://github.com/stern/stern/commit/1c5aa2b) (Takashi Kusumi) * Refactor template logic ([#233](https://github.com/stern/stern/pull/233)) [371daf1](https://github.com/stern/stern/commit/371daf1) (Takashi Kusumi) * Revert "add support to parse JSON logs ([#228](https://github.com/stern/stern/pull/228))" ([#232](https://github.com/stern/stern/pull/232)) [202f7e8](https://github.com/stern/stern/commit/202f7e8) (Dmytro Milinevskyi) * Change the default of --container-state to `all` ([#225](https://github.com/stern/stern/pull/225)) [2502c91](https://github.com/stern/stern/commit/2502c91) (Takashi Kusumi) * Highlight matched strings in the log lines with the include option ([#231](https://github.com/stern/stern/pull/231)) [9fbaa18](https://github.com/stern/stern/commit/9fbaa18) (Kazuki Suda) * Support resuming from the last log when retrying ([#230](https://github.com/stern/stern/pull/230)) [52894f8](https://github.com/stern/stern/commit/52894f8) (Takashi Kusumi) * add support to parse JSON logs ([#228](https://github.com/stern/stern/pull/228)) [72a5854](https://github.com/stern/stern/commit/72a5854) (Dmytro Milinevskyi) * Show initContainers first when --no-follow and --max-log-requests 1 ([#226](https://github.com/stern/stern/pull/226)) [ef753f1](https://github.com/stern/stern/commit/ef753f1) (Takashi Kusumi) * Add --max-log-requests flag to limit concurrent requests ([#224](https://github.com/stern/stern/pull/224)) [0b939c5](https://github.com/stern/stern/commit/0b939c5) (Takashi Kusumi) * Improve handling of container termination ([#221](https://github.com/stern/stern/pull/221)) [8312782](https://github.com/stern/stern/commit/8312782) (Takashi Kusumi) * Allow pods without labels to be selected in the resource query ([#223](https://github.com/stern/stern/pull/223)) [fc51906](https://github.com/stern/stern/commit/fc51906) (Takashi Kusumi) * Add `all` option to --container-state flag ([#222](https://github.com/stern/stern/pull/222)) [6e0d5fc](https://github.com/stern/stern/commit/6e0d5fc) (Takashi Kusumi) # v1.23.0 ## New features ### Add `--no-follow` flag to exit when all logs have been shown New `--no-follow` flag allows you to exit when all logs have been shown. ``` stern --no-follow . ``` ### Support `<resource>/<name>` form as a query Stern now supports a Kubernetes resource query in the form `<resource>/<name>`. Pod query can still be used. ``` stern deployment/nginx ``` The following Kubernetes resources are supported: - daemonset - deployment - job - pod - replicaset - replicationcontroller - service - statefulset Shell completion of stern already supports this feature. ### Add --verbosity flag to set log level verbosity New `--verbosity` flag allows you to set the log level verbosity of Kubernetes client-go. This feature is useful when you want to know how stern interacts with a Kubernetes API server in troubleshooting. ``` stern --verbosity=6 . ``` ### Add --only-log-lines flag to print only log lines New `--only-log-lines` flag allows you to print only log lines (and errors if occur). The difference between not specifying the flag and specifying it is as follows: ``` $ stern . --tail=1 --no-follow + nginx-cfbcb7b98-96xsv › nginx + nginx-cfbcb7b98-29wn7 › nginx nginx-cfbcb7b98-96xsv nginx 2023/01/27 13:20:48 [notice] 1#1: start worker process 46 - nginx-cfbcb7b98-96xsv › nginx nginx-cfbcb7b98-29wn7 nginx 2023/01/27 13:20:45 [notice] 1#1: start worker process 46 - nginx-cfbcb7b98-29wn7 › nginx $ stern . --tail=1 --no-follow --only-log-lines nginx-cfbcb7b98-96xsv nginx 2023/01/27 13:20:48 [notice] 1#1: start worker process 46 nginx-cfbcb7b98-29wn7 nginx 2023/01/27 13:20:45 [notice] 1#1: start worker process 46 ``` ## Changes * Allow to specify --exclude-pod/container multiple times ([#218](https://github.com/stern/stern/pull/218)) [b04478c](https://github.com/stern/stern/commit/b04478c) (Kazuki Suda) * Add --only-log-lines flag that prints only log lines ([#216](https://github.com/stern/stern/pull/216)) [995be39](https://github.com/stern/stern/commit/995be39) (Kazuki Suda) * Fix typo of --verbosity flag ([#215](https://github.com/stern/stern/pull/215)) [6c6db1d](https://github.com/stern/stern/commit/6c6db1d) (Takashi Kusumi) * Add --verbosity flag to set log level verbosity ([#214](https://github.com/stern/stern/pull/214)) [5327626](https://github.com/stern/stern/commit/5327626) (Takashi Kusumi) * Add completion for flags with pre-defined choices ([#211](https://github.com/stern/stern/pull/211)) [e03646c](https://github.com/stern/stern/commit/e03646c) (Takashi Kusumi) * Fix bug where container-state is ignored when no-follow specified ([#210](https://github.com/stern/stern/pull/210)) [1bbee8c](https://github.com/stern/stern/commit/1bbee8c) (Takashi Kusumi) * Add dynamic completion for a resource query ([#209](https://github.com/stern/stern/pull/209)) [2983c8f](https://github.com/stern/stern/commit/2983c8f) (Takashi Kusumi) * Support `<resource>/<name>` form as a query ([#208](https://github.com/stern/stern/pull/208)) [7bc45f0](https://github.com/stern/stern/commit/7bc45f0) (Takashi Kusumi) * Fix indent in update-readme.go ([#207](https://github.com/stern/stern/pull/207)) [daf2464](https://github.com/stern/stern/commit/daf2464) (Takashi Kusumi) * Update dependencies and tools ([#205](https://github.com/stern/stern/pull/205)) [1bcb576](https://github.com/stern/stern/commit/1bcb576) (Kazuki Suda) * Add --no-follow flag to exit when all logs have been shown ([#204](https://github.com/stern/stern/pull/204)) [a5e581d](https://github.com/stern/stern/commit/a5e581d) (Takashi Kusumi) * Use StringArrayVarP for --include and --exclude flags ([#196](https://github.com/stern/stern/pull/196)) [80a68a9](https://github.com/stern/stern/commit/80a68a9) (partcyborg) * Fix the invalid command in README.md ([#193](https://github.com/stern/stern/pull/193)) [f6e76ba](https://github.com/stern/stern/commit/f6e76ba) (Kazuki Suda) 0707010000000D000081A4000000000000000000000001664F523B00000393000000000000000000000000000000000000001D00000000stern-1.30.0/CONTRIBUTING.md# Contributing If you want to submit a pull request to fix a bug or enhance an existing feature, please first open an issue and link to that issue when you submit your pull request. If you have any questions about a possible submission, feel free to open an issue too. ### Pull request process 1. Fork this repository 1. Create a branch in your fork to implement the changes. We recommend using the issue number as part of your branch name, e.g. `1234-fixes` 1. Ensure that any documentation is updated with the changes that are required by your fix. 1. Ensure that any samples are updated if the base image has been changed. 1. Submit the pull request. *Do not leave the pull request blank*. Explain exactly what your changes are meant to do and provide simple steps on how to validate your changes. Ensure that you reference the issue you created as well. The pull request will be review before it is merged. 0707010000000E000081A4000000000000000000000001664F523B000000AB000000000000000000000000000000000000001800000000stern-1.30.0/DockerfileFROM gcr.io/distroless/static-debian10 LABEL org.opencontainers.image.source https://github.com/stern/stern COPY stern /usr/local/bin/ ENTRYPOINT ["/usr/local/bin/stern"] 0707010000000F000081A4000000000000000000000001664F523B00002C5D000000000000000000000000000000000000001500000000stern-1.30.0/LICENSE Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 07070100000010000081A4000000000000000000000001664F523B0000084C000000000000000000000000000000000000001600000000stern-1.30.0/MakefileSHELL:=/usr/bin/env bash .PHONY: build build: go build -o dist/stern . TOOLS_BIN_DIR := $(CURDIR)/hack/tools/bin GORELEASER_VERSION ?= v1.25.1 GORELEASER := $(TOOLS_BIN_DIR)/goreleaser GOLANGCI_LINT_VERSION ?= v1.57.2 GOLANGCI_LINT := $(TOOLS_BIN_DIR)/golangci-lint VALIDATE_KREW_MAIFEST_VERSION ?= v0.4.4 VALIDATE_KREW_MAIFEST := $(TOOLS_BIN_DIR)/validate-krew-manifest GORELEASER_FILTER_VERSION ?= v0.3.0 GORELEASER_FILTER := $(TOOLS_BIN_DIR)/goreleaser-filter $(GORELEASER): GOBIN=$(TOOLS_BIN_DIR) go install github.com/goreleaser/goreleaser@$(GORELEASER_VERSION) $(GOLANGCI_LINT): GOBIN=$(TOOLS_BIN_DIR) go install github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) $(VALIDATE_KREW_MAIFEST): GOBIN=$(TOOLS_BIN_DIR) go install sigs.k8s.io/krew/cmd/validate-krew-manifest@$(VALIDATE_KREW_MAIFEST_VERSION) $(GORELEASER_FILTER): GOBIN=$(TOOLS_BIN_DIR) go install github.com/t0yv0/goreleaser-filter@$(GORELEASER_FILTER_VERSION) .PHONY: build-cross build-cross: $(GORELEASER) $(GORELEASER) build --snapshot --clean .PHONY: test test: lint go test -v ./... .PHONY: lint lint: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run .PHONY: lint-fix lint-fix: $(GOLANGCI_LINT) $(GOLANGCI_LINT) run --fix README_FILE ?= ./README.md .PHONY: update-readme update-readme: go run hack/update-readme/update-readme.go $(README_FILE) .PHONY: verify-readme verify-readme: ./hack/verify-readme.sh .PHONY: validate-krew-manifest validate-krew-manifest: $(VALIDATE_KREW_MAIFEST) $(VALIDATE_KREW_MAIFEST) -manifest dist/krew/stern.yaml -skip-install .PHONY: dist dist: $(GORELEASER) $(GORELEASER_FILTER) cat .goreleaser.yaml | $(GORELEASER_FILTER) -goos $(shell go env GOOS) -goarch $(shell go env GOARCH) | $(GORELEASER) release -f- --clean --skip=publish --snapshot .PHONY: dist-all dist-all: $(GORELEASER) $(GORELEASER) release --clean --skip=publish --snapshot .PHONY: release release: $(GORELEASER) $(GORELEASER) release --clean --skip=validate .PHONY: clean clean: clean-tools clean-dist .PHONY: clean-tools clean-tools: rm -rf $(TOOLS_BIN_DIR) .PHONY: clean-dist clean-dist: rm -rf ./dist 07070100000011000081A4000000000000000000000001664F523B00004D39000000000000000000000000000000000000001700000000stern-1.30.0/README.md[![Build](https://github.com/stern/stern/workflows/CI/badge.svg)](https://github.com/stern/stern/actions?query=workflow%3ACI+branch%3Amaster) # stern *Fork of discontinued [wercker/stern](https://github.com/wercker/stern)* Stern allows you to `tail` multiple pods on Kubernetes and multiple containers within the pod. Each result is color coded for quicker debugging. The query is a regular expression or a Kubernetes resource in the form `<resource>/<name>` so the pod name can easily be filtered and you don't need to specify the exact id (for instance omitting the deployment id). If a pod is deleted it gets removed from tail and if a new pod is added it automatically gets tailed. When a pod contains multiple containers Stern can tail all of them too without having to do this manually for each one. Simply specify the `container` flag to limit what containers to show. By default all containers are listened to. ## Installation ### Download binary Download a [binary release](https://github.com/stern/stern/releases) ### Build from source ``` go install github.com/stern/stern@latest ``` ### asdf (Linux/macOS) If you use [asdf](https://asdf-vm.com/), you can install like this: ``` asdf plugin-add stern asdf install stern latest ``` ### Homebrew (Linux/macOS) If you use [Homebrew](https://brew.sh), you can install like this: ``` brew install stern ``` ### Krew (Linux/macOS/Windows) If you use [Krew](https://krew.sigs.k8s.io/) which is the package manager for kubectl plugins, you can install like this: ``` kubectl krew install stern ``` ## Usage ``` stern pod-query [flags] ``` The `pod-query` is a regular expression or a Kubernetes resource in the form `<resource>/<name>`. The query is a regular expression when it is not a Kubernetes resource, so you could provide `"web-\w"` to tail `web-backend` and `web-frontend` pods but not `web-123`. When the query is in the form `<resource>/<name>` (exact match), you can select all pods belonging to the specified Kubernetes resource, such as `deployment/nginx`. Supported Kubernetes resources are `pod`, `replicationcontroller`, `service`, `daemonset`, `deployment`, `replicaset`, `statefulset` and `job`. ### cli flags <!-- auto generated cli flags begin ---> flag | default | purpose -----------------------------|-------------------------------|--------- `--all-namespaces`, `-A` | `false` | If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace. `--color` | `auto` | Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize. `--completion` | | Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'. `--config` | `~/.config/stern/config.yaml` | Path to the stern config file `--container`, `-c` | `.*` | Container name when multiple containers in pod. (regular expression) `--container-colors` | | Specifies the colors used to highlight container names. Use the same format as --pod-colors. Defaults to the values of --pod-colors if omitted, and must match its length. `--container-state` | `all` | Tail containers with state in running, waiting, terminated, or all. 'all' matches all container states. To specify multiple states, repeat this or set comma-separated value. `--context` | | The name of the kubeconfig context to use `--diff-container`, `-d` | `false` | Display different colors for different containers. `--ephemeral-containers` | `true` | Include or exclude ephemeral containers. `--exclude`, `-e` | `[]` | Log lines to exclude. (regular expression) `--exclude-container`, `-E` | `[]` | Container name to exclude when multiple containers in pod. (regular expression) `--exclude-pod` | `[]` | Pod name to exclude. (regular expression) `--field-selector` | | Selector (field query) to filter on. If present, default to ".*" for the pod-query. `--highlight`, `-H` | `[]` | Log lines to highlight. (regular expression) `--include`, `-i` | `[]` | Log lines to include. (regular expression) `--init-containers` | `true` | Include or exclude init containers. `--kubeconfig` | | Path to the kubeconfig file to use for CLI requests. `--max-log-requests` | `-1` | Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow `--namespace`, `-n` | | Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value. `--no-follow` | `false` | Exit when all logs have been shown. `--node` | | Node name to filter on. `--only-log-lines` | `false` | Print only log lines `--output`, `-o` | `default` | Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson] `--pod-colors` | | Specifies the colors used to highlight pod names. Provide colors as a comma-separated list using SGR (Select Graphic Rendition) sequences, e.g., "91,92,93,94,95,96". `--prompt`, `-p` | `false` | Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values. `--selector`, `-l` | | Selector (label query) to filter on. If present, default to ".*" for the pod-query. `--show-hidden-options` | `false` | Print a list of hidden options. `--since`, `-s` | `48h0m0s` | Return logs newer than a relative duration like 5s, 2m, or 3h. `--stdin` | `false` | Parse logs from stdin. All Kubernetes related flags are ignored when it is set. `--tail` | `-1` | The number of lines from the end of the logs to show. Defaults to -1, showing all logs. `--template` | | Template to use for log lines, leave empty to use --output flag. `--template-file`, `-T` | | Path to template to use for log lines, leave empty to use --output flag. It overrides --template option. `--timestamps`, `-t` | | Print timestamps with the specified format. One of 'default' or 'short' in the form '--timestamps=format' ('=' cannot be omitted). If specified but without value, 'default' is used. `--timezone` | `Local` | Set timestamps to specific timezone. `--verbosity` | `0` | Number of the log level verbosity `--version`, `-v` | `false` | Print the version and exit. <!-- auto generated cli flags end ---> See `stern --help` for details Stern will use the `$KUBECONFIG` environment variable if set. If both the environment variable and `--kubeconfig` flag are passed the cli flag will be used. ### config file You can use the config file to change the default values of stern options. The default config file path is `~/.config/stern/config.yaml`. ```yaml # <flag name>: <value> tail: 10 max-log-requests: 999 timestamps: short ``` You can change the config file path with `--config` flag or `STERNCONFIG` environment variable. ### templates stern supports outputting custom log messages. There are a few predefined templates which you can use by specifying the `--output` flag: | output | description | |-----------|-------------------------------------------------------------------------------------------------------| | `default` | Displays the namespace, pod and container, and decorates it with color depending on --color | | `raw` | Only outputs the log message itself, useful when your logs are json and you want to pipe them to `jq` | | `json` | Marshals the log struct to json. Useful for programmatic purposes | It accepts a custom template through the `--template` flag, which will be compiled to a Go template and then used for every log message. This Go template will receive the following struct: | property | type | description | |-----------------|--------|---------------------------------------------| | `Message` | string | The log message itself | | `NodeName` | string | The node name where the pod is scheduled on | | `Namespace` | string | The namespace of the pod | | `PodName` | string | The name of the pod | | `ContainerName` | string | The name of the container | The following functions are available within the template (besides the [builtin functions](https://golang.org/pkg/text/template/#hdr-Functions)): | func | arguments | description | |-----------------|-----------------------|-----------------------------------------------------------------------------------| | `json` | `object` | Marshal the object and output it as a json text | | `color` | `color.Color, string` | Wrap the text in color (.ContainerColor and .PodColor provided) | | `parseJSON` | `string` | Parse string as JSON | | `tryParseJSON` | `string` | Attempt to parse string as JSON, return nil on failure | | `extractJSONParts` | `string, ...string` | Parse string as JSON and concatenate the given keys. | | `tryExtractJSONParts` | `string, ...string` | Attempt to parse string as JSON and concatenate the given keys. , return text on failure | | `extjson` | `string` | Parse the object as json and output colorized json | | `ppextjson` | `string` | Parse the object as json and output pretty-print colorized json | | `toRFC3339Nano` | `object` | Parse timestamp (string, int, json.Number) and output it using RFC3339Nano format | | `toTimestamp` | `object, string [, string]` | Parse timestamp (string, int, json.Number) and output it using the given layout in the timezone that is optionally given (defaults to UTC). | | `levelColor` | `string` | Print log level using appropriate color | | `colorBlack` | `string` | Print text using black color | | `colorRed` | `string` | Print text using red color | | `colorGreen` | `string` | Print text using green color | | `colorYellow` | `string` | Print text using yellow color | | `colorBlue` | `string` | Print text using blue color | | `colorMagenta` | `string` | Print text using magenta color | | `colorCyan` | `string` | Print text using cyan color | | `colorWhite` | `string` | Print text using white color | ### Log level verbosity You can configure the log level verbosity by the `--verbosity` flag. It is useful when you want to know how stern interacts with a Kubernetes API server in troubleshooting. Increasing the verbosity increases the number of logs. `--verbosity 6` would be a good starting point. ### Max log requests Stern has the maximum number of concurrent logs to request to prevent unintentional load to a cluster. The number can be configured by the `--max-log-requests` flag. The behavior and the default are different depending on the presence of the `--no-follow` flag. | `--no-follow` | default | behavior | |---------------|---------|------------------| | specified | 5 | limits the number of concurrent logs to request | | not specified | 50 | exits with an error when if it reaches the concurrent limit | The combination of `--max-log-requests 1` and `--no-follow` will be helpful if you want to show logs in order. ### Customize highlight colors You can configure highlight colors for pods and containers in [the config file](#config-file) using a comma-separated list of [SGR (Select Graphic Rendition) sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters), as shown below. If you omit `container-colors`, the pod colors will be used as container colors as well. ```yaml # Green, Yellow, Blue, Magenta, Cyan, White pod-colors: "32,33,34,35,36,37" # Colors with underline (4) # If empty, the pod colors will be used as container colors container-colors: "32;4,33;4,34;4,35;4,36;4,37;4" ``` This format enables the use of various attributes, such as underline, background colors, 8-bit colors, and 24-bit colors, if your terminal supports them. The equivalent flags `--pod-colors` and `--container-colors` are also available. The following command applies [24-bit colors](https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit) using the `--pod-colors` flag. ```bash # Monokai theme podColors="38;2;255;97;136,38;2;169;220;118,38;2;255;216;102,38;2;120;220;232,38;2;171;157;242" stern --pod-colors "$podColors" deploy/app ``` ## Examples: Tail all logs from all namespaces ``` stern . --all-namespaces ``` Tail the `kube-system` namespace without printing any prior logs ``` stern . -n kube-system --tail 0 ``` Tail the `gateway` container running inside of the `envvars` pod on staging ``` stern envvars --context staging --container gateway ``` Tail the `staging` namespace excluding logs from `istio-proxy` container ``` stern -n staging --exclude-container istio-proxy . ``` Tail the `kube-system` namespace excluding logs from `kube-apiserver` pod ``` stern -n kube-system --exclude-pod kube-apiserver . ``` Show auth activity from 15min ago with timestamps ``` stern auth -t --since 15m ``` Show all logs of the last 5min by time, sorted by time ``` stern --since=5m --no-follow --only-log-lines -A -t . | sort -k4 ``` Show auth activity with timestamps in specific timezone (default is your local timezone) ``` stern auth -t --timezone Asia/Tokyo ``` Follow the development of `some-new-feature` in minikube ``` stern some-new-feature --context minikube ``` View pods from another namespace ``` stern kubernetes-dashboard --namespace kube-system ``` Tail the pods filtered by `run=nginx` label selector across all namespaces ``` stern --all-namespaces -l run=nginx ``` Follow the `frontend` pods in canary release ``` stern frontend --selector release=canary ``` Tail the pods on `kind-control-plane` node across all namespaces ``` stern --all-namespaces --field-selector spec.nodeName=kind-control-plane ``` Tail the pods created by `deployment/nginx` ``` stern deployment/nginx ``` Pipe the log message to jq: ``` stern backend -o json | jq . ``` Only output the log message itself: ``` stern backend -o raw ``` Output using a custom template: ``` stern --template '{{printf "%s (%s/%s/%s/%s)\n" .Message .NodeName .Namespace .PodName .ContainerName}}' backend ``` Output using a custom template with stern-provided colors: ``` stern --template '{{.Message}} ({{.Namespace}}/{{color .PodColor .PodName}}/{{color .ContainerColor .ContainerName}}){{"\n"}}' backend ``` Output using a custom template with `parseJSON`: ``` stern --template='{{.PodName}}/{{.ContainerName}} {{with $d := .Message | parseJSON}}[{{$d.level}}] {{$d.message}}{{end}}{{"\n"}}' backend ``` Output using a custom template that tries to parse JSON or fallbacks to raw format: ``` stern --template='{{.PodName}}/{{.ContainerName}} {{ with $msg := .Message | tryParseJSON }}[{{ colorGreen (toRFC3339Nano $msg.ts) }}] {{ levelColor $msg.level }} ({{ colorCyan $msg.caller }}) {{ $msg.msg }}{{ else }} {{ .Message }} {{ end }}{{"\n"}}' backend ``` Load custom template from file: ``` stern --template-file=~/.stern.tpl backend ``` Trigger the interactive prompt to select an 'app.kubernetes.io/instance' label value: ``` stern -p ``` Output log lines only: ``` stern . --only-log-lines ``` Read from stdin: ``` stern --stdin < service.log ``` ## Completion Stern supports command-line auto completion for bash, zsh or fish. `stern --completion=(bash|zsh|fish)` outputs the shell completion code which work by being evaluated in `.bashrc`, etc for the specified shell. In addition, Stern supports dynamic completion for `--namespace`, `--context`, `--node`, a resource query in the form `<resource>/<name>`, and flags with pre-defined choices. If you use bash, stern bash completion code depends on the [bash-completion](https://github.com/scop/bash-completion). On the macOS, you can install it with homebrew as follows: ``` # If running Bash 3.2 brew install bash-completion # or, if running Bash 4.1+ brew install bash-completion@2 ``` Note that bash-completion must be sourced before sourcing the stern bash completion code in `.bashrc`. ```sh source "$(brew --prefix)/etc/profile.d/bash_completion.sh" source <(stern --completion=bash) ``` If installed via Krew, use: ```bash source <(kubectl stern --completion bash) complete -o default -F __start_stern kubectl stern ``` If you use zsh, just source the stern zsh completion code in `.zshrc`. ```sh source <(stern --completion=zsh) ``` if you use fish shell, just source the stern fish completion code. ```sh stern --completion=fish | source # To load completions for each session, execute once: stern --completion=fish >~/.config/fish/completions/stern.fish ``` ## Running with container You can also use stern using a container: ``` docker run ghcr.io/stern/stern --version ``` If you are using a minikube cluster, you need to run a container as follows: ``` docker run --rm -v "$HOME/.minikube:$HOME/.minikube" -v "$HOME/.kube:/$HOME/.kube" -e KUBECONFIG="$HOME/.kube/config" ghcr.io/stern/stern . ``` You can find image tags in https://github.com/orgs/stern/packages/container/package/stern. ## Running in Kubernetes Pods If you want to use stern in Kubernetes Pods, you need to create the following ClusterRole and bind it to ServiceAccount. ```yaml apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: stern rules: - apiGroups: [""] resources: ["pods", "pods/log"] verbs: ["get", "watch", "list"] ``` ## Contributing to this repository Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 07070100000012000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001100000000stern-1.30.0/cmd07070100000013000081A4000000000000000000000001664F523B00005C8A000000000000000000000000000000000000001800000000stern-1.30.0/cmd/cmd.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmd import ( "context" "encoding/json" goflag "flag" "fmt" "io" "os" "regexp" "strconv" "strings" "text/template" "time" "github.com/fatih/color" "github.com/mitchellh/go-homedir" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/stern/stern/stern" "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "k8s.io/klog/v2" // load all auth plugins _ "k8s.io/client-go/plugin/pkg/client/auth" ) // Use "~" to avoid exposing the user name in the help message var defaultConfigFilePath = "~/.config/stern/config.yaml" type options struct { configFlags *genericclioptions.ConfigFlags genericclioptions.IOStreams excludePod []string container string excludeContainer []string containerStates []string timestamps string timezone string since time.Duration namespaces []string exclude []string include []string highlight []string initContainers bool ephemeralContainers bool allNamespaces bool selector string fieldSelector string tail int64 color string version bool completion string template string templateFile string output string prompt bool podQuery string noFollow bool resource string verbosity int onlyLogLines bool maxLogRequests int node string configFilePath string showHiddenOptions bool stdin bool diffContainer bool podColors []string containerColors []string client kubernetes.Interface clientConfig clientcmd.ClientConfig } func NewOptions(streams genericclioptions.IOStreams) *options { configFlags := genericclioptions.NewConfigFlags(true) // stern has its own namespace flag, so disable the one in configFlags configFlags.Namespace = nil return &options{ configFlags: configFlags, IOStreams: streams, color: "auto", container: ".*", containerStates: []string{stern.ALL_STATES}, initContainers: true, ephemeralContainers: true, output: "default", since: 48 * time.Hour, tail: -1, template: "", templateFile: "", timestamps: "", timezone: "Local", prompt: false, noFollow: false, maxLogRequests: -1, configFilePath: defaultConfigFilePath, } } func (o *options) Complete(args []string) error { if len(args) > 0 { if s := args[0]; strings.Contains(s, "/") { o.resource = s } else { o.podQuery = s } } envVar, ok := os.LookupEnv("STERNCONFIG") if ok { o.configFilePath = envVar } o.clientConfig = o.configFlags.ToRawKubeConfigLoader() restConfig, err := o.configFlags.ToRESTConfig() if err != nil { return err } o.client = kubernetes.NewForConfigOrDie(restConfig) if len(o.namespaces) == 0 { namespace, _, err := o.clientConfig.Namespace() if err != nil { return err } o.namespaces = []string{namespace} } return nil } func (o *options) Validate() error { if !o.prompt && o.podQuery == "" && o.resource == "" && o.selector == "" && o.fieldSelector == "" && !o.stdin { return errors.New("One of pod-query, --selector, --field-selector, --prompt or --stdin is required") } if o.selector != "" && o.resource != "" { return errors.New("--selector and the <resource>/<name> query can not be set at the same time") } return nil } func (o *options) Run(cmd *cobra.Command) error { if err := o.setVerbosity(); err != nil { return err } if err := o.setColorList(); err != nil { return err } config, err := o.sternConfig() if err != nil { return err } ctx, cancel := context.WithCancel(context.Background()) defer cancel() if o.prompt { if err := promptHandler(ctx, o.client, config, o.Out); err != nil { return err } } return stern.Run(ctx, o.client, config) } func (o *options) sternConfig() (*stern.Config, error) { pod, err := regexp.Compile(o.podQuery) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression from query") } excludePod, err := compileREs(o.excludePod) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for excluded pod query") } container, err := regexp.Compile(o.container) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for container query") } excludeContainer, err := compileREs(o.excludeContainer) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for excluded container query") } exclude, err := compileREs(o.exclude) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for exclusion filter") } include, err := compileREs(o.include) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for inclusion filter") } highlight, err := compileREs(o.highlight) if err != nil { return nil, errors.Wrap(err, "failed to compile regular expression for highlight filter") } containerStates := []stern.ContainerState{} for _, containerStateStr := range makeUnique(o.containerStates) { containerState, err := stern.NewContainerState(containerStateStr) if err != nil { return nil, err } containerStates = append(containerStates, containerState) } labelSelector := labels.Everything() if o.selector != "" { labelSelector, err = labels.Parse(o.selector) if err != nil { return nil, errors.Wrap(err, "failed to parse selector as label selector") } } fieldSelector, err := o.generateFieldSelector() if err != nil { return nil, err } var tailLines *int64 if o.tail != -1 { tailLines = &o.tail } switch o.color { case "always": color.NoColor = false case "never": color.NoColor = true case "auto": default: return nil, errors.New("color should be one of 'always', 'never', or 'auto'") } template, err := o.generateTemplate() if err != nil { return nil, err } namespaces := makeUnique(o.namespaces) var timestampFormat string switch o.timestamps { case "default": timestampFormat = stern.TimestampFormatDefault case "short": timestampFormat = stern.TimestampFormatShort case "": default: return nil, errors.New("timestamps should be one of 'default', or 'short'") } // --timezone location, err := time.LoadLocation(o.timezone) if err != nil { return nil, err } maxLogRequests := o.maxLogRequests if maxLogRequests == -1 { if o.noFollow { maxLogRequests = 5 } else { maxLogRequests = 50 } } return &stern.Config{ Namespaces: namespaces, PodQuery: pod, ExcludePodQuery: excludePod, Timestamps: timestampFormat != "", TimestampFormat: timestampFormat, Location: location, ContainerQuery: container, ExcludeContainerQuery: excludeContainer, ContainerStates: containerStates, Exclude: exclude, Include: include, Highlight: highlight, InitContainers: o.initContainers, EphemeralContainers: o.ephemeralContainers, Since: o.since, AllNamespaces: o.allNamespaces, LabelSelector: labelSelector, FieldSelector: fieldSelector, TailLines: tailLines, Template: template, Follow: !o.noFollow, Resource: o.resource, OnlyLogLines: o.onlyLogLines, MaxLogRequests: maxLogRequests, Stdin: o.stdin, DiffContainer: o.diffContainer, Out: o.Out, ErrOut: o.ErrOut, }, nil } // setVerbosity sets the log level verbosity func (o *options) setVerbosity() error { if o.verbosity != 0 { // klog does not have an external method to set verbosity, // so we need to set it by a flag. // See https://github.com/kubernetes/klog/issues/336 for details var fs goflag.FlagSet klog.InitFlags(&fs) return fs.Set("v", strconv.Itoa(o.verbosity)) } return nil } func (o *options) setColorList() error { if len(o.podColors) > 0 || len(o.containerColors) > 0 { return stern.SetColorList(o.podColors, o.containerColors) } return nil } // overrideFlagSetDefaultFromConfig overrides the default value of the flagSets // from the config file func (o *options) overrideFlagSetDefaultFromConfig(fs *pflag.FlagSet) error { expanded, err := homedir.Expand(o.configFilePath) if err != nil { return err } if o.configFilePath == defaultConfigFilePath { if _, err := os.Stat(expanded); os.IsNotExist(err) { return nil } } configFile, err := os.Open(expanded) if err != nil { return err } data := make(map[string]interface{}) if err := yaml.NewDecoder(configFile).Decode(data); err != nil && err != io.EOF { return err } for name, value := range data { flag := fs.Lookup(name) if flag == nil { // To avoid command execution failure, we only output a warning // message instead of exiting with an error if an unknown option is // specified. klog.Warningf("Unknown option specified in the config file: %s", name) continue } // flag has higher priority than the config file if flag.Changed { continue } if valueSlice, ok := value.([]any); ok { // the value is an array if flagSlice, ok := flag.Value.(pflag.SliceValue); ok { values := make([]string, len(valueSlice)) for i, v := range valueSlice { values[i] = fmt.Sprint(v) } if err := flagSlice.Replace(values); err != nil { return fmt.Errorf("invalid value %q for %q in the config file: %v", value, name, err) } continue } } if err := flag.Value.Set(fmt.Sprint(value)); err != nil { return fmt.Errorf("invalid value %q for %q in the config file: %v", value, name, err) } } return nil } // AddFlags adds all the flags used by stern. func (o *options) AddFlags(fs *pflag.FlagSet) { o.addKubernetesFlags(fs) fs.BoolVarP(&o.allNamespaces, "all-namespaces", "A", o.allNamespaces, "If present, tail across all namespaces. A specific namespace is ignored even if specified with --namespace.") fs.StringVar(&o.color, "color", o.color, "Force set color output. 'auto': colorize if tty attached, 'always': always colorize, 'never': never colorize.") fs.StringVar(&o.completion, "completion", o.completion, "Output stern command-line completion code for the specified shell. Can be 'bash', 'zsh' or 'fish'.") fs.StringVarP(&o.container, "container", "c", o.container, "Container name when multiple containers in pod. (regular expression)") fs.StringSliceVar(&o.containerStates, "container-state", o.containerStates, "Tail containers with state in running, waiting, terminated, or all. 'all' matches all container states. To specify multiple states, repeat this or set comma-separated value.") fs.StringArrayVarP(&o.exclude, "exclude", "e", o.exclude, "Log lines to exclude. (regular expression)") fs.StringArrayVarP(&o.excludeContainer, "exclude-container", "E", o.excludeContainer, "Container name to exclude when multiple containers in pod. (regular expression)") fs.StringArrayVar(&o.excludePod, "exclude-pod", o.excludePod, "Pod name to exclude. (regular expression)") fs.BoolVar(&o.noFollow, "no-follow", o.noFollow, "Exit when all logs have been shown.") fs.StringArrayVarP(&o.include, "include", "i", o.include, "Log lines to include. (regular expression)") fs.StringArrayVarP(&o.highlight, "highlight", "H", o.highlight, "Log lines to highlight. (regular expression)") fs.BoolVar(&o.initContainers, "init-containers", o.initContainers, "Include or exclude init containers.") fs.BoolVar(&o.ephemeralContainers, "ephemeral-containers", o.ephemeralContainers, "Include or exclude ephemeral containers.") fs.StringSliceVarP(&o.namespaces, "namespace", "n", o.namespaces, "Kubernetes namespace to use. Default to namespace configured in kubernetes context. To specify multiple namespaces, repeat this or set comma-separated value.") fs.StringVar(&o.node, "node", o.node, "Node name to filter on.") fs.IntVar(&o.maxLogRequests, "max-log-requests", o.maxLogRequests, "Maximum number of concurrent logs to request. Defaults to 50, but 5 when specifying --no-follow") fs.StringVarP(&o.output, "output", "o", o.output, "Specify predefined template. Currently support: [default, raw, json, extjson, ppextjson]") fs.BoolVarP(&o.prompt, "prompt", "p", o.prompt, "Toggle interactive prompt for selecting 'app.kubernetes.io/instance' label values.") fs.StringVarP(&o.selector, "selector", "l", o.selector, "Selector (label query) to filter on. If present, default to \".*\" for the pod-query.") fs.StringVar(&o.fieldSelector, "field-selector", o.fieldSelector, "Selector (field query) to filter on. If present, default to \".*\" for the pod-query.") fs.DurationVarP(&o.since, "since", "s", o.since, "Return logs newer than a relative duration like 5s, 2m, or 3h.") fs.Int64Var(&o.tail, "tail", o.tail, "The number of lines from the end of the logs to show. Defaults to -1, showing all logs.") fs.StringVar(&o.template, "template", o.template, "Template to use for log lines, leave empty to use --output flag.") fs.StringVarP(&o.templateFile, "template-file", "T", o.templateFile, "Path to template to use for log lines, leave empty to use --output flag. It overrides --template option.") fs.StringVarP(&o.timestamps, "timestamps", "t", o.timestamps, "Print timestamps with the specified format. One of 'default' or 'short' in the form '--timestamps=format' ('=' cannot be omitted). If specified but without value, 'default' is used.") fs.StringVar(&o.timezone, "timezone", o.timezone, "Set timestamps to specific timezone.") fs.BoolVar(&o.onlyLogLines, "only-log-lines", o.onlyLogLines, "Print only log lines") fs.StringVar(&o.configFilePath, "config", o.configFilePath, "Path to the stern config file") fs.IntVar(&o.verbosity, "verbosity", o.verbosity, "Number of the log level verbosity") fs.BoolVarP(&o.version, "version", "v", o.version, "Print the version and exit.") fs.BoolVar(&o.showHiddenOptions, "show-hidden-options", o.showHiddenOptions, "Print a list of hidden options.") fs.BoolVar(&o.stdin, "stdin", o.stdin, "Parse logs from stdin. All Kubernetes related flags are ignored when it is set.") fs.BoolVarP(&o.diffContainer, "diff-container", "d", o.diffContainer, "Display different colors for different containers.") fs.StringSliceVar(&o.podColors, "pod-colors", o.podColors, "Specifies the colors used to highlight pod names. Provide colors as a comma-separated list using SGR (Select Graphic Rendition) sequences, e.g., \"91,92,93,94,95,96\".") fs.StringSliceVar(&o.containerColors, "container-colors", o.containerColors, "Specifies the colors used to highlight container names. Use the same format as --pod-colors. Defaults to the values of --pod-colors if omitted, and must match its length.") fs.Lookup("timestamps").NoOptDefVal = "default" } func (o *options) addKubernetesFlags(fs *pflag.FlagSet) { flagset := pflag.NewFlagSet("", pflag.ExitOnError) o.configFlags.AddFlags(flagset) flagset.VisitAll(func(f *pflag.Flag) { // Hide Kubernetes flags except some if !(f.Name == "kubeconfig" || f.Name == "context") { f.Hidden = true } // `server` flag in configFlags has `s` shorthand, which is used by stern // as shorthand for `since` flag, so do not use it. if f.Name == "server" { f.Shorthand = "" } }) fs.AddFlagSet(flagset) } func (o *options) outputHiddenOptions() { fs := pflag.NewFlagSet("", pflag.ExitOnError) o.AddFlags(fs) fs.VisitAll(func(f *pflag.Flag) { f.Hidden = !f.Hidden }) fmt.Println("The following options can also be used in stern:") fs.PrintDefaults() } func (o *options) generateTemplate() (*template.Template, error) { t := o.template if o.templateFile != "" { data, err := os.ReadFile(o.templateFile) if err != nil { return nil, err } t = string(data) } if t == "" { switch o.output { case "default": t = "{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{.Message}}" if o.allNamespaces || len(o.namespaces) > 1 { t = fmt.Sprintf("{{color .PodColor .Namespace}} %s", t) } case "raw": t = "{{.Message}}" case "json": t = "{{json .}}" case "extjson": t = "\"pod\": \"{{color .PodColor .PodName}}\", \"container\": \"{{color .ContainerColor .ContainerName}}\", \"message\": {{extjson .Message}}" if o.allNamespaces { t = fmt.Sprintf("\"namespace\": \"{{color .PodColor .Namespace}}\", %s", t) } t = fmt.Sprintf("{%s}", t) case "ppextjson": t = " \"pod\": \"{{color .PodColor .PodName}}\",\n \"container\": \"{{color .ContainerColor .ContainerName}}\",\n \"message\": {{extjson .Message}}" if o.allNamespaces { t = fmt.Sprintf(" \"namespace\": \"{{color .PodColor .Namespace}}\",\n%s", t) } t = fmt.Sprintf("{\n%s\n}", t) default: return nil, errors.New("output should be one of 'default', 'raw', 'json', 'extjson', and 'ppextjson'") } t += "\n" } funs := map[string]interface{}{ "json": func(in interface{}) (string, error) { b, err := json.Marshal(in) if err != nil { return "", err } return string(b), nil }, "tryParseJSON": func(text string) map[string]interface{} { decoder := json.NewDecoder(strings.NewReader(text)) decoder.UseNumber() obj := make(map[string]interface{}) if err := decoder.Decode(&obj); err != nil { return nil } return obj }, "parseJSON": func(text string) (map[string]interface{}, error) { obj := make(map[string]interface{}) if err := json.Unmarshal([]byte(text), &obj); err != nil { return obj, err } return obj, nil }, "extractJSONParts": func(text string, part ...string) (string, error) { obj := make(map[string]interface{}) if err := json.Unmarshal([]byte(text), &obj); err != nil { return "", err } parts := make([]string, 0) for _, key := range part { parts = append(parts, fmt.Sprintf("%v", obj[key])) } return strings.Join(parts, ", "), nil }, "tryExtractJSONParts": func(text string, part ...string) string { obj := make(map[string]interface{}) if err := json.Unmarshal([]byte(text), &obj); err != nil { return text } parts := make([]string, 0) for _, key := range part { parts = append(parts, fmt.Sprintf("%v", obj[key])) } return strings.Join(parts, ", ") }, "extjson": func(in string) (string, error) { if json.Valid([]byte(in)) { return strings.TrimSuffix(in, "\n"), nil } b, err := json.Marshal(in) if err != nil { return "", err } return strings.TrimSuffix(string(b), "\n"), nil }, "toRFC3339Nano": func(ts any) string { return toTime(ts).Format(time.RFC3339Nano) }, "toUTC": func(ts any) time.Time { return toTime(ts).UTC() }, "toTimestamp": func(ts any, layout string, optionalTZ ...string) (string, error) { t, parseErr := toTimeE(ts) if parseErr != nil { return "", parseErr } var tz string if len(optionalTZ) > 0 { tz = optionalTZ[0] } loc, loadErr := time.LoadLocation(tz) if loadErr != nil { return "", loadErr } return t.In(loc).Format(layout), nil }, "color": func(color color.Color, text string) string { return color.SprintFunc()(text) }, "colorBlack": color.BlackString, "colorRed": color.RedString, "colorGreen": color.GreenString, "colorYellow": color.YellowString, "colorBlue": color.BlueString, "colorMagenta": color.MagentaString, "colorCyan": color.CyanString, "colorWhite": color.WhiteString, "levelColor": func(level string) string { var levelColor *color.Color switch strings.ToLower(level) { case "debug": levelColor = color.New(color.FgMagenta) case "info": levelColor = color.New(color.FgBlue) case "warn": levelColor = color.New(color.FgYellow) case "warning": levelColor = color.New(color.FgYellow) case "error": levelColor = color.New(color.FgRed) case "dpanic": levelColor = color.New(color.FgRed) case "panic": levelColor = color.New(color.FgRed) case "fatal": levelColor = color.New(color.FgCyan) case "critical": levelColor = color.New(color.FgCyan) default: return level } return levelColor.SprintFunc()(level) }, } template, err := template.New("log").Funcs(funs).Parse(t) if err != nil { return nil, errors.Wrap(err, "unable to parse template") } return template, err } func (o *options) generateFieldSelector() (fields.Selector, error) { var queries []string if o.fieldSelector != "" { queries = append(queries, o.fieldSelector) } if o.node != "" { queries = append(queries, fmt.Sprintf("spec.nodeName=%s", o.node)) } if len(queries) == 0 { return fields.Everything(), nil } fieldSelector, err := fields.ParseSelector(strings.Join(queries, ",")) if err != nil { return nil, errors.Wrap(err, "failed to parse selector as field selector") } return fieldSelector, nil } func NewSternCmd(stream genericclioptions.IOStreams) (*cobra.Command, error) { o := NewOptions(stream) cmd := &cobra.Command{ Use: "stern pod-query", Short: "Tail multiple pods and containers from Kubernetes", RunE: func(cmd *cobra.Command, args []string) error { // Output version information and exit if o.version { outputVersionInfo(o.Out) return nil } // Output shell completion code for the specified shell and exit if o.completion != "" { return runCompletion(o.completion, cmd, o.Out) } if o.showHiddenOptions { o.outputHiddenOptions() return nil } if err := o.Complete(args); err != nil { return err } if err := o.overrideFlagSetDefaultFromConfig(cmd.Flags()); err != nil { return err } if err := o.Validate(); err != nil { return err } cmd.SilenceUsage = true return o.Run(cmd) }, ValidArgsFunction: queryCompletionFunc(o), } cmd.SetUsageTemplate(cmd.UsageTemplate() + "\nUse \"stern --show-hidden-options\" for a list of hidden command-line options.\n") o.AddFlags(cmd.Flags()) if err := registerCompletionFuncForFlags(cmd, o); err != nil { return cmd, err } return cmd, nil } // makeUnique makes items in string slice unique func makeUnique(items []string) []string { result := []string{} m := make(map[string]struct{}) for _, item := range items { if item == "" { continue } if _, ok := m[item]; !ok { m[item] = struct{}{} result = append(result, item) } } return result } func compileREs(exprs []string) ([]*regexp.Regexp, error) { var regexps []*regexp.Regexp for _, s := range exprs { re, err := regexp.Compile(s) if err != nil { return nil, err } regexps = append(regexps, re) } return regexps, nil } 07070100000014000081A4000000000000000000000001664F523B00004F94000000000000000000000000000000000000001D00000000stern-1.30.0/cmd/cmd_test.gopackage cmd import ( "bytes" "os" "path/filepath" "reflect" "regexp" "strings" "testing" "time" "github.com/fatih/color" "github.com/spf13/pflag" "github.com/stern/stern/stern" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/utils/ptr" ) func TestSternCommand(t *testing.T) { tests := []struct { name string args []string out string }{ { "Output version info with --version", []string{"--version"}, "version: dev", }, { "Output completion code for bash with --completion=bash", []string{"--completion=bash"}, "complete -o default -F __start_stern stern", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { streams, _, out, _ := genericclioptions.NewTestIOStreams() stern, err := NewSternCmd(streams) if err != nil { t.Fatal(err) } stern.SetArgs(tt.args) if err := stern.Execute(); err != nil { t.Fatal(err) } if !strings.Contains(out.String(), tt.out) { t.Errorf("expected to contain %s, but actual %s", tt.out, out.String()) } }) } } func TestOptionsComplete(t *testing.T) { streams := genericclioptions.NewTestIOStreamsDiscard() tests := []struct { name string env map[string]string args []string expectedConfigFilePath string }{ { name: "No environment variables", env: map[string]string{}, args: []string{}, expectedConfigFilePath: defaultConfigFilePath, }, { name: "Set STERNCONFIG env to ./config.yaml", env: map[string]string{ "STERNCONFIG": "./config.yaml", }, args: []string{}, expectedConfigFilePath: "./config.yaml", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for k, v := range tt.env { t.Setenv(k, v) } o := NewOptions(streams) _ = o.Complete(tt.args) if tt.expectedConfigFilePath != o.configFilePath { t.Errorf("expected %s for configFilePath, but got %s", tt.expectedConfigFilePath, o.configFilePath) } }) } } func TestOptionsValidate(t *testing.T) { streams := genericclioptions.NewTestIOStreamsDiscard() tests := []struct { name string o *options err string }{ { "No required options", NewOptions(streams), "One of pod-query, --selector, --field-selector, --prompt or --stdin is required", }, { "Specify both selector and resource", func() *options { o := NewOptions(streams) o.selector = "app=nginx" o.resource = "deployment/nginx" return o }(), "--selector and the <resource>/<name> query can not be set at the same time", }, { "Use prompt", func() *options { o := NewOptions(streams) o.prompt = true return o }(), "", }, { "Specify pod-query", func() *options { o := NewOptions(streams) o.podQuery = "." return o }(), "", }, { "Specify selector", func() *options { o := NewOptions(streams) o.selector = "app=nginx" return o }(), "", }, { "Specify fieldSelector", func() *options { o := NewOptions(streams) o.fieldSelector = "spec.nodeName=kind-kind" return o }(), "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.o.Validate() if err == nil { if tt.err != "" { t.Errorf("expected %q err, but actual no err", tt.err) } } else { if tt.err != err.Error() { t.Errorf("expected %q err, but actual %q", tt.err, err) } } }) } } func TestOptionsGenerateTemplate(t *testing.T) { t.Setenv("NO_COLOR", "1") streams := genericclioptions.NewTestIOStreamsDiscard() tests := []struct { name string o *options message string want string wantError bool }{ { "output=default", func() *options { o := NewOptions(streams) o.output = "default" return o }(), "default message", "pod1 container1 default message\n", false, }, { "output=default+allNamespaces", func() *options { o := NewOptions(streams) o.output = "default" o.allNamespaces = true return o }(), "default message", "ns1 pod1 container1 default message\n", false, }, { "output=raw", func() *options { o := NewOptions(streams) o.output = "raw" return o }(), "raw message", "raw message\n", false, }, { "output=json", func() *options { o := NewOptions(streams) o.output = "json" return o }(), "json message", `{"message":"json message","nodeName":"node1","namespace":"ns1","podName":"pod1","containerName":"container1"} `, false, }, { "output=extjson", func() *options { o := NewOptions(streams) o.output = "extjson" return o }(), `{"msg":"extjson message"}`, `{"pod": "pod1", "container": "container1", "message": {"msg":"extjson message"}} `, false, }, { "output=extjson+allNamespaces", func() *options { o := NewOptions(streams) o.output = "extjson" o.allNamespaces = true return o }(), `{"msg":"extjson message"}`, `{"namespace": "ns1", "pod": "pod1", "container": "container1", "message": {"msg":"extjson message"}} `, false, }, { "output=ppextjson", func() *options { o := NewOptions(streams) o.output = "ppextjson" return o }(), `{"msg":"ppextjson message"}`, `{ "pod": "pod1", "container": "container1", "message": {"msg":"ppextjson message"} } `, false, }, { "output=ppextjson+allNamespaces", func() *options { o := NewOptions(streams) o.output = "ppextjson" o.allNamespaces = true return o }(), `{"msg":"ppextjson message"}`, `{ "namespace": "ns1", "pod": "pod1", "container": "container1", "message": {"msg":"ppextjson message"} } `, false, }, { "invalid output", func() *options { o := NewOptions(streams) o.output = "invalid" return o }(), "message", "", true, }, { "template", func() *options { o := NewOptions(streams) o.template = "Message={{.Message}} NodeName={{.NodeName}} Namespace={{.Namespace}} PodName={{.PodName}} ContainerName={{.ContainerName}}" return o }(), "template message", // no new line "Message=template message NodeName=node1 Namespace=ns1 PodName=pod1 ContainerName=container1", false, }, { "invalid template", func() *options { o := NewOptions(streams) o.template = "{{invalid" return o }(), "template message", "", true, }, { "template-file", func() *options { o := NewOptions(streams) o.templateFile = "test.tpl" return o }(), "template message", "pod1 container1 template message", false, }, { "template-file-json-log-ts-float", func() *options { o := NewOptions(streams) o.templateFile = "test.tpl" return o }(), `{"ts": 123, "level": "INFO", "msg": "template message"}`, "pod1 container1 [1970-01-01T00:02:03Z] INFO template message", false, }, { "template-file-json-log-ts-str", func() *options { o := NewOptions(streams) o.templateFile = "test.tpl" return o }(), `{"ts": "1970-01-01T01:02:03+01:00", "level": "INFO", "msg": "template message"}`, "pod1 container1 [1970-01-01T00:02:03Z] INFO template message", false, }, { "template-to-timestamp-with-timezone", func() *options { o := NewOptions(streams) o.template = `{{ toTimestamp .Message "Jan 02 2006 15:04 MST" "US/Eastern" }}` return o }(), `2024-01-01T05:00:00`, `Jan 01 2024 00:00 EST`, false, }, { "template-to-timestamp-without-timezone", func() *options { o := NewOptions(streams) o.template = `{{ toTimestamp .Message "Jan 02 2006 15:04 MST" }}` return o }(), `2024-01-01T05:00:00`, `Jan 01 2024 05:00 UTC`, false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { log := stern.Log{ Message: tt.message, NodeName: "node1", Namespace: "ns1", PodName: "pod1", ContainerName: "container1", PodColor: color.New(color.FgRed), ContainerColor: color.New(color.FgBlue), } tmpl, err := tt.o.generateTemplate() if tt.wantError { if err == nil { t.Errorf("expected error, but got no error") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } var buf bytes.Buffer if err := tmpl.Execute(&buf, log); err != nil { t.Errorf("unexpected error: %v", err) return } if want, got := tt.want, buf.String(); want != got { t.Errorf("want %v, but got %v", want, got) } }) } } func TestOptionsSternConfig(t *testing.T) { streams := genericclioptions.NewTestIOStreamsDiscard() local, _ := time.LoadLocation("Local") utc, _ := time.LoadLocation("UTC") labelSelector, _ := labels.Parse("l=sel") fieldSelector, _ := fields.ParseSelector("f=field,spec.nodeName=node1") re := regexp.MustCompile defaultConfig := func() *stern.Config { return &stern.Config{ Namespaces: []string{}, PodQuery: re(""), ExcludePodQuery: nil, Timestamps: false, TimestampFormat: "", Location: local, ContainerQuery: re(".*"), ExcludeContainerQuery: nil, ContainerStates: []stern.ContainerState{stern.ALL_STATES}, Exclude: nil, Include: nil, Highlight: nil, InitContainers: true, EphemeralContainers: true, Since: 48 * time.Hour, AllNamespaces: false, LabelSelector: labels.Everything(), FieldSelector: fields.Everything(), TailLines: nil, Template: nil, // ignore when comparing Follow: true, Resource: "", OnlyLogLines: false, MaxLogRequests: 50, Out: streams.Out, ErrOut: streams.ErrOut, } } tests := []struct { name string o *options want *stern.Config wantError bool }{ { "default", NewOptions(streams), defaultConfig(), false, }, { "change all options", func() *options { o := NewOptions(streams) o.namespaces = []string{"ns1", "ns2"} o.podQuery = "query1" o.excludePod = []string{"exp1", "exp2"} o.timestamps = "default" o.timezone = "UTC" // Location o.container = "container1" o.excludeContainer = []string{"exc1", "exc2"} o.containerStates = []string{"running", "terminated"} o.exclude = []string{"ex1", "ex2"} o.include = []string{"in1", "in2"} o.highlight = []string{"hi1", "hi2"} o.initContainers = false o.ephemeralContainers = false o.since = 1 * time.Hour o.allNamespaces = true o.selector = "l=sel" o.fieldSelector = "f=field" o.tail = 10 o.noFollow = true // Follow = false o.maxLogRequests = 30 o.resource = "res1" o.onlyLogLines = true o.node = "node1" return o }(), func() *stern.Config { c := defaultConfig() c.Namespaces = []string{"ns1", "ns2"} c.PodQuery = re("query1") c.ExcludePodQuery = []*regexp.Regexp{re("exp1"), re("exp2")} c.Timestamps = true c.TimestampFormat = stern.TimestampFormatDefault c.Location = utc c.ContainerQuery = re("container1") c.ExcludeContainerQuery = []*regexp.Regexp{re("exc1"), re("exc2")} c.ContainerStates = []stern.ContainerState{stern.RUNNING, stern.TERMINATED} c.Exclude = []*regexp.Regexp{re("ex1"), re("ex2")} c.Include = []*regexp.Regexp{re("in1"), re("in2")} c.Highlight = []*regexp.Regexp{re("hi1"), re("hi2")} c.InitContainers = false c.EphemeralContainers = false c.Since = 1 * time.Hour c.AllNamespaces = true c.LabelSelector = labelSelector c.FieldSelector = fieldSelector c.TailLines = ptr.To[int64](10) c.Follow = false c.Resource = "res1" c.OnlyLogLines = true c.MaxLogRequests = 30 return c }(), false, }, { "fieldSelector without node", func() *options { o := NewOptions(streams) o.fieldSelector = "f=field" return o }(), func() *stern.Config { c := defaultConfig() sel, _ := fields.ParseSelector("f=field") c.FieldSelector = sel return c }(), false, }, { "node without fieldSelector", func() *options { o := NewOptions(streams) o.node = "node1" return o }(), func() *stern.Config { c := defaultConfig() sel, _ := fields.ParseSelector("spec.nodeName=node1") c.FieldSelector = sel return c }(), false, }, { "timestamp=short", func() *options { o := NewOptions(streams) o.timestamps = "short" return o }(), func() *stern.Config { c := defaultConfig() c.Timestamps = true c.TimestampFormat = stern.TimestampFormatShort return c }(), false, }, { "noFollow has the different default", func() *options { o := NewOptions(streams) o.noFollow = true // Follow = false return o }(), func() *stern.Config { c := defaultConfig() c.Follow = false c.MaxLogRequests = 5 // default of noFollow return c }(), false, }, { "nil should be allowed", func() *options { o := NewOptions(streams) o.excludePod = nil o.excludeContainer = nil o.containerStates = nil o.namespaces = nil o.exclude = nil o.include = nil o.highlight = nil return o }(), func() *stern.Config { c := defaultConfig() c.ContainerStates = []stern.ContainerState{} return c }(), false, }, { "error podQuery", func() *options { o := NewOptions(streams) o.podQuery = "[invalid" return o }(), nil, true, }, { "error excludePod", func() *options { o := NewOptions(streams) o.excludePod = []string{"exp1", "[invalid"} return o }(), nil, true, }, { "error container", func() *options { o := NewOptions(streams) o.container = "[invalid" return o }(), nil, true, }, { "error excludeContainer", func() *options { o := NewOptions(streams) o.excludeContainer = []string{"exc1", "[invalid"} return o }(), nil, true, }, { "error exclude", func() *options { o := NewOptions(streams) o.exclude = []string{"ex1", "[invalid"} return o }(), nil, true, }, { "error include", func() *options { o := NewOptions(streams) o.include = []string{"in1", "[invalid"} return o }(), nil, true, }, { "error highlight", func() *options { o := NewOptions(streams) o.highlight = []string{"hi1", "[invalid"} return o }(), nil, true, }, { "error containerStates", func() *options { o := NewOptions(streams) o.containerStates = []string{"running", "invalid"} return o }(), nil, true, }, { "error selector", func() *options { o := NewOptions(streams) o.selector = "-" return o }(), nil, true, }, { "error fieldSelector", func() *options { o := NewOptions(streams) o.fieldSelector = "-" return o }(), nil, true, }, { "error color", func() *options { o := NewOptions(streams) o.color = "invalid" return o }(), nil, true, }, { "error output", func() *options { o := NewOptions(streams) o.output = "invalid" return o }(), nil, true, }, { "error timezone", func() *options { o := NewOptions(streams) o.timezone = "invalid" return o }(), nil, true, }, { "error timestamps", func() *options { o := NewOptions(streams) o.timestamps = "invalid" return o }(), nil, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := tt.o.sternConfig() if tt.wantError { if err == nil { t.Errorf("expected error, but got no error") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } // We skip the template as it is difficult to check // and is tested in TestOptionsGenerateTemplate(). got.Template = nil if !reflect.DeepEqual(tt.want, got) { t.Errorf("want %+v, but got %+v", tt.want, got) } }) } } func TestOptionsOverrideFlagSetDefaultFromConfig(t *testing.T) { orig := defaultConfigFilePath defer func() { defaultConfigFilePath = orig }() defaultConfigFilePath = "./config.yaml" wd, _ := os.Getwd() tests := []struct { name string flagConfigFilePathValue string flagTailValue string expectedTailValue int64 wantErr bool }{ { name: "--config=testdata/config-tail1.yaml", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"), expectedTailValue: 1, wantErr: false, }, { name: "--config=testdata/config-empty.yaml", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-empty.yaml"), expectedTailValue: -1, wantErr: false, }, { name: "--config=config-not-exist.yaml", flagConfigFilePathValue: filepath.Join(wd, "config-not-exist.yaml"), wantErr: true, }, { name: "--config=config-invalid.yaml", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-invalid.yaml"), wantErr: true, }, { name: "--config=config-unknown-option.yaml", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-unknown-option.yaml"), expectedTailValue: 1, wantErr: false, }, { name: "--config=config-tail-invalid-value.yaml", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail-invalid-value.yaml"), wantErr: true, }, { name: "config file path is not specified and config file does not exist", expectedTailValue: -1, wantErr: false, }, { name: "--config=testdata/config-tail1.yaml and --tail=2", flagConfigFilePathValue: filepath.Join(wd, "testdata/config-tail1.yaml"), flagTailValue: "2", expectedTailValue: 2, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { o := NewOptions(genericclioptions.NewTestIOStreamsDiscard()) fs := pflag.NewFlagSet("", pflag.ExitOnError) o.AddFlags(fs) args := []string{} if tt.flagConfigFilePathValue != "" { args = append(args, "--config="+tt.flagConfigFilePathValue) } if tt.flagTailValue != "" { args = append(args, "--tail="+tt.flagTailValue) } if err := fs.Parse(args); err != nil { t.Fatal(err) } err := o.overrideFlagSetDefaultFromConfig(fs) if tt.wantErr { if err == nil { t.Error("expected err, but got nil") } return } if err != nil { t.Errorf("unexpected err: %v", err) } if tt.expectedTailValue != o.tail { t.Errorf("expected %d for tail, but got %d", tt.expectedTailValue, o.tail) } }) } } func TestOptionsOverrideFlagSetDefaultFromConfigArray(t *testing.T) { tests := []struct { config string want []string }{ { config: "testdata/config-string.yaml", want: []string{"hello-world"}, }, { config: "testdata/config-array0.yaml", want: []string{}, }, { config: "testdata/config-array1.yaml", want: []string{"abcd"}, }, { config: "testdata/config-array2.yaml", want: []string{"abcd", "efgh"}, }, } for _, tt := range tests { tt := tt t.Run(tt.config, func(t *testing.T) { o := NewOptions(genericclioptions.NewTestIOStreamsDiscard()) fs := pflag.NewFlagSet("", pflag.ExitOnError) o.AddFlags(fs) if err := fs.Parse([]string{"--config=" + tt.config}); err != nil { t.Fatal(err) } if err := o.overrideFlagSetDefaultFromConfig(fs); err != nil { t.Fatal(err) } if !reflect.DeepEqual(tt.want, o.exclude) { t.Errorf("expected %v, but got %v", tt.want, o.exclude) } }) } } 07070100000015000081A4000000000000000000000001664F523B0000232C000000000000000000000000000000000000002400000000stern-1.30.0/cmd/flag_completion.go// Copyright 2017 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package cmd import ( "bytes" "context" "fmt" "io" "strings" "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/stern/stern/stern" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) var flagChoices = map[string][]string{ "color": {"always", "never", "auto"}, "completion": {"bash", "zsh", "fish"}, "container-state": {stern.RUNNING, stern.WAITING, stern.TERMINATED, stern.ALL_STATES}, "output": {"default", "raw", "json", "extjson", "ppextjson"}, "timestamps": {"default", "short"}, } func runCompletion(shell string, cmd *cobra.Command, out io.Writer) error { var err error switch shell { case "bash": err = cmd.GenBashCompletion(out) case "zsh": err = runCompletionZsh(cmd, out) case "fish": err = cmd.GenFishCompletion(out, true) default: err = fmt.Errorf("Unsupported shell type: %q", shell) } return err } // runCompletionZsh is based on `kubectl completion zsh`. This function should // be replaced by cobra implementation when cobra itself supports zsh completion. // https://github.com/kubernetes/kubernetes/blob/v1.6.1/pkg/kubectl/cmd/completion.go#L136 func runCompletionZsh(cmd *cobra.Command, out io.Writer) error { b := new(bytes.Buffer) if err := cmd.GenZshCompletion(b); err != nil { return err } // Cobra doesn't source zsh completion file, explicitly doing it here fmt.Fprintf(b, "compdef _stern stern") fmt.Fprint(out, b.String()) return nil } func registerCompletionFuncForFlags(cmd *cobra.Command, o *options) error { if err := cmd.RegisterFlagCompletionFunc("namespace", namespaceCompletionFunc(o)); err != nil { return err } if err := cmd.RegisterFlagCompletionFunc("node", nodeCompletionFunc(o)); err != nil { return err } if err := cmd.RegisterFlagCompletionFunc("context", contextCompletionFunc(o)); err != nil { return err } // flags with pre-defined choices for flag, choices := range flagChoices { if err := cmd.RegisterFlagCompletionFunc(flag, cobra.FixedCompletions(choices, cobra.ShellCompDirectiveNoFileComp)); err != nil { return err } } return nil } // namespaceCompletionFunc is a completion function that completes namespaces // that match the toComplete prefix. func namespaceCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { if err := o.Complete(nil); err != nil { return compError(err) } namespaceList, err := o.client.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) if err != nil { return compError(err) } var comps []string for _, ns := range namespaceList.Items { if strings.HasPrefix(ns.GetName(), toComplete) { comps = append(comps, ns.GetName()) } } return comps, cobra.ShellCompDirectiveNoFileComp } } // nodeCompletionFunc is a completion function that completes node names // that match the toComplete prefix. func nodeCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { if err := o.Complete(nil); err != nil { return compError(err) } nodeList, err := o.client.CoreV1().Nodes().List(context.TODO(), metav1.ListOptions{}) if err != nil { return compError(err) } var comps []string for _, node := range nodeList.Items { if strings.HasPrefix(node.GetName(), toComplete) { comps = append(comps, node.GetName()) } } return comps, cobra.ShellCompDirectiveNoFileComp } } // contextCompletionFunc is a completion function that completes contexts // that match the toComplete prefix. func contextCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { if err := o.Complete(nil); err != nil { return compError(err) } var comps []string kubeConfig, err := o.clientConfig.RawConfig() if err != nil { return compError(err) } for name := range kubeConfig.Contexts { if strings.HasPrefix(name, toComplete) { comps = append(comps, name) } } return comps, cobra.ShellCompDirectiveNoFileComp } } // queryCompletionFunc is a completion function that completes a resource // that match the toComplete prefix. func queryCompletionFunc(o *options) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { if err := o.Complete(nil); err != nil { return compError(err) } var comps []string parts := strings.Split(toComplete, "/") if len(parts) != 2 { // list available resources in the form "<resource>/" for _, matcher := range stern.ResourceMatchers { if strings.HasPrefix(matcher.Name(), toComplete) { comps = append(comps, matcher.Name()+"/") } } return comps, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace } // list available names in the resources in the form "<resource>/<name>" uniqueNamespaces := makeUnique(o.namespaces) if o.allNamespaces || len(uniqueNamespaces) > 1 { // do not support multiple namespaces for simplicity return compError(errors.New("multiple namespaces are not supported")) } var namespace string if len(uniqueNamespaces) == 1 { namespace = uniqueNamespaces[0] } else { n, _, err := o.clientConfig.Namespace() if err != nil { return compError(err) } namespace = n } kind, name := parts[0], parts[1] names, err := retrieveNamesFromResource(context.TODO(), o.client, namespace, kind) if err != nil { return compError(err) } for _, n := range names { if strings.HasPrefix(n, name) { comps = append(comps, kind+"/"+n) } } return comps, cobra.ShellCompDirectiveNoFileComp } } func compError(err error) ([]string, cobra.ShellCompDirective) { cobra.CompError(err.Error()) return nil, cobra.ShellCompDirectiveError } func retrieveNamesFromResource(ctx context.Context, client kubernetes.Interface, namespace, kind string) ([]string, error) { opt := metav1.ListOptions{} var names []string switch { // core case stern.PodMatcher.Matches(kind): l, err := client.CoreV1().Pods(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } case stern.ReplicationControllerMatcher.Matches(kind): l, err := client.CoreV1().ReplicationControllers(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } case stern.ServiceMatcher.Matches(kind): l, err := client.CoreV1().Services(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } // apps case stern.DeploymentMatcher.Matches(kind): l, err := client.AppsV1().Deployments(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } case stern.DaemonSetMatcher.Matches(kind): l, err := client.AppsV1().DaemonSets(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } case stern.ReplicaSetMatcher.Matches(kind): l, err := client.AppsV1().ReplicaSets(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } case stern.StatefulSetMatcher.Matches(kind): l, err := client.AppsV1().StatefulSets(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } // batch case stern.JobMatcher.Matches(kind): l, err := client.BatchV1().Jobs(namespace).List(ctx, opt) if err != nil { return nil, err } for _, item := range l.Items { names = append(names, item.GetName()) } default: return nil, fmt.Errorf("resource type %s is not supported", kind) } return names, nil } 07070100000016000081A4000000000000000000000001664F523B00000CF3000000000000000000000000000000000000002900000000stern-1.30.0/cmd/flag_completion_test.gopackage cmd import ( "context" "reflect" "testing" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" ) func TestRetrieveNamesFromResource(t *testing.T) { genMeta := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{ Name: name, Namespace: "ns1", } } objs := []runtime.Object{ &corev1.Pod{ObjectMeta: genMeta("pod1")}, &corev1.Pod{ObjectMeta: genMeta("pod2")}, &corev1.Pod{ObjectMeta: genMeta("pod3")}, &corev1.ReplicationController{ObjectMeta: genMeta("rc1")}, &corev1.Service{ObjectMeta: genMeta("svc1")}, &appsv1.Deployment{ObjectMeta: genMeta("deploy1")}, &appsv1.Deployment{ObjectMeta: genMeta("deploy2")}, &appsv1.DaemonSet{ObjectMeta: genMeta("ds1")}, &appsv1.DaemonSet{ObjectMeta: genMeta("ds2")}, &appsv1.ReplicaSet{ObjectMeta: genMeta("rs1")}, &appsv1.ReplicaSet{ObjectMeta: genMeta("rs2")}, &appsv1.StatefulSet{ObjectMeta: genMeta("sts1")}, &appsv1.StatefulSet{ObjectMeta: genMeta("sts2")}, &batchv1.Job{ObjectMeta: genMeta("job1")}, &batchv1.Job{ObjectMeta: genMeta("job2")}, } client := fake.NewSimpleClientset(objs...) tests := []struct { desc string kinds []string expected []string wantError bool }{ // core { desc: "pods", kinds: []string{"po", "pods", "pod"}, expected: []string{"pod1", "pod2", "pod3"}, }, { desc: "replicationcontrollers", kinds: []string{"rc", "replicationcontrollers", "replicationcontroller"}, expected: []string{"rc1"}, }, // apps { desc: "deployments", kinds: []string{"deploy", "deployments", "deployment"}, expected: []string{"deploy1", "deploy2"}, }, { desc: "daemonsets", kinds: []string{"ds", "daemonsets", "daemonset"}, expected: []string{"ds1", "ds2"}, }, { desc: "replicasets", kinds: []string{"rs", "replicasets", "replicaset"}, expected: []string{"rs1", "rs2"}, }, { desc: "statefulsets", kinds: []string{"sts", "statefulsets", "statefulset"}, expected: []string{"sts1", "sts2"}, }, // batch { desc: "jobs", kinds: []string{"job", "jobs"}, expected: []string{"job1", "job2"}, }, // invalid { desc: "invalid", kinds: []string{"", "unknown"}, wantError: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { for _, kind := range tt.kinds { names, err := retrieveNamesFromResource(context.Background(), client, "ns1", kind) if tt.wantError { if err == nil { t.Errorf("expected error, but got no error") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if !reflect.DeepEqual(tt.expected, names) { t.Errorf("expected %v, but actual %v", tt.expected, names) } // expect empty slice with no error when no objects are found in the valid resource names, err = retrieveNamesFromResource(context.Background(), client, "not-matched", kind) if err != nil { t.Errorf("unexpected error: %v", err) return } if len(names) != 0 { t.Errorf("expected empty slice, but got %v", names) return } } }) } } 07070100000017000081A4000000000000000000000001664F523B0000065B000000000000000000000000000000000000002000000000stern-1.30.0/cmd/flag_prompt.gopackage cmd import ( "context" "fmt" "io" "sort" "github.com/AlecAivazis/survey/v2" "github.com/fatih/color" "github.com/pkg/errors" "github.com/stern/stern/stern" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" ) // promptHandler invokes the interactive prompt and updates config.LabelSelector with the selected value. func promptHandler(ctx context.Context, client kubernetes.Interface, config *stern.Config, out io.Writer) error { labelsMap, err := stern.List(ctx, client, config) if err != nil { return err } if len(labelsMap) == 0 { return errors.New("No matching labels") } var choices []string for key := range labelsMap { choices = append(choices, key) } sort.Strings(choices) choice, err := selectPods(choices) if err != nil { return err } selector := fmt.Sprintf("%v=%v", labelsMap[choice], choice) fmt.Fprintf(out, "Selector: %v\n", color.BlueString(selector)) labelSelector, err := labels.Parse(selector) if err != nil { return err } config.LabelSelector = labelSelector return nil } // selectPods surfaces an interactive prompt for selecting an app.kubernetes.io/instance. func selectPods(pods []string) (string, error) { arrow := survey.WithIcons(func(icons *survey.IconSet) { icons.Question.Text = "❯" icons.SelectFocus.Text = "❯" icons.Question.Format = "blue" icons.SelectFocus.Format = "blue" }) prompt := &survey.Select{ Message: "Select \"app.kubernetes.io/instance\" label value:", Options: pods, } var pod string if err := survey.AskOne(prompt, &pod, arrow); err != nil { return "", err } return pod, nil } 07070100000018000081A4000000000000000000000001664F523B00000134000000000000000000000000000000000000002100000000stern-1.30.0/cmd/flag_version.gopackage cmd import ( "fmt" "io" ) var ( version = "dev" commit = "" date = "" ) func outputVersionInfo(out io.Writer) { fmt.Fprintf(out, "version: %s\n", version) if commit != "" { fmt.Fprintf(out, "commit: %s\n", commit) } if date != "" { fmt.Fprintf(out, "built at: %s\n", date) } } 07070100000019000081A4000000000000000000000001664F523B00000434000000000000000000000000000000000000001B00000000stern-1.30.0/cmd/helper.gopackage cmd import ( "encoding/json" "strconv" "strings" "time" "github.com/spf13/cast" ) func toTime(a any) time.Time { t, _ := toTimeE(a) return t } func toTimeE(a any) (time.Time, error) { switch v := a.(type) { case string: if t, ok := parseUnixTimeNanoString(v); ok { return t, nil } case json.Number: if t, ok := parseUnixTimeNanoString(v.String()); ok { return t, nil } } return cast.ToTimeE(a) } func parseUnixTimeNanoString(num string) (time.Time, bool) { parts := strings.Split(num, ".") if len(parts) > 2 { return time.Time{}, false } sec, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { return time.Time{}, false } var nsec int64 if len(parts) == 2 { // convert fraction part to nanoseconds const digits = 9 frac := parts[1] if len(frac) > digits { frac = frac[:digits] } else if len(frac) < digits { frac = frac + strings.Repeat("0", digits-len(frac)) } nsec, err = strconv.ParseInt(frac, 10, 64) if err != nil { return time.Time{}, false } } return time.Unix(sec, nsec), true } 0707010000001A000081A4000000000000000000000001664F523B000005AA000000000000000000000000000000000000002000000000stern-1.30.0/cmd/helper_test.gopackage cmd import ( "encoding/json" "fmt" "testing" "time" ) func TestToTimeE(t *testing.T) { base := time.Date(2006, 1, 2, 3, 4, 5, 0, time.UTC) tests := []struct { arg any expected time.Time wantError bool }{ // nanoseconds {"1136171045", base, false}, {"1136171045.0", base, false}, {"1136171045.1", base.Add(1e8 * time.Nanosecond), false}, {json.Number("1136171045.1"), base.Add(1e8 * time.Nanosecond), false}, {"1136171056.02", base.Add(11*time.Second + 2e7*time.Nanosecond), false}, {"1136171045.000000001", base.Add(1 * time.Nanosecond), false}, {"1136171045.123456789", base.Add(123456789 * time.Nanosecond), false}, {"1136171045.12345678912345", base.Add(123456789 * time.Nanosecond), false}, // cast.ToTimeE {1136171045, base, false}, {"2006-01-02T03:04:05.123456789", base.Add(123456789 * time.Nanosecond), false}, // error {"", time.Time{}, true}, {".", time.Time{}, true}, {"a.b", time.Time{}, true}, {"1.a", time.Time{}, true}, {"abc", time.Time{}, true}, } for _, tt := range tests { t.Run(fmt.Sprintf("%v", tt.arg), func(t *testing.T) { tm, err := toTimeE(tt.arg) if tt.wantError { if err == nil { t.Errorf("expected error, but got no error") } return } if err != nil { t.Errorf("unexpected error: %+v", err) return } if !tt.expected.Equal(tm) { t.Errorf("expected %v, but actual %v", tt.expected, tm.UTC()) } }) } } 0707010000001B000081A4000000000000000000000001664F523B000000EE000000000000000000000000000000000000001A00000000stern-1.30.0/cmd/test.tpl{{color .PodColor .PodName}} {{color .ContainerColor .ContainerName}} {{ with $msg := .Message | tryParseJSON }}[{{ colorGreen (toRFC3339Nano (toUTC $msg.ts)) }}] {{ levelColor $msg.level }} {{ $msg.msg }}{{ else }}{{ .Message }}{{ end }}0707010000001C000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001A00000000stern-1.30.0/cmd/testdata0707010000001D000081A4000000000000000000000001664F523B0000000C000000000000000000000000000000000000002D00000000stern-1.30.0/cmd/testdata/config-array0.yamlexclude: [] 0707010000001E000081A4000000000000000000000001664F523B00000012000000000000000000000000000000000000002D00000000stern-1.30.0/cmd/testdata/config-array1.yamlexclude: ["abcd"] 0707010000001F000081A4000000000000000000000001664F523B0000001A000000000000000000000000000000000000002D00000000stern-1.30.0/cmd/testdata/config-array2.yamlexclude: ["abcd", "efgh"] 07070100000020000081A4000000000000000000000001664F523B00000000000000000000000000000000000000000000002C00000000stern-1.30.0/cmd/testdata/config-empty.yaml07070100000021000081A4000000000000000000000001664F523B0000001C000000000000000000000000000000000000002E00000000stern-1.30.0/cmd/testdata/config-invalid.yamlthis is invalid config file 07070100000022000081A4000000000000000000000001664F523B00000017000000000000000000000000000000000000002D00000000stern-1.30.0/cmd/testdata/config-string.yamlexclude: "hello-world" 07070100000023000081A4000000000000000000000001664F523B0000000E000000000000000000000000000000000000003900000000stern-1.30.0/cmd/testdata/config-tail-invalid-value.yamltail: invalid 07070100000024000081A4000000000000000000000001664F523B00000008000000000000000000000000000000000000002C00000000stern-1.30.0/cmd/testdata/config-tail1.yamltail: 1 07070100000025000081A4000000000000000000000001664F523B00000015000000000000000000000000000000000000003500000000stern-1.30.0/cmd/testdata/config-unknown-option.yamlunknown: 999 tail: 1 07070100000026000081A4000000000000000000000001664F523B00000D42000000000000000000000000000000000000001400000000stern-1.30.0/go.modmodule github.com/stern/stern go 1.22.0 toolchain go1.22.2 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/fatih/color v1.16.0 github.com/mitchellh/go-homedir v1.1.0 github.com/pkg/errors v0.9.1 github.com/spf13/cast v1.6.0 github.com/spf13/cobra v1.8.0 github.com/spf13/pflag v1.0.5 golang.org/x/sync v0.7.0 golang.org/x/time v0.5.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.30.0 k8s.io/apimachinery v0.30.0 k8s.io/cli-runtime v0.30.0 k8s.io/client-go v0.30.0 k8s.io/klog/v2 v2.120.1 k8s.io/utils v0.0.0-20240310230437-4693a0247e57 ) 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.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.0 // indirect github.com/evanphx/json-patch v5.9.0+incompatible // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.1.2 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect github.com/imdario/mergo v0.3.16 // 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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // 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/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/xlab/treeprint v1.2.0 // indirect go.starlark.net v0.0.0-20240411212711-9b43f0afd521 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.17.1 // indirect sigs.k8s.io/kustomize/kyaml v0.17.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) 07070100000027000081A4000000000000000000000001664F523B0000573C000000000000000000000000000000000000001400000000stern-1.30.0/go.sumgithub.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= 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/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 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.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.12.0 h1:y2DdzBAURM29NFF94q6RaY4vjIH1rtwDapwQtU84iWk= github.com/emicklei/go-restful/v3 v3.12.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 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.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 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 v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= 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/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/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= 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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 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/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 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/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 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/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 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.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.starlark.net v0.0.0-20240411212711-9b43f0afd521 h1:1Ufp2S2fPpj0RHIQ4rbzpCdPLCPkzdK7BaVFH3nkYBQ= go.starlark.net v0.0.0-20240411212711-9b43f0afd521/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8= 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/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 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.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= 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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= 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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 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.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 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.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 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.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= k8s.io/cli-runtime v0.30.0 h1:0vn6/XhOvn1RJ2KJOC6IRR2CGqrpT6QQF4+8pYpWQ48= k8s.io/cli-runtime v0.30.0/go.mod h1:vATpDMATVTMA79sZ0YUCzlMelf6rUjoBzlp+RnoM+cg= k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 h1:SbdLaI6mM6ffDSJCadEaD4IkuPzepLDGlkd2xV0t1uA= k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= k8s.io/utils v0.0.0-20240310230437-4693a0247e57 h1:gbqbevonBh57eILzModw6mrkbwM0gQBEuevE/AaBsHY= k8s.io/utils v0.0.0-20240310230437-4693a0247e57/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.17.1 h1:MYJBOP/yQ3/5tp4/sf6HiiMfNNyO97LmtnirH9SLNr4= sigs.k8s.io/kustomize/api v0.17.1/go.mod h1:ffn5491s2EiNrJSmgqcWGzQUVhc/pB0OKNI0HsT/0tA= sigs.k8s.io/kustomize/kyaml v0.17.0 h1:G2bWs03V9Ur2PinHLzTUJ8Ded+30SzXZKiO92SRDs3c= sigs.k8s.io/kustomize/kyaml v0.17.0/go.mod h1:6lxkYF1Cv9Ic8g/N7I86cvxNc5iinUo/P2vKsHNmpyE= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 07070100000028000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001200000000stern-1.30.0/hack07070100000029000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000002000000000stern-1.30.0/hack/update-readme0707010000002A000081A4000000000000000000000001664F523B000010FA000000000000000000000000000000000000003100000000stern-1.30.0/hack/update-readme/update-readme.gopackage main import ( "bufio" "fmt" "io" "os" "strings" "github.com/spf13/pflag" "github.com/stern/stern/cmd" "k8s.io/cli-runtime/pkg/genericclioptions" ) const ( targetSectionBegin = "<!-- auto generated cli flags begin --->" targetSectionEnd = "<!-- auto generated cli flags end --->" ) func main() { if len(os.Args) != 2 { fmt.Fprintln(os.Stderr, "readmePath argument required") os.Exit(1) } readmePath := os.Args[1] flagsMarkdownTable := GenerateFlagsMarkdownTable() readmeString, err := GenerateReadme(readmePath, flagsMarkdownTable) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } err = OverwriteReadme(readmePath, readmeString) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } // GenerateFlagsMarkdownTable generates markdown table of flag list. // This function loads flag list and generates such as following text: // // flag | default | purpose // -----------------|-----------------|--------- // `--flag1`, `-f` | | This is flag1. // `--flag2` | `flag2-default` | This is flag2. func GenerateFlagsMarkdownTable() string { fs := pflag.NewFlagSet("", pflag.ExitOnError) o := cmd.NewOptions(genericclioptions.NewTestIOStreamsDiscard()) o.AddFlags(fs) flagMaxlen, defaultMaxlen := len(" flag "), len(" default ") allTexts := make([][]string, 0) fs.VisitAll(func(flag *pflag.Flag) { // won't append deprecated flag if flag.Deprecated != "" { return } if flag.Hidden { return } flagText := "" if flag.Shorthand != "" { flagText = fmt.Sprintf(" `--%s`, `-%s` ", flag.Name, flag.Shorthand) } else { flagText = fmt.Sprintf(" `--%s` ", flag.Name) } if len(flagText) > flagMaxlen { flagMaxlen = len(flagText) } flagTypeName, usage := pflag.UnquoteUsage(flag) defaultText := "" if flag.DefValue != "" { switch flagTypeName { // convert []string{"aaa", "bbb"} to "aaa,bbb" case "strings": stirngSlice, err := fs.GetStringSlice(flag.Name) if err != nil { panic(err) } defaultValuesString := "" for _, s := range stirngSlice { defaultValuesString += fmt.Sprintf("%s,", s) } defaultValuesString = strings.TrimRight(defaultValuesString, ",") if defaultValuesString != "" { defaultText += fmt.Sprintf(" `%s` ", defaultValuesString) } default: defaultText += fmt.Sprintf(" `%s` ", flag.DefValue) } } if len(defaultText) > defaultMaxlen { defaultMaxlen = len(defaultText) } purposeText := fmt.Sprintf(" %s", usage) allTexts = append(allTexts, []string{flagText, defaultText, purposeText}) }) tableText := fmt.Sprintf( " flag %s| default %s| purpose\n", strings.Repeat(" ", flagMaxlen-len(" flag ")), strings.Repeat(" ", defaultMaxlen-len(" default "))) tableText += fmt.Sprintf( "%s|%s|%s\n", strings.Repeat("-", flagMaxlen), strings.Repeat("-", defaultMaxlen), strings.Repeat("-", len(" purpose "))) for _, text := range allTexts { tableText += text[0] tableText += strings.Repeat(" ", flagMaxlen-len(text[0])) tableText += "|" + text[1] tableText += strings.Repeat(" ", defaultMaxlen-len(text[1])) tableText += "|" + text[2] tableText += "\n" } return tableText } // GenerateReadme generates README.md in which flags markdown table is embedded. // Overwrite the section which specified as the target. func GenerateReadme(readmePath, flagsMarkdownTable string) (string, error) { f, err := os.Open(readmePath) if err != nil { return "", err } defer f.Close() buf, err := io.ReadAll(f) if err != nil { return "", err } inTargetSection := false tableText := "" scanner := bufio.NewScanner(strings.NewReader(string(buf))) for scanner.Scan() { if !inTargetSection { tableText += scanner.Text() + "\n" } if scanner.Text() == targetSectionBegin { inTargetSection = true } if scanner.Text() == targetSectionEnd { tableText += flagsMarkdownTable tableText += scanner.Text() + "\n" inTargetSection = false } } return tableText, nil } // OverwriteReadme overwrites README.md with passed readmeString. func OverwriteReadme(readmePath, readmeString string) error { f, err := os.Create(readmePath) if err != nil { return err } defer f.Close() _, err = f.WriteString(readmeString) if err != nil { return err } return nil } 0707010000002B000081ED000000000000000000000001664F523B000001D3000000000000000000000000000000000000002300000000stern-1.30.0/hack/verify-readme.sh#!/usr/bin/env bash set -eo pipefail; [[ -n "$DEBUG" ]] && set -ux ROOT_DIR="$(cd "$(dirname $0)" && pwd)/.." cd "$ROOT_DIR" tempfile="$(mktemp)" cat README.md >"$tempfile" make update-readme README_FILE="$tempfile" >/dev/null diff="$(diff -u ./README.md "$tempfile" ||:)" if [[ -n "$diff" ]]; then echo "$diff" >&2 echo "Error: Running update-readme made a difference in README.md." >&2 echo "Maybe you forgot to run 'make update-readme'." >&2 exit 1 fi 0707010000002C000081A4000000000000000000000001664F523B000003BC000000000000000000000000000000000000001500000000stern-1.30.0/main.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "log" "os" "github.com/stern/stern/cmd" "k8s.io/cli-runtime/pkg/genericclioptions" ) func main() { streams := genericclioptions.IOStreams{Out: os.Stdout, ErrOut: os.Stderr} stern, err := cmd.NewSternCmd(streams) if err != nil { log.Fatal(err) } if err := stern.Execute(); err != nil { os.Exit(1) } } 0707010000002D000041ED000000000000000000000002664F523B00000000000000000000000000000000000000000000001300000000stern-1.30.0/stern0707010000002E000081A4000000000000000000000001664F523B000008E2000000000000000000000000000000000000001C00000000stern-1.30.0/stern/color.gopackage stern import ( "errors" "strconv" "strings" "github.com/fatih/color" ) var colorList = [][2]*color.Color{ {color.New(color.FgHiCyan), color.New(color.FgCyan)}, {color.New(color.FgHiGreen), color.New(color.FgGreen)}, {color.New(color.FgHiMagenta), color.New(color.FgMagenta)}, {color.New(color.FgHiYellow), color.New(color.FgYellow)}, {color.New(color.FgHiBlue), color.New(color.FgBlue)}, {color.New(color.FgHiRed), color.New(color.FgRed)}, } func SetColorList(podColors, containerColors []string) error { colors, err := parseColors(podColors, containerColors) if err != nil { return err } colorList = colors return nil } func parseColors(podColors, containerColors []string) ([][2]*color.Color, error) { if len(podColors) == 0 { return nil, errors.New("pod-colors must not be empty") } if len(containerColors) == 0 { // if containerColors is empty, use podColors as containerColors return createColorPairs(podColors, podColors) } if len(containerColors) != len(podColors) { return nil, errors.New("pod-colors and container-colors must have the same length") } return createColorPairs(podColors, containerColors) } func createColorPairs(podColors, containerColors []string) ([][2]*color.Color, error) { colorList := make([][2]*color.Color, 0, len(podColors)) for i := 0; i < len(podColors); i++ { podColor, err := sgrSequenceToColor(podColors[i]) if err != nil { return nil, err } containerColor, err := sgrSequenceToColor(containerColors[i]) if err != nil { return nil, err } colorList = append(colorList, [2]*color.Color{podColor, containerColor}) } return colorList, nil } // sgrSequenceToColor converts a string representing SGR sequence // separated by ";" into a *color.Color instance. // For example, "31;4" means red foreground with underline. // https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_(Select_Graphic_Rendition)_parameters func sgrSequenceToColor(s string) (*color.Color, error) { parts := strings.Split(s, ";") attrs := make([]color.Attribute, 0, len(parts)) for _, part := range parts { attr, err := strconv.ParseInt(strings.TrimSpace(part), 10, 32) if err != nil { return nil, err } attrs = append(attrs, color.Attribute(attr)) } return color.New(attrs...), nil } 0707010000002F000081A4000000000000000000000001664F523B00000B34000000000000000000000000000000000000002100000000stern-1.30.0/stern/color_test.gopackage stern import ( "testing" "github.com/fatih/color" ) func TestParseColors(t *testing.T) { tests := []struct { desc string podColors []string containerColors []string want [][2]*color.Color wantError bool }{ { desc: "both pod and container colors are specified", podColors: []string{"91", "92", "93"}, containerColors: []string{"31", "32", "33"}, want: [][2]*color.Color{ {color.New(color.FgHiRed), color.New(color.FgRed)}, {color.New(color.FgHiGreen), color.New(color.FgGreen)}, {color.New(color.FgHiYellow), color.New(color.FgYellow)}, }, }, { desc: "only pod colors are specified", podColors: []string{"91", "92", "93"}, containerColors: []string{}, want: [][2]*color.Color{ {color.New(color.FgHiRed), color.New(color.FgHiRed)}, {color.New(color.FgHiGreen), color.New(color.FgHiGreen)}, {color.New(color.FgHiYellow), color.New(color.FgHiYellow)}, }, }, { desc: "multiple attributes", podColors: []string{"4;91"}, containerColors: []string{"38;2;255;97;136"}, want: [][2]*color.Color{ { color.New(color.Underline, color.FgHiRed), color.New(38, 2, 255, 97, 136), // 24-bit color }, }, }, { desc: "spaces are ignored", podColors: []string{" 91 ", "\t92\t"}, containerColors: []string{}, want: [][2]*color.Color{ {color.New(color.FgHiRed), color.New(color.FgHiRed)}, {color.New(color.FgHiGreen), color.New(color.FgHiGreen)}, }, }, // error patterns { desc: "only container colors are specified", podColors: []string{}, containerColors: []string{"31", "32", "33"}, wantError: true, }, { desc: "both pod and container colors are empty", podColors: []string{}, containerColors: []string{}, wantError: true, }, { desc: "invalid color", podColors: []string{"a"}, containerColors: []string{""}, wantError: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { colorList, err := parseColors(tt.podColors, tt.containerColors) if tt.wantError { if err == nil { t.Error("expected err, but got nil") } return } if err != nil { t.Fatalf("unexpected err: %v", err) } if len(tt.want) != len(colorList) { t.Fatalf("expected colorList of size %d, but got %d", len(tt.want), len(colorList)) } for i, wantPair := range tt.want { gotPair := colorList[i] if !wantPair[0].Equals(gotPair[0]) { t.Errorf("colorList[%d][0]: expected %v, but got %v", i, wantPair[0], gotPair[0]) } if !wantPair[1].Equals(gotPair[1]) { t.Errorf("colorList[%d][1]: expected %v, but got %v", i, wantPair[1], gotPair[1]) } } }) } } 07070100000030000081A4000000000000000000000001664F523B000006C9000000000000000000000000000000000000001D00000000stern-1.30.0/stern/config.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "io" "regexp" "text/template" "time" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" ) // Config contains the config for stern type Config struct { Namespaces []string PodQuery *regexp.Regexp ExcludePodQuery []*regexp.Regexp Timestamps bool TimestampFormat string Location *time.Location ContainerQuery *regexp.Regexp ExcludeContainerQuery []*regexp.Regexp ContainerStates []ContainerState Exclude []*regexp.Regexp Include []*regexp.Regexp Highlight []*regexp.Regexp InitContainers bool EphemeralContainers bool Since time.Duration AllNamespaces bool LabelSelector labels.Selector FieldSelector fields.Selector TailLines *int64 Template *template.Template Follow bool Resource string OnlyLogLines bool MaxLogRequests int Stdin bool DiffContainer bool Out io.Writer ErrOut io.Writer } 07070100000031000081A4000000000000000000000001664F523B0000067C000000000000000000000000000000000000002600000000stern-1.30.0/stern/container_state.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "errors" v1 "k8s.io/api/core/v1" ) type ContainerState string const ( RUNNING = "running" WAITING = "waiting" TERMINATED = "terminated" ALL_STATES = "all" ) // NewContainerState returns corresponding ContainerState func NewContainerState(stateConfig string) (ContainerState, error) { if stateConfig == RUNNING { return RUNNING, nil } else if stateConfig == WAITING { return WAITING, nil } else if stateConfig == TERMINATED { return TERMINATED, nil } else if stateConfig == ALL_STATES { return ALL_STATES, nil } return "", errors.New("containerState should be one of 'running', 'waiting', 'terminated', or 'all'") } // Match returns ContainerState is matched func (stateConfig ContainerState) Match(containerState v1.ContainerState) bool { if stateConfig == ALL_STATES { return true } return (stateConfig == RUNNING && containerState.Running != nil) || (stateConfig == WAITING && containerState.Waiting != nil) || (stateConfig == TERMINATED && containerState.Terminated != nil) } 07070100000032000081A4000000000000000000000001664F523B000007DC000000000000000000000000000000000000002B00000000stern-1.30.0/stern/container_state_test.gopackage stern import ( "testing" v1 "k8s.io/api/core/v1" ) func TestNewContainerState(t *testing.T) { tests := []struct { stateConfig string expected ContainerState isError bool }{ { "running", ContainerState(RUNNING), false, }, { "waiting", ContainerState(WAITING), false, }, { "terminated", ContainerState(TERMINATED), false, }, { "all", ContainerState(ALL_STATES), false, }, { "wrongValue", ContainerState(""), true, }, } for i, tt := range tests { containerState, err := NewContainerState(tt.stateConfig) if tt.expected != containerState { t.Errorf("%d: expected %v, but actual %v", i, tt.expected, containerState) } if (tt.isError && err == nil) || (!tt.isError && err != nil) { t.Errorf("%d: expected error is %v, but actual %v", i, tt.isError, err) } } } func TestMatch(t *testing.T) { tests := []struct { containerState ContainerState v1ContainerState v1.ContainerState expected bool }{ { ContainerState(RUNNING), v1.ContainerState{ Running: &v1.ContainerStateRunning{}, Waiting: nil, Terminated: nil, }, true, }, { ContainerState(WAITING), v1.ContainerState{ Running: nil, Waiting: &v1.ContainerStateWaiting{}, Terminated: nil, }, true, }, { ContainerState(TERMINATED), v1.ContainerState{ Running: nil, Waiting: nil, Terminated: &v1.ContainerStateTerminated{}, }, true, }, { // "all" always matches all containers regardless of their states ContainerState(ALL_STATES), v1.ContainerState{}, true, }, { ContainerState(RUNNING), v1.ContainerState{ Running: nil, Waiting: &v1.ContainerStateWaiting{}, Terminated: nil, }, false, }, } for i, tt := range tests { actual := tt.containerState.Match(tt.v1ContainerState) if tt.expected != actual { t.Errorf("%d: expected %v, but actual %v", i, tt.expected, actual) } } } 07070100000033000081A4000000000000000000000001664F523B000006B0000000000000000000000000000000000000002000000000stern-1.30.0/stern/file_tail.gopackage stern import ( "bufio" "bytes" "fmt" "io" "strings" "text/template" "github.com/fatih/color" ) type FileTail struct { Options *TailOptions tmpl *template.Template in io.Reader out io.Writer errOut io.Writer } // NewFileTail returns a new tail of the input reader func NewFileTail(tmpl *template.Template, in io.Reader, out, errOut io.Writer, options *TailOptions) *FileTail { return &FileTail{ Options: options, tmpl: tmpl, in: in, out: out, errOut: errOut, } } // Start starts tailing func (t *FileTail) Start() error { reader := bufio.NewReader(t.in) err := t.ConsumeReader(reader) return err } // ConsumeReader reads the data from the reader and writes into the out // writer. func (t *FileTail) ConsumeReader(reader *bufio.Reader) error { for { line, err := reader.ReadBytes('\n') if len(line) != 0 { t.consumeLine(strings.TrimSuffix(string(line), "\n")) } if err != nil { if err != io.EOF { return err } return nil } } } // Print prints a color coded log message func (t *FileTail) Print(msg string) { vm := Log{ Message: msg, NodeName: "", Namespace: "", PodName: "", ContainerName: "", PodColor: color.New(color.Reset), ContainerColor: color.New(color.Reset), } var buf bytes.Buffer if err := t.tmpl.Execute(&buf, vm); err != nil { fmt.Fprintf(t.errOut, "expanding template failed: %s\n", err) return } fmt.Fprint(t.out, buf.String()) } func (t *FileTail) consumeLine(line string) { content := line if t.Options.IsExclude(content) || !t.Options.IsInclude(content) { return } msg := t.Options.HighlightMatchedString(content) t.Print(msg) } 07070100000034000081A4000000000000000000000001664F523B0000035E000000000000000000000000000000000000002500000000stern-1.30.0/stern/file_tail_test.gopackage stern import ( "bufio" "bytes" "io" "strings" "testing" "text/template" ) func TestConsumeFileTail(t *testing.T) { logLines := `line 1 line 2 line 3 line 4` tmpl := template.Must(template.New("").Parse(`{{printf "%s\n" .Message}}`)) tests := []struct { name string resumeReq *ResumeRequest expected []byte }{ { name: "normal", expected: []byte(`line 1 line 2 line 3 line 4 `), }, } for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { out := new(bytes.Buffer) tail := NewFileTail(tmpl, nil, out, io.Discard, &TailOptions{}) if err := tail.ConsumeReader(bufio.NewReader(strings.NewReader(logLines))); err != nil { t.Fatalf("%d: unexpected err %v", i, err) } if !bytes.Equal(tt.expected, out.Bytes()) { t.Errorf("%d: expected %s, but actual %s", i, tt.expected, out) } }) } } 07070100000035000081A4000000000000000000000001664F523B0000097A000000000000000000000000000000000000001B00000000stern-1.30.0/stern/list.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "context" "sync" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ) // List returns a map of all 'app.kubernetes.io/instance' values. func List(ctx context.Context, client kubernetes.Interface, config *Config) (map[string]string, error) { var namespaces []string // A specific namespace is ignored if all-namespaces is provided. if config.AllNamespaces { namespaces = []string{""} } else { namespaces = config.Namespaces } labels := make(map[string]string) options := metav1.ListOptions{} wg := sync.WaitGroup{} wg.Add(len(namespaces)) // Concurrently iterate through provided namespaces. for _, n := range namespaces { go func(n string) { defer wg.Done() pods, err := client.CoreV1().Pods(n).List(ctx, options) if err != nil { return } match := "app.kubernetes.io/instance" // Iterate through pods in namespace, looking for matching labels. for _, pod := range pods.Items { key := pod.Labels[match] if key == "" { continue } labels[key] = match } }(n) } wg.Wait() return labels, nil } // ListTargets returns targets by listing and filtering pods func ListTargets(ctx context.Context, i corev1client.PodInterface, labelSelector labels.Selector, fieldSelector fields.Selector, filter *targetFilter) ([]*Target, error) { list, err := i.List(ctx, metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()}) if err != nil { return nil, err } var targets []*Target for i := range list.Items { filter.visit(&list.Items[i], func(t *Target) { targets = append(targets, t) }) } return targets, nil } 07070100000036000081A4000000000000000000000001664F523B00000986000000000000000000000000000000000000002700000000stern-1.30.0/stern/resource_matcher.go// Copyright 2017 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern // ResourceMatcher is a matcher for Kubernetes resources type ResourceMatcher struct { name string // the resource name in singular e.g. "deployment" aliases []string // the aliases of the resource e.g. "deploy" and "deployments" } // Name returns the resource name in singular func (r *ResourceMatcher) Name() string { return r.name } // AllNames returns the resource names including the aliases func (r *ResourceMatcher) AllNames() []string { return append(r.aliases, r.name) } // Matches returns if name matches one of the resource names func (r *ResourceMatcher) Matches(name string) bool { for _, n := range r.AllNames() { if n == name { return true } } return false } var ( PodMatcher = ResourceMatcher{name: "pod", aliases: []string{"po", "pods"}} ReplicationControllerMatcher = ResourceMatcher{name: "replicationcontroller", aliases: []string{"rc", "replicationcontrollers"}} ServiceMatcher = ResourceMatcher{name: "service", aliases: []string{"svc", "services"}} DaemonSetMatcher = ResourceMatcher{name: "daemonset", aliases: []string{"ds", "daemonsets"}} DeploymentMatcher = ResourceMatcher{name: "deployment", aliases: []string{"deploy", "deployments"}} ReplicaSetMatcher = ResourceMatcher{name: "replicaset", aliases: []string{"rs", "replicasets"}} StatefulSetMatcher = ResourceMatcher{name: "statefulset", aliases: []string{"sts", "statefulsets"}} JobMatcher = ResourceMatcher{name: "job", aliases: []string{"jobs"}} // job does not have a short name ResourceMatchers = []ResourceMatcher{ PodMatcher, ReplicationControllerMatcher, ServiceMatcher, DeploymentMatcher, DaemonSetMatcher, ReplicaSetMatcher, StatefulSetMatcher, JobMatcher, } ) 07070100000037000081A4000000000000000000000001664F523B00002010000000000000000000000000000000000000001C00000000stern-1.30.0/stern/stern.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "context" "fmt" "os" "regexp" "strings" "time" "sync/atomic" "github.com/pkg/errors" "golang.org/x/sync/errgroup" "golang.org/x/time/rate" "k8s.io/apimachinery/pkg/labels" "k8s.io/utils/ptr" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) // Run starts the main run loop func Run(ctx context.Context, client kubernetes.Interface, config *Config) error { var namespaces []string // A specific namespace is ignored if all-namespaces is provided if config.AllNamespaces { namespaces = []string{""} } else { namespaces = config.Namespaces if len(namespaces) == 0 { return errors.New("no namespace specified") } } newTailOptions := func() *TailOptions { return &TailOptions{ Timestamps: config.Timestamps, TimestampFormat: config.TimestampFormat, Location: config.Location, SinceSeconds: ptr.To[int64](int64(config.Since.Seconds())), Exclude: config.Exclude, Include: config.Include, Highlight: config.Highlight, Namespace: config.AllNamespaces || len(namespaces) > 1, TailLines: config.TailLines, Follow: config.Follow, OnlyLogLines: config.OnlyLogLines, } } newTail := func(t *Target) *Tail { return NewTail(client.CoreV1(), t.Node, t.Namespace, t.Pod, t.Container, config.Template, config.Out, config.ErrOut, newTailOptions(), config.DiffContainer) } if config.Stdin { tail := NewFileTail(config.Template, os.Stdin, config.Out, config.ErrOut, newTailOptions()) return tail.Start() } var resource struct { kind string name string } if config.Resource != "" { parts := strings.Split(config.Resource, "/") if len(parts) != 2 { return errors.New("resource must be specified in the form \"<resource>/<name>\"") } resource.kind, resource.name = parts[0], parts[1] if PodMatcher.Matches(resource.kind) { // Pods might have no labels or share the same labels, // so we use an exact match instead. podName, err := regexp.Compile("^" + resource.name + "$") if err != nil { return errors.Wrap(err, "failed to compile regular expression for pod") } config.PodQuery = podName } } filter := newTargetFilter(targetFilterConfig{ podFilter: config.PodQuery, excludePodFilter: config.ExcludePodQuery, containerFilter: config.ContainerQuery, containerExcludeFilter: config.ExcludeContainerQuery, initContainers: config.InitContainers, ephemeralContainers: config.EphemeralContainers, containerStates: config.ContainerStates, }) if !config.Follow { var eg errgroup.Group eg.SetLimit(config.MaxLogRequests) for _, n := range namespaces { selector, err := chooseSelector(ctx, client, n, resource.kind, resource.name, config.LabelSelector) if err != nil { return err } targets, err := ListTargets(ctx, client.CoreV1().Pods(n), selector, config.FieldSelector, filter, ) if err != nil { return err } for _, t := range targets { t := t eg.Go(func() error { tail := newTail(t) defer tail.Close() return tail.Start(ctx) }) } } return eg.Wait() } tailTarget := func(ctx context.Context, target *Target) { // We use a rate limiter to prevent a burst of retries. // It also enables us to retry immediately, in most cases, // when it is disconnected on the way. limiter := rate.NewLimiter(rate.Every(time.Second*20), 2) var resumeRequest *ResumeRequest for { if err := limiter.Wait(ctx); err != nil { fmt.Fprintf(config.ErrOut, "failed to retry: %v\n", err) return } tail := newTail(target) var err error if resumeRequest == nil { err = tail.Start(ctx) } else { err = tail.Resume(ctx, resumeRequest) } tail.Close() if err == nil { return } if !filter.isActive(target) { fmt.Fprintf(config.ErrOut, "failed to tail: %v\n", err) return } fmt.Fprintf(config.ErrOut, "failed to tail: %v, will retry\n", err) if resumeReq := tail.GetResumeRequest(); resumeReq != nil { resumeRequest = resumeReq } } } eg, nctx := errgroup.WithContext(ctx) var numRequests atomic.Int64 for _, n := range namespaces { selector, err := chooseSelector(nctx, client, n, resource.kind, resource.name, config.LabelSelector) if err != nil { return err } a, err := WatchTargets(nctx, client.CoreV1().Pods(n), selector, config.FieldSelector, filter, ) if err != nil { return errors.Wrap(err, "failed to set up watch") } eg.Go(func() error { for { select { case target, ok := <-a: if !ok { return fmt.Errorf("lost watch connection") } numRequests.Add(1) if numRequests.Load() > int64(config.MaxLogRequests) { return fmt.Errorf( "stern reached the maximum number of log requests (%d),"+ " use --max-log-requests to increase the limit", config.MaxLogRequests) } go func() { tailTarget(nctx, target) numRequests.Add(-1) }() case <-nctx.Done(): return nil } } }) } return eg.Wait() } func chooseSelector(ctx context.Context, client kubernetes.Interface, namespace, kind, name string, selector labels.Selector) (labels.Selector, error) { if kind == "" { return selector, nil } if PodMatcher.Matches(kind) { // We use an exact match for pods instead of a label to select pods without labels. return labels.Everything(), nil } labelMap, err := retrieveLabelsFromResource(ctx, client, namespace, kind, name) if err != nil { return nil, err } if len(labelMap) == 0 { return nil, fmt.Errorf("resource %s/%s has no labels to select", kind, name) } return labels.SelectorFromSet(labelMap), nil } func retrieveLabelsFromResource(ctx context.Context, client kubernetes.Interface, namespace, kind, name string) (map[string]string, error) { opt := metav1.GetOptions{} switch { // core case ReplicationControllerMatcher.Matches(kind): o, err := client.CoreV1().ReplicationControllers(namespace).Get(ctx, name, opt) if err != nil { return nil, err } if o.Spec.Template == nil { // RC's spec.template is a pointer field return nil, fmt.Errorf("%s does not have spec.template", name) } return o.Spec.Template.Labels, nil case ServiceMatcher.Matches(kind): o, err := client.CoreV1().Services(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Selector, nil // apps case DaemonSetMatcher.Matches(kind): o, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Template.Labels, nil case DeploymentMatcher.Matches(kind): o, err := client.AppsV1().Deployments(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Template.Labels, nil case ReplicaSetMatcher.Matches(kind): o, err := client.AppsV1().ReplicaSets(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Template.Labels, nil case StatefulSetMatcher.Matches(kind): o, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Template.Labels, nil // batch // We do not support cronjobs because they might not have labels to select. case JobMatcher.Matches(kind): o, err := client.BatchV1().Jobs(namespace).Get(ctx, name, opt) if err != nil { return nil, err } return o.Spec.Template.Labels, nil } return nil, fmt.Errorf("resource type %s is not supported", kind) } 07070100000038000081A4000000000000000000000001664F523B0000104F000000000000000000000000000000000000002100000000stern-1.30.0/stern/stern_test.gopackage stern import ( "context" "reflect" "testing" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/kubernetes/fake" ) func TestRetrieveLabelsFromResource(t *testing.T) { genMeta := func(name string) metav1.ObjectMeta { return metav1.ObjectMeta{ Name: name, Namespace: "ns1", } } genPodTemplateSpec := func(name string) corev1.PodTemplateSpec { return corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "app": name, }, }, } } objs := []runtime.Object{ // core &corev1.ReplicationController{ ObjectMeta: genMeta("rc1"), Spec: corev1.ReplicationControllerSpec{ Template: &corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ "app": "rc-label", }, }, }, }, }, &corev1.Service{ ObjectMeta: genMeta("svc1"), Spec: corev1.ServiceSpec{ Selector: map[string]string{ "app": "svc-label", }, }, }, // apps &appsv1.DaemonSet{ ObjectMeta: genMeta("ds1"), Spec: appsv1.DaemonSetSpec{ Template: genPodTemplateSpec("ds-label"), }, }, &appsv1.Deployment{ ObjectMeta: genMeta("deploy1"), Spec: appsv1.DeploymentSpec{ Template: genPodTemplateSpec("deploy-label"), }, }, &appsv1.ReplicaSet{ ObjectMeta: genMeta("rs1"), Spec: appsv1.ReplicaSetSpec{ Template: genPodTemplateSpec("rs-label"), }, }, &appsv1.StatefulSet{ ObjectMeta: genMeta("sts1"), Spec: appsv1.StatefulSetSpec{ Template: genPodTemplateSpec("sts-label"), }, }, // batch &batchv1.Job{ ObjectMeta: genMeta("job1"), Spec: batchv1.JobSpec{ Template: genPodTemplateSpec("job-label"), }, }, &batchv1.CronJob{ ObjectMeta: genMeta("cj1"), Spec: batchv1.CronJobSpec{ JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: genPodTemplateSpec("cj-label"), }, }, }, }, } client := fake.NewSimpleClientset(objs...) tests := []struct { desc string kinds []string name string label string wantError bool }{ // core { desc: "replicationcontrollers", kinds: []string{"rc", "replicationcontrollers", "replicationcontroller"}, name: "rc1", label: "rc-label", }, { desc: "services", kinds: []string{"svc", "services", "service"}, name: "svc1", label: "svc-label", }, // apps { desc: "daemonsets", kinds: []string{"ds", "daemonsets", "daemonset"}, name: "ds1", label: "ds-label", }, { desc: "deployments", kinds: []string{"deploy", "deployments", "deployment"}, name: "deploy1", label: "deploy-label", }, { desc: "replicasets", kinds: []string{"rs", "replicasets", "replicaset"}, name: "rs1", label: "rs-label", }, { desc: "statefulsets", kinds: []string{"sts", "statefulsets", "statefulset"}, name: "sts1", label: "sts-label", }, // batch { desc: "jobs", kinds: []string{"job", "jobs"}, name: "job1", label: "job-label", }, // invalid { desc: "invalid", kinds: []string{"", "unknown"}, name: "dummy", wantError: true, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { for _, kind := range tt.kinds { labels, err := retrieveLabelsFromResource(context.Background(), client, "ns1", kind, tt.name) if tt.wantError { if err == nil { t.Errorf("expected error, but got no error") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } expectedLabels := map[string]string{"app": tt.label} if !reflect.DeepEqual(expectedLabels, labels) { t.Errorf("expected %v, but actual %v", expectedLabels, labels) } // test not found _, err = retrieveLabelsFromResource(context.Background(), client, "ns1", kind, "not-found") if !kerrors.IsNotFound(err) { t.Errorf("expected not found, but actual %v", err) } } }) } } 07070100000039000081A4000000000000000000000001664F523B00001FE3000000000000000000000000000000000000001B00000000stern-1.30.0/stern/tail.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "bufio" "bytes" "context" "errors" "fmt" "hash/fnv" "io" "strings" "text/template" "time" "unicode" "github.com/fatih/color" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1client "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/rest" ) // RFC3339Nano with trailing zeros const TimestampFormatDefault = "2006-01-02T15:04:05.000000000Z07:00" // time.DateTime without year const TimestampFormatShort = "01-02 15:04:05" type Tail struct { clientset corev1client.CoreV1Interface NodeName string Namespace string PodName string ContainerName string Options *TailOptions closed chan struct{} podColor *color.Color containerColor *color.Color tmpl *template.Template last struct { timestamp string // RFC3339 timestamp (not RFC3339Nano) lines int // the number of lines seen during this timestamp } resumeRequest *ResumeRequest out io.Writer errOut io.Writer } type ResumeRequest struct { Timestamp string // RFC3339 timestamp (not RFC3339Nano) LinesToSkip int // the number of lines to skip during this timestamp } // NewTail returns a new tail for a Kubernetes container inside a pod func NewTail(clientset corev1client.CoreV1Interface, nodeName, namespace, podName, containerName string, tmpl *template.Template, out, errOut io.Writer, options *TailOptions, diffContainer bool) *Tail { podColor, containerColor := determineColor(podName, containerName, diffContainer) return &Tail{ clientset: clientset, NodeName: nodeName, Namespace: namespace, PodName: podName, ContainerName: containerName, Options: options, closed: make(chan struct{}), tmpl: tmpl, podColor: podColor, containerColor: containerColor, out: out, errOut: errOut, } } func determineColor(podName, containerName string, diffContainer bool) (podColor, containerColor *color.Color) { colors := colorList[colorIndex(podName)] if diffContainer { return colors[0], colorList[colorIndex(containerName)][1] } return colors[0], colors[1] } func colorIndex(name string) uint32 { hash := fnv.New32() _, _ = hash.Write([]byte(name)) return hash.Sum32() % uint32(len(colorList)) } // Start starts tailing func (t *Tail) Start(ctx context.Context) error { ctx, cancel := context.WithCancel(ctx) go func() { <-t.closed cancel() }() t.printStarting() req := t.clientset.Pods(t.Namespace).GetLogs(t.PodName, &corev1.PodLogOptions{ Follow: t.Options.Follow, Timestamps: true, Container: t.ContainerName, SinceSeconds: t.Options.SinceSeconds, SinceTime: t.Options.SinceTime, TailLines: t.Options.TailLines, }) err := t.ConsumeRequest(ctx, req) if errors.Is(err, context.Canceled) { return nil } return err } func (t *Tail) Resume(ctx context.Context, resumeRequest *ResumeRequest) error { sinceTime, err := resumeRequest.sinceTime() if err != nil { fmt.Fprintf(t.errOut, "failed to resume: %s, fallback to Start()\n", err) return t.Start(ctx) } t.resumeRequest = resumeRequest t.Options.SinceTime = sinceTime t.Options.SinceSeconds = nil t.Options.TailLines = nil return t.Start(ctx) } // Close stops tailing func (t *Tail) Close() { t.printStopping() close(t.closed) } func (t *Tail) printStarting() { if !t.Options.OnlyLogLines { g := color.New(color.FgHiGreen, color.Bold).SprintFunc() p := t.podColor.SprintFunc() c := t.containerColor.SprintFunc() if t.Options.Namespace { fmt.Fprintf(t.errOut, "%s %s %s › %s\n", g("+"), p(t.Namespace), p(t.PodName), c(t.ContainerName)) } else { fmt.Fprintf(t.errOut, "%s %s › %s\n", g("+"), p(t.PodName), c(t.ContainerName)) } } } func (t *Tail) printStopping() { if !t.Options.OnlyLogLines { r := color.New(color.FgHiRed, color.Bold).SprintFunc() p := t.podColor.SprintFunc() c := t.containerColor.SprintFunc() if t.Options.Namespace { fmt.Fprintf(t.errOut, "%s %s %s › %s\n", r("-"), p(t.Namespace), p(t.PodName), c(t.ContainerName)) } else { fmt.Fprintf(t.errOut, "%s %s › %s\n", r("-"), p(t.PodName), c(t.ContainerName)) } } } // ConsumeRequest reads the data from request and writes into the out // writer. func (t *Tail) ConsumeRequest(ctx context.Context, request rest.ResponseWrapper) error { stream, err := request.Stream(ctx) if err != nil { return err } defer stream.Close() r := bufio.NewReader(stream) for { line, err := r.ReadBytes('\n') if len(line) != 0 { t.consumeLine(strings.TrimSuffix(string(line), "\n")) } if err != nil { if err != io.EOF { return err } return nil } } } // Print prints a color coded log message with the pod and container names func (t *Tail) Print(msg string) { vm := Log{ Message: msg, NodeName: t.NodeName, Namespace: t.Namespace, PodName: t.PodName, ContainerName: t.ContainerName, PodColor: t.podColor, ContainerColor: t.containerColor, } var buf bytes.Buffer if err := t.tmpl.Execute(&buf, vm); err != nil { fmt.Fprintf(t.errOut, "expanding template failed: %s\n", err) return } fmt.Fprint(t.out, buf.String()) } func (t *Tail) GetResumeRequest() *ResumeRequest { if t.last.timestamp == "" { return nil } return &ResumeRequest{Timestamp: t.last.timestamp, LinesToSkip: t.last.lines} } func (t *Tail) consumeLine(line string) { rfc3339Nano, content, err := splitLogLine(line) if err != nil { t.Print(fmt.Sprintf("[%v] %s", err, line)) return } // PodLogOptions.SinceTime is RFC3339, not RFC3339Nano. // We convert it to RFC3339 to skip the lines seen during this timestamp when resuming. rfc3339 := removeSubsecond(rfc3339Nano) t.rememberLastTimestamp(rfc3339) if t.resumeRequest.shouldSkip(rfc3339) { return } if t.Options.IsExclude(content) || !t.Options.IsInclude(content) { return } msg := t.Options.HighlightMatchedString(content) if t.Options.Timestamps { updatedTs, err := t.Options.UpdateTimezoneAndFormat(rfc3339Nano) if err != nil { t.Print(fmt.Sprintf("[%v] %s", err, line)) return } msg = updatedTs + " " + msg } t.Print(msg) } func (t *Tail) rememberLastTimestamp(timestamp string) { if t.last.timestamp == timestamp { t.last.lines++ return } t.last.timestamp = timestamp t.last.lines = 1 } func (r *ResumeRequest) sinceTime() (*metav1.Time, error) { sinceTime, err := time.Parse(time.RFC3339, r.Timestamp) if err != nil { return nil, err } metaTime := metav1.NewTime(sinceTime) return &metaTime, nil } func (r *ResumeRequest) shouldSkip(timestamp string) bool { if r == nil { return false } if r.Timestamp == "" { return false } if r.Timestamp != timestamp { return false } if r.LinesToSkip <= 0 { return false } r.LinesToSkip-- return true } func splitLogLine(line string) (timestamp string, content string, err error) { idx := strings.IndexRune(line, ' ') if idx == -1 { return "", "", errors.New("missing timestamp") } return line[:idx], line[idx+1:], nil } // removeSubsecond removes the subsecond of the timestamp. // It converts RFC3339Nano to RFC3339 fast. func removeSubsecond(timestamp string) string { dot := strings.IndexRune(timestamp, '.') if dot == -1 { return timestamp } var last int for i := dot; i < len(timestamp); i++ { if unicode.IsDigit(rune(timestamp[i])) { last = i } } if last == 0 { return timestamp } return timestamp[:dot] + timestamp[last+1:] } 0707010000003A000081A4000000000000000000000001664F523B00001D29000000000000000000000000000000000000002000000000stern-1.30.0/stern/tail_test.gopackage stern import ( "bytes" "context" "io" "reflect" "testing" "text/template" "k8s.io/client-go/kubernetes/fake" ) func TestDetermineColor(t *testing.T) { podName := "stern" containerName := "foo" diffContainer := false podColor1, containerColor1 := determineColor(podName, containerName, diffContainer) podColor2, containerColor2 := determineColor(podName, containerName, diffContainer) if podColor1 != podColor2 { t.Errorf("expected color for pod to be the same between invocations but was %v and %v", podColor1, podColor2) } if containerColor1 != containerColor2 { t.Errorf("expected color for container to be the same between invocations but was %v and %v", containerColor1, containerColor2) } } func TestDetermineColorDiffContainer(t *testing.T) { podName := "stern" containerName1 := "foo" containerName2 := "bar" diffContainer := true podColor1, containerColor1 := determineColor(podName, containerName1, diffContainer) podColor2, containerColor2 := determineColor(podName, containerName2, diffContainer) if podColor1 != podColor2 { t.Errorf("expected color for pod to be the same between invocations but was %v and %v", podColor1, podColor2) } if containerColor1 == containerColor2 { t.Errorf("expected color for container to be different between invocations but was the same: %v", containerColor1) } } func TestConsumeStreamTail(t *testing.T) { logLines := `2023-02-13T21:20:30.000000001Z line 1 2023-02-13T21:20:30.000000002Z line 2 2023-02-13T21:20:31.000000001Z line 3 2023-02-13T21:20:31.000000002Z line 4` tmpl := template.Must(template.New("").Parse(`{{printf "%s (%s/%s/%s/%s)\n" .Message .NodeName .Namespace .PodName .ContainerName}}`)) tests := []struct { name string resumeReq *ResumeRequest expected []byte }{ { name: "normal", expected: []byte(`line 1 (my-node/my-namespace/my-pod/my-container) line 2 (my-node/my-namespace/my-pod/my-container) line 3 (my-node/my-namespace/my-pod/my-container) line 4 (my-node/my-namespace/my-pod/my-container) `), }, { name: "ResumeRequest LinesToSkip=1", resumeReq: &ResumeRequest{Timestamp: "2023-02-13T21:20:30Z", LinesToSkip: 1}, expected: []byte(`line 2 (my-node/my-namespace/my-pod/my-container) line 3 (my-node/my-namespace/my-pod/my-container) line 4 (my-node/my-namespace/my-pod/my-container) `), }, { name: "ResumeRequest LinesToSkip=2", resumeReq: &ResumeRequest{Timestamp: "2023-02-13T21:20:30Z", LinesToSkip: 2}, expected: []byte(`line 3 (my-node/my-namespace/my-pod/my-container) line 4 (my-node/my-namespace/my-pod/my-container) `), }, { name: "ResumeRequest LinesToSkip=3 (exceed)", resumeReq: &ResumeRequest{Timestamp: "2023-02-13T21:20:30Z", LinesToSkip: 3}, expected: []byte(`line 3 (my-node/my-namespace/my-pod/my-container) line 4 (my-node/my-namespace/my-pod/my-container) `), }, { name: "ResumeRequest does not match", resumeReq: &ResumeRequest{Timestamp: "2222-22-22T21:20:30Z", LinesToSkip: 3}, expected: []byte(`line 1 (my-node/my-namespace/my-pod/my-container) line 2 (my-node/my-namespace/my-pod/my-container) line 3 (my-node/my-namespace/my-pod/my-container) line 4 (my-node/my-namespace/my-pod/my-container) `), }, } clientset := fake.NewSimpleClientset() for i, tt := range tests { t.Run(tt.name, func(t *testing.T) { out := new(bytes.Buffer) tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", tmpl, out, io.Discard, &TailOptions{}, false) tail.resumeRequest = tt.resumeReq if err := tail.ConsumeRequest(context.TODO(), &responseWrapperMock{data: bytes.NewBufferString(logLines)}); err != nil { t.Fatalf("%d: unexpected err %v", i, err) } if !bytes.Equal(tt.expected, out.Bytes()) { t.Errorf("%d: expected %s, but actual %s", i, tt.expected, out) } }) } } type responseWrapperMock struct { data io.Reader } func (r *responseWrapperMock) DoRaw(context.Context) ([]byte, error) { data, _ := io.ReadAll(r.data) return data, nil } func (r *responseWrapperMock) Stream(context.Context) (io.ReadCloser, error) { return io.NopCloser(r.data), nil } func TestPrintStarting(t *testing.T) { tests := []struct { options *TailOptions expected []byte }{ { &TailOptions{}, []byte("+ my-pod › my-container\n"), }, { &TailOptions{ Namespace: true, }, []byte("+ my-namespace my-pod › my-container\n"), }, { &TailOptions{ OnlyLogLines: true, }, []byte{}, }, { &TailOptions{ Namespace: true, OnlyLogLines: true, }, []byte{}, }, } clientset := fake.NewSimpleClientset() for i, tt := range tests { errOut := new(bytes.Buffer) tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false) tail.printStarting() if !bytes.Equal(tt.expected, errOut.Bytes()) { t.Errorf("%d: expected %q, but actual %q", i, tt.expected, errOut) } } } func TestPrintStopping(t *testing.T) { tests := []struct { options *TailOptions expected []byte }{ { &TailOptions{}, []byte("- my-pod › my-container\n"), }, { &TailOptions{ Namespace: true, }, []byte("- my-namespace my-pod › my-container\n"), }, { &TailOptions{ OnlyLogLines: true, }, []byte{}, }, { &TailOptions{ Namespace: true, OnlyLogLines: true, }, []byte{}, }, } clientset := fake.NewSimpleClientset() for i, tt := range tests { errOut := new(bytes.Buffer) tail := NewTail(clientset.CoreV1(), "my-node", "my-namespace", "my-pod", "my-container", nil, io.Discard, errOut, tt.options, false) tail.printStopping() if !bytes.Equal(tt.expected, errOut.Bytes()) { t.Errorf("%d: expected %q, but actual %q", i, tt.expected, errOut) } } } func TestResumeRequestShouldSkip(t *testing.T) { tests := []struct { rr ResumeRequest timestamps []string expected []bool }{ { rr: ResumeRequest{Timestamp: "t1", LinesToSkip: 1}, timestamps: []string{"t1", "t1"}, expected: []bool{true, false}, }, { rr: ResumeRequest{Timestamp: "t1", LinesToSkip: 3}, timestamps: []string{"t1", "t1", "t1", "t1"}, expected: []bool{true, true, true, false}, }, { rr: ResumeRequest{Timestamp: "t1", LinesToSkip: 3}, timestamps: []string{"t2", "t2"}, expected: []bool{false, false}, }, } for _, tt := range tests { var actual []bool for _, ts := range tt.timestamps { actual = append(actual, tt.rr.shouldSkip(ts)) } if !reflect.DeepEqual(tt.expected, actual) { t.Errorf("expected %v, but actual %v", tt.expected, actual) } } } func TestRemoveSubsecond(t *testing.T) { tests := []struct { ts string expected string }{ { ts: "2023-02-14T05:36:39.902767599Z", expected: "2023-02-14T05:36:39Z", }, { ts: "2023-02-14T05:36:39.1Z", expected: "2023-02-14T05:36:39Z", }, { ts: "2023-02-14T05:36:39Z", expected: "2023-02-14T05:36:39Z", }, { ts: "1.1", expected: "1", }, { ts: "10.1", expected: "10", }, { ts: "", expected: "", }, { ts: ".", expected: ".", }, { ts: ".1", expected: "", }, } for _, tt := range tests { actual := removeSubsecond(tt.ts) if tt.expected != actual { t.Errorf("expected %v, but actual %v", tt.expected, actual) } } } 0707010000003B000081A4000000000000000000000001664F523B00000994000000000000000000000000000000000000002100000000stern-1.30.0/stern/tail_utils.gopackage stern import ( "errors" "regexp" "sort" "strings" "time" "github.com/fatih/color" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Log is the object which will be used together with the template to generate // the output. type Log struct { // Message is the log message itself Message string `json:"message"` // Node name of the pod NodeName string `json:"nodeName"` // Namespace of the pod Namespace string `json:"namespace"` // PodName of the pod PodName string `json:"podName"` // ContainerName of the container ContainerName string `json:"containerName"` PodColor *color.Color `json:"-"` ContainerColor *color.Color `json:"-"` } type TailOptions struct { Timestamps bool TimestampFormat string Location *time.Location SinceSeconds *int64 SinceTime *metav1.Time Exclude []*regexp.Regexp Include []*regexp.Regexp Highlight []*regexp.Regexp Namespace bool TailLines *int64 Follow bool OnlyLogLines bool // regexp for highlighting the matched string reHightlight *regexp.Regexp } func (o TailOptions) IsExclude(msg string) bool { for _, rex := range o.Exclude { if rex.MatchString(msg) { return true } } return false } func (o TailOptions) IsInclude(msg string) bool { if len(o.Include) == 0 { return true } for _, rin := range o.Include { if rin.MatchString(msg) { return true } } return false } var colorHighlight = color.New(color.FgRed, color.Bold).SprintFunc() func (o TailOptions) HighlightMatchedString(msg string) string { highlight := append(o.Include, o.Highlight...) if len(highlight) == 0 { return msg } if o.reHightlight == nil { ss := make([]string, len(highlight)) for i, hl := range highlight { ss[i] = hl.String() } // We expect a longer match sort.Slice(ss, func(i, j int) bool { return len(ss[i]) > len(ss[j]) }) o.reHightlight = regexp.MustCompile("(" + strings.Join(ss, "|") + ")") } msg = o.reHightlight.ReplaceAllStringFunc(msg, func(part string) string { return colorHighlight(part) }) return msg } func (o TailOptions) UpdateTimezoneAndFormat(timestamp string) (string, error) { t, err := time.ParseInLocation(time.RFC3339Nano, timestamp, time.UTC) if err != nil { return "", errors.New("missing timestamp") } format := TimestampFormatDefault if o.TimestampFormat != "" { format = o.TimestampFormat } return t.In(o.Location).Format(format), nil } 0707010000003C000081A4000000000000000000000001664F523B00001A27000000000000000000000000000000000000002600000000stern-1.30.0/stern/tail_utils_test.gopackage stern import ( "fmt" "regexp" "testing" "time" "github.com/fatih/color" ) func TestIsIncludeTestOptions(t *testing.T) { msg := "this is a log message" tests := []struct { include []*regexp.Regexp expected bool }{ { include: []*regexp.Regexp{}, expected: true, }, { include: []*regexp.Regexp{ regexp.MustCompile(`this is not`), }, expected: false, }, { include: []*regexp.Regexp{ regexp.MustCompile(`this is`), }, expected: true, }, } for i, tt := range tests { o := &TailOptions{Include: tt.include} if o.IsInclude(msg) != tt.expected { t.Errorf("%d: expected %s, but actual %s", i, fmt.Sprint(tt.expected), fmt.Sprint(!tt.expected)) } } } func TestUpdateTimezoneAndFormat(t *testing.T) { location, _ := time.LoadLocation("Asia/Tokyo") tests := []struct { name string format string message string expected string err string }{ { "normal case", "", // default format is used if empty "2021-04-18T03:54:44.764981564Z", "2021-04-18T12:54:44.764981564+09:00", "", }, { "padding", "", "2021-04-18T03:54:44.764981500Z", "2021-04-18T12:54:44.764981500+09:00", "", }, { "timestamp required on non timestamp message", "", "", "", "missing timestamp", }, { "not UTC", "", "2021-08-03T01:26:29.953994922+02:00", "2021-08-03T08:26:29.953994922+09:00", "", }, { "RFC3339Nano format removed trailing zeros", "", "2021-06-20T08:20:30.331385Z", "2021-06-20T17:20:30.331385000+09:00", "", }, { "Specified the short format", TimestampFormatShort, "2021-06-20T08:20:30.331385Z", "06-20 17:20:30", "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tailOptions := &TailOptions{ Location: location, TimestampFormat: tt.format, } message, err := tailOptions.UpdateTimezoneAndFormat(tt.message) if tt.expected != message { t.Errorf("expected %q, but actual %q", tt.expected, message) } if err != nil && tt.err != err.Error() { t.Errorf("expected %q, but actual %q", tt.err, err) } }) } } func TestHighlighIncludedString(t *testing.T) { tests := []struct { msg string include []*regexp.Regexp expected string }{ { "test matched", []*regexp.Regexp{ regexp.MustCompile(`test`), }, "\x1b[31;1mtest\x1b[0;22m matched", }, { "test not-matched", []*regexp.Regexp{ regexp.MustCompile(`hoge`), }, "test not-matched", }, { "test matched", []*regexp.Regexp{ regexp.MustCompile(`not-matched`), regexp.MustCompile(`matched`), }, "test \x1b[31;1mmatched\x1b[0;22m", }, { "test multiple matched", []*regexp.Regexp{ regexp.MustCompile(`multiple`), regexp.MustCompile(`matched`), }, "test \x1b[31;1mmultiple\x1b[0;22m \x1b[31;1mmatched\x1b[0;22m", }, { "test match on the longer one", []*regexp.Regexp{ regexp.MustCompile(`match`), regexp.MustCompile(`match on the longer one`), }, "test \x1b[31;1mmatch on the longer one\x1b[0;22m", }, } orig := color.NoColor color.NoColor = false defer func() { color.NoColor = orig }() for i, tt := range tests { o := &TailOptions{Include: tt.include} actual := o.HighlightMatchedString(tt.msg) if actual != tt.expected { t.Errorf("%d: expected %q, but actual %q", i, tt.expected, actual) } } } func TestIncludeAndHighlightMatchedString(t *testing.T) { tests := []struct { msg string include []*regexp.Regexp highlight []*regexp.Regexp expected string }{ { "test matched with highlight", []*regexp.Regexp{ regexp.MustCompile(`test`), }, []*regexp.Regexp{ regexp.MustCompile(`highlight`), }, "\x1b[31;1mtest\x1b[0;22m matched with \x1b[31;1mhighlight\x1b[0;22m", }, { "test not-matched", []*regexp.Regexp{ regexp.MustCompile(`hoge`), }, []*regexp.Regexp{ regexp.MustCompile(`highlight`), }, "test not-matched", }, { "test matched with highlight", []*regexp.Regexp{ regexp.MustCompile(`not-matched`), regexp.MustCompile(`matched`), }, []*regexp.Regexp{ regexp.MustCompile(`no-with-highlight`), regexp.MustCompile(`with highlight`), }, "test \x1b[31;1mmatched\x1b[0;22m \x1b[31;1mwith highlight\x1b[0;22m", }, { "test multiple matched with many highlight", []*regexp.Regexp{ regexp.MustCompile(`multiple`), regexp.MustCompile(`matched`), }, []*regexp.Regexp{ regexp.MustCompile(`many`), regexp.MustCompile(`highlight`), }, "test \x1b[31;1mmultiple\x1b[0;22m \x1b[31;1mmatched\x1b[0;22m with \x1b[31;1mmany\x1b[0;22m \x1b[31;1mhighlight\x1b[0;22m", }, { "test match on the longer one", []*regexp.Regexp{ regexp.MustCompile(`match`), regexp.MustCompile(`match on the longer one`), }, []*regexp.Regexp{ regexp.MustCompile(`match`), regexp.MustCompile(`match on the longer one`), }, "test \x1b[31;1mmatch on the longer one\x1b[0;22m", }, } orig := color.NoColor color.NoColor = false defer func() { color.NoColor = orig }() for i, tt := range tests { o := &TailOptions{Include: tt.include, Highlight: tt.highlight} actual := o.HighlightMatchedString(tt.msg) if actual != tt.expected { t.Errorf("%d: expected %q, but actual %q", i, tt.expected, actual) } } } func TestHighlightMatchedString(t *testing.T) { tests := []struct { msg string highlight []*regexp.Regexp expected string }{ { "test matched", []*regexp.Regexp{ regexp.MustCompile(`test`), }, "\x1b[31;1mtest\x1b[0;22m matched", }, { "test not-matched", []*regexp.Regexp{ regexp.MustCompile(`hoge`), }, "test not-matched", }, { "test matched", []*regexp.Regexp{ regexp.MustCompile(`not-matched`), regexp.MustCompile(`matched`), }, "test \x1b[31;1mmatched\x1b[0;22m", }, { "test multiple matched", []*regexp.Regexp{ regexp.MustCompile(`multiple`), regexp.MustCompile(`matched`), }, "test \x1b[31;1mmultiple\x1b[0;22m \x1b[31;1mmatched\x1b[0;22m", }, { "test match on the longer one", []*regexp.Regexp{ regexp.MustCompile(`match`), regexp.MustCompile(`match on the longer one`), }, "test \x1b[31;1mmatch on the longer one\x1b[0;22m", }, } orig := color.NoColor color.NoColor = false defer func() { color.NoColor = orig }() for i, tt := range tests { o := &TailOptions{Highlight: tt.highlight} actual := o.HighlightMatchedString(tt.msg) if actual != tt.expected { t.Errorf("%d: expected %q, but actual %q", i, tt.expected, actual) } } } 0707010000003D000081A4000000000000000000000001664F523B00001631000000000000000000000000000000000000001D00000000stern-1.30.0/stern/target.go// Copyright 2017 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "fmt" "regexp" "sync" corev1 "k8s.io/api/core/v1" "k8s.io/klog/v2" ) // Target is a target to watch type Target struct { Node string Namespace string Pod string Container string } // GetID returns the ID of the object func (t *Target) GetID() string { return fmt.Sprintf("%s-%s-%s", t.Namespace, t.Pod, t.Container) } // targetState holds a last shown container ID type targetState struct { podUID string containerID string } // targetFilter is a filter of Target type targetFilter struct { c targetFilterConfig targetStates map[string]*targetState mu sync.RWMutex } type targetFilterConfig struct { podFilter *regexp.Regexp excludePodFilter []*regexp.Regexp containerFilter *regexp.Regexp containerExcludeFilter []*regexp.Regexp initContainers bool ephemeralContainers bool containerStates []ContainerState } func newTargetFilter(c targetFilterConfig) *targetFilter { return &targetFilter{ c: c, targetStates: make(map[string]*targetState), } } // visit passes filtered Targets to the visitor function func (f *targetFilter) visit(pod *corev1.Pod, visitor func(t *Target)) { // filter by pod if !f.c.podFilter.MatchString(pod.Name) { return } for _, re := range f.c.excludePodFilter { if re.MatchString(pod.Name) { return } } // filter by container statuses var statuses []corev1.ContainerStatus if f.c.initContainers { // show initContainers first when --no-follow and --max-log-requests 1 statuses = append(statuses, pod.Status.InitContainerStatuses...) } statuses = append(statuses, pod.Status.ContainerStatuses...) if f.c.ephemeralContainers { statuses = append(statuses, pod.Status.EphemeralContainerStatuses...) } OUTER: for _, c := range statuses { if !f.c.containerFilter.MatchString(c.Name) { continue } for _, re := range f.c.containerExcludeFilter { if re.MatchString(c.Name) { continue OUTER } } t := &Target{ Node: pod.Spec.NodeName, Namespace: pod.Namespace, Pod: pod.Name, Container: c.Name, } if f.shouldAdd(t, string(pod.UID), c) { visitor(t) } } } func (f *targetFilter) matchContainerState(state corev1.ContainerState) bool { for _, containerState := range f.c.containerStates { if containerState.Match(state) { return true } } return false } func (f *targetFilter) shouldAdd(t *Target, podUID string, cs corev1.ContainerStatus) bool { state := stateToString(cs.State) containerID := chooseContainerID(cs) f.mu.Lock() last := f.targetStates[t.GetID()] f.targetStates[t.GetID()] = &targetState{podUID: podUID, containerID: containerID} f.mu.Unlock() if containerID == "" { // does not have a container to retrieve logs klog.V(7).InfoS("Container ID is empty", "state", state, "target", t.GetID()) return false } if last == nil { // We filter out only containers that have existed before stern starts by container states. // The container state transition skips the "running" when a pod immediately completes, // so filtering by container states does not work as expected for newly created containers. klog.V(7).InfoS("Container ID has existed before observation", "state", state, "target", t.GetID(), "container", containerID) return f.matchContainerState(cs.State) } if last.containerID == containerID { klog.V(7).InfoS("Container ID is the same", "state", state, "target", t.GetID(), "container", containerID) return false } // add a container when the container ID is changed from the last time klog.V(7).InfoS("Container ID was changed", "state", state, "target", t.GetID(), "container", containerID, "last", last.containerID) return true } func (f *targetFilter) forget(podUID string) { f.mu.Lock() defer f.mu.Unlock() // delete target states belonging to the pod for targetID, state := range f.targetStates { if state.podUID == podUID { klog.V(7).InfoS("Forget targetState", "target", targetID) delete(f.targetStates, targetID) } } } func (f *targetFilter) isActive(t *Target) bool { f.mu.RLock() defer f.mu.RUnlock() last := f.targetStates[t.GetID()] return last != nil && last.containerID != "" } func chooseContainerID(cs corev1.ContainerStatus) string { // This logic is based on kubelet's validateContainerLogStatus // https://github.com/kubernetes/kubernetes/blob/v1.26.1/pkg/kubelet/kubelet_pods.go#L1246 switch { case cs.State.Running != nil: return cs.ContainerID case cs.State.Terminated != nil: if cs.State.Terminated.ContainerID != "" { return cs.State.Terminated.ContainerID } } lastTerminated := cs.LastTerminationState.Terminated if lastTerminated != nil && lastTerminated.ContainerID != "" { return lastTerminated.ContainerID } return "" } func stateToString(state corev1.ContainerState) string { switch { case state.Running != nil: return "running" case state.Terminated != nil: return "terminated" case state.Waiting != nil: return "waiting" } return "unknown" } 0707010000003E000081A4000000000000000000000001664F523B000044BD000000000000000000000000000000000000002200000000stern-1.30.0/stern/target_test.gopackage stern import ( "reflect" "regexp" "testing" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) func TestTargetFilter(t *testing.T) { running := corev1.ContainerState{Running: &corev1.ContainerStateRunning{}} terminated := corev1.ContainerState{Terminated: &corev1.ContainerStateTerminated{ContainerID: "dummy"}} waiting := corev1.ContainerState{Waiting: &corev1.ContainerStateWaiting{}} createPod := func(node, pod string) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: pod, }, Spec: corev1.PodSpec{ NodeName: node, }, Status: corev1.PodStatus{ InitContainerStatuses: []corev1.ContainerStatus{ {Name: "init-container1-running", State: running, ContainerID: "dummy"}, {Name: "init-container2-terminated", State: terminated}, {Name: "init-container3-waiting", State: waiting, LastTerminationState: terminated}, }, ContainerStatuses: []corev1.ContainerStatus{ {Name: "container1-running", State: running, ContainerID: "dummy"}, {Name: "container2-terminated", State: terminated}, {Name: "container3-waiting", State: waiting, LastTerminationState: terminated}, }, EphemeralContainerStatuses: []corev1.ContainerStatus{ {Name: "ephemeral-container1-running", State: running, ContainerID: "dummy"}, {Name: "ephemeral-container2-terminated", State: terminated}, {Name: "ephemeral-container3-waiting", State: waiting, LastTerminationState: terminated}, }, }, } } pods := []*corev1.Pod{ createPod("node1", "pod1"), createPod("node2", "pod2"), } genTarget := func(node, pod, container string) Target { return Target{ Namespace: "ns1", Node: node, Pod: pod, Container: container, } } tests := []struct { name string config targetFilterConfig expected []Target }{ { name: "match all", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container1-running"), genTarget("node1", "pod1", "init-container2-terminated"), genTarget("node1", "pod1", "init-container3-waiting"), genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node1", "pod1", "ephemeral-container1-running"), genTarget("node1", "pod1", "ephemeral-container2-terminated"), genTarget("node1", "pod1", "ephemeral-container3-waiting"), genTarget("node2", "pod2", "init-container1-running"), genTarget("node2", "pod2", "init-container2-terminated"), genTarget("node2", "pod2", "init-container3-waiting"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), genTarget("node2", "pod2", "ephemeral-container1-running"), genTarget("node2", "pod2", "ephemeral-container2-terminated"), genTarget("node2", "pod2", "ephemeral-container3-waiting"), }, }, { name: "filter by podFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(`not-matched`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{}, }, { name: "filter by excludePodFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(``), excludePodFilter: []*regexp.Regexp{regexp.MustCompile(`pod1`)}, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node2", "pod2", "init-container1-running"), genTarget("node2", "pod2", "init-container2-terminated"), genTarget("node2", "pod2", "init-container3-waiting"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), genTarget("node2", "pod2", "ephemeral-container1-running"), genTarget("node2", "pod2", "ephemeral-container2-terminated"), genTarget("node2", "pod2", "ephemeral-container3-waiting"), }, }, { name: "filter by multiple excludePodFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(``), excludePodFilter: []*regexp.Regexp{ regexp.MustCompile(`not-matched`), regexp.MustCompile(`pod2`), }, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container1-running"), genTarget("node1", "pod1", "init-container2-terminated"), genTarget("node1", "pod1", "init-container3-waiting"), genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node1", "pod1", "ephemeral-container1-running"), genTarget("node1", "pod1", "ephemeral-container2-terminated"), genTarget("node1", "pod1", "ephemeral-container3-waiting"), }, }, { name: "filter by containerFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*container1.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container1-running"), genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "ephemeral-container1-running"), genTarget("node2", "pod2", "init-container1-running"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "ephemeral-container1-running"), }, }, { name: "filter by containerExcludeFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: []*regexp.Regexp{regexp.MustCompile(`.*container1.*`)}, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container2-terminated"), genTarget("node1", "pod1", "init-container3-waiting"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node1", "pod1", "ephemeral-container2-terminated"), genTarget("node1", "pod1", "ephemeral-container3-waiting"), genTarget("node2", "pod2", "init-container2-terminated"), genTarget("node2", "pod2", "init-container3-waiting"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), genTarget("node2", "pod2", "ephemeral-container2-terminated"), genTarget("node2", "pod2", "ephemeral-container3-waiting"), }, }, { name: "filter by multiple containerExcludeFilter", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: []*regexp.Regexp{ regexp.MustCompile(`.*container1.*`), regexp.MustCompile(`init-container2.*`), }, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container3-waiting"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node1", "pod1", "ephemeral-container2-terminated"), genTarget("node1", "pod1", "ephemeral-container3-waiting"), genTarget("node2", "pod2", "init-container3-waiting"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), genTarget("node2", "pod2", "ephemeral-container2-terminated"), genTarget("node2", "pod2", "ephemeral-container3-waiting"), }, }, { name: "dot not include initContainers", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: false, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node1", "pod1", "ephemeral-container1-running"), genTarget("node1", "pod1", "ephemeral-container2-terminated"), genTarget("node1", "pod1", "ephemeral-container3-waiting"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), genTarget("node2", "pod2", "ephemeral-container1-running"), genTarget("node2", "pod2", "ephemeral-container2-terminated"), genTarget("node2", "pod2", "ephemeral-container3-waiting"), }, }, { name: "dot not include ephemeralContainers", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: false, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container1-running"), genTarget("node1", "pod1", "init-container2-terminated"), genTarget("node1", "pod1", "init-container3-waiting"), genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "container2-terminated"), genTarget("node1", "pod1", "container3-waiting"), genTarget("node2", "pod2", "init-container1-running"), genTarget("node2", "pod2", "init-container2-terminated"), genTarget("node2", "pod2", "init-container3-waiting"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "container2-terminated"), genTarget("node2", "pod2", "container3-waiting"), }, }, { name: "match running states", config: targetFilterConfig{ podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING}, }, expected: []Target{ genTarget("node1", "pod1", "init-container1-running"), genTarget("node1", "pod1", "container1-running"), genTarget("node1", "pod1", "ephemeral-container1-running"), genTarget("node2", "pod2", "init-container1-running"), genTarget("node2", "pod2", "container1-running"), genTarget("node2", "pod2", "ephemeral-container1-running"), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := []Target{} for _, pod := range pods { filter := newTargetFilter(tt.config) filter.visit(pod, func(target *Target) { actual = append(actual, *target) }) } if !reflect.DeepEqual(tt.expected, actual) { t.Errorf("expected %v, but actual %v", tt.expected, actual) } }) } } func TestTargetFilterShouldAdd(t *testing.T) { filter := newTargetFilter(targetFilterConfig{ // matches all podFilter: regexp.MustCompile(`.*`), excludePodFilter: nil, containerFilter: regexp.MustCompile(`.*`), containerExcludeFilter: nil, initContainers: true, ephemeralContainers: true, containerStates: []ContainerState{RUNNING, TERMINATED, WAITING}, }) createPod := func(cs corev1.ContainerStatus) *corev1.Pod { return &corev1.Pod{ ObjectMeta: metav1.ObjectMeta{ Namespace: "ns1", Name: "pod1", UID: "uid1", }, Spec: corev1.PodSpec{ NodeName: "node1", }, Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{cs}, }, } } genTarget := func(container string) Target { return Target{ Namespace: "ns1", Node: "node1", Pod: "pod1", Container: container, } } tests := []struct { name string forget bool cs corev1.ContainerStatus expected []Target }{ { name: "empty state should be ignored", cs: corev1.ContainerStatus{Name: "c1"}, expected: []Target{}, }, { name: "running container observed the first time", cs: corev1.ContainerStatus{ Name: "c1", ContainerID: "cid1", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: []Target{genTarget("c1")}, }, { name: "same container ID should be ignored", cs: corev1.ContainerStatus{ Name: "c1", ContainerID: "cid1", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: []Target{}, }, { name: "different container ID can be added", cs: corev1.ContainerStatus{ Name: "c1", ContainerID: "cid2", // changed State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: []Target{genTarget("c1")}, }, { name: "forget() allows the same ID ", forget: true, cs: corev1.ContainerStatus{ Name: "c1", ContainerID: "cid2", State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: []Target{genTarget("c1")}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.forget { filter.forget("uid1") } actual := []Target{} filter.visit(createPod(tt.cs), func(target *Target) { actual = append(actual, *target) }) if !reflect.DeepEqual(tt.expected, actual) { t.Errorf("expected %v, but actual %v", tt.expected, actual) } }) } } func TestChooseContainerID(t *testing.T) { lastState := corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ContainerID: "last", }, } tests := []struct { name string cs corev1.ContainerStatus expected string }{ { name: "running", cs: corev1.ContainerStatus{ ContainerID: "current", LastTerminationState: lastState, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: "current", }, { name: "running (empty)", cs: corev1.ContainerStatus{ LastTerminationState: lastState, State: corev1.ContainerState{ Running: &corev1.ContainerStateRunning{}, }, }, expected: "", }, { name: "terminated (current terminated container)", cs: corev1.ContainerStatus{ ContainerID: "current", LastTerminationState: lastState, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{ ContainerID: "terminated", }, }, }, expected: "terminated", }, { name: "terminated (last terminated container)", cs: corev1.ContainerStatus{ ContainerID: "current", LastTerminationState: lastState, State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{}, }, }, expected: "last", }, { name: "terminated (empty)", cs: corev1.ContainerStatus{ State: corev1.ContainerState{ Terminated: &corev1.ContainerStateTerminated{}, }, }, expected: "", }, { name: "waiting", cs: corev1.ContainerStatus{ ContainerID: "current", LastTerminationState: lastState, State: corev1.ContainerState{ Waiting: &corev1.ContainerStateWaiting{}, }, }, expected: "last", }, { name: "waiting (empty)", cs: corev1.ContainerStatus{ ContainerID: "current", // should be ignored State: corev1.ContainerState{ Waiting: &corev1.ContainerStateWaiting{}, }, }, expected: "", }, { name: "no current state with last state", cs: corev1.ContainerStatus{ ContainerID: "current", LastTerminationState: lastState, }, expected: "last", }, { name: "empty state", cs: corev1.ContainerStatus{}, expected: "", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := chooseContainerID(tt.cs) if tt.expected != actual { t.Errorf("expected %v, but actual %v", tt.expected, actual) } }) } } 0707010000003F000081A4000000000000000000000001664F523B00000954000000000000000000000000000000000000001C00000000stern-1.30.0/stern/watch.go// Copyright 2016 Wercker Holding BV // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package stern import ( "context" "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/watch" v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/cache" watchtools "k8s.io/client-go/tools/watch" ) // Watch starts listening to Kubernetes events and emits modified // containers/pods. The result is targets added. func WatchTargets(ctx context.Context, i v1.PodInterface, labelSelector labels.Selector, fieldSelector fields.Selector, filter *targetFilter) (chan *Target, error) { // RetryWatcher will make sure that in case the underlying watcher is // closed (e.g. due to API timeout or etcd timeout) it will get restarted // from the last point without the consumer even knowing about it. watcher, err := watchtools.NewRetryWatcher("1", &cache.ListWatch{ WatchFunc: func(options metav1.ListOptions) (watch.Interface, error) { return i.Watch(ctx, metav1.ListOptions{LabelSelector: labelSelector.String(), FieldSelector: fieldSelector.String()}) }, }) if err != nil { return nil, errors.Wrap(err, "failed to create a watcher") } added := make(chan *Target) go func() { for { select { case e := <-watcher.ResultChan(): if e.Object == nil { // Closed because of error close(added) return } pod, ok := e.Object.(*corev1.Pod) if !ok { continue } switch e.Type { case watch.Added, watch.Modified: filter.visit(pod, func(t *Target) { added <- t }) case watch.Deleted: filter.forget(string(pod.UID)) } case <-ctx.Done(): watcher.Stop() close(added) return } } }() return added, nil } 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!471 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor