File stern-1.32.0.obscpio of Package stern

07070100000000000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001500000000stern-1.32.0/.github07070100000001000081A4000000000000000000000001678EF50A0000002B000000000000000000000000000000000000002000000000stern-1.32.0/.github/CODEOWNERS*   @superbrothers @floryut @rkmathi @tksm
07070100000002000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000002400000000stern-1.32.0/.github/ISSUE_TEMPLATE07070100000003000081A4000000000000000000000001678EF50A00000183000000000000000000000000000000000000003200000000stern-1.32.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:
07070100000004000081A4000000000000000000000001678EF50A00000094000000000000000000000000000000000000003300000000stern-1.32.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**:
07070100000005000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001F00000000stern-1.32.0/.github/workflows07070100000006000081A4000000000000000000000001678EF50A000002E8000000000000000000000000000000000000002700000000stern-1.32.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
07070100000007000081A4000000000000000000000001678EF50A000000F9000000000000000000000000000000000000003300000000stern-1.32.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"'
07070100000008000081A4000000000000000000000001678EF50A0000029C000000000000000000000000000000000000002C00000000stern-1.32.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.47
      with:
        krew_template_file: dist/krew/stern.yaml
07070100000009000081A4000000000000000000000001678EF50A0000002B000000000000000000000000000000000000001800000000stern-1.32.0/.gitignore/dist
/hack/tools/bin
vendor
.idea
.vscode
0707010000000A000081A4000000000000000000000001678EF50A000000A1000000000000000000000000000000000000001B00000000stern-1.32.0/.golangci.ymlrun:
  timeout: 5m
linters:
  disable-all: true
  enable:
  - errcheck
  - gofmt
  - gosimple
  - govet
  - ineffassign
  - staticcheck
  - typecheck
  - unused
0707010000000B000081A4000000000000000000000001678EF50A0000062B000000000000000000000000000000000000001E00000000stern-1.32.0/.goreleaser.yamlversion: 2
builds:
- 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
0707010000000C000081A4000000000000000000000001678EF50A0000544F000000000000000000000000000000000000001A00000000stern-1.32.0/CHANGELOG.md# v1.32.0

## :zap: Notable Changes

### A new template function `prettyJSON`

You can now use a new template function `prettyJSON` that parse input and emit it as pretty printed JSON. If it parse fails output string as is.

```
# Will try to parse .Message as JSON and pretty print it, if not json will output as is
stern --template='{{ .Message | prettyJSON }}{{"\n"}}' backend
# Or with parsed json, will drop non-json logs because of `with`
stern --template='{{ with $msg := .Message | tryParseJSON }}{{ prettyJSON $msg }}{{"\n"}}{{end}}' backend
```

### A new template function `bunyanLevelColor`

You can now use a new template function `bunyanLevelColor` that print [bunyan](https://github.com/trentm/node-bunyan) numeric log level using appropriate color.

### A new flag `--condition`

A new `--condition` allows you to filter logs with the pod condition on: `[condition-name[=condition-value]`. The default condition-value is true. Match is case-insensitive. Currently, it is only supported with --tail=0 or --no-follow.

```
# Only display logs for pods that are not ready:
stern . --condition=ready=false --tail=0
```

## Changes

* Add `--condition` (#276) 2576972 (Felipe Santos)
* Add check for when `--no-follow` is set with `--tail=0` (#331) 276e906 (Felipe Santos)
* Implement JSON pretty print (#324) ccd8add (Fabio Napoleoni)
* Fix descriptions of `extjson` and `ppextjson` (#325) d9a9858 (Takashi Kusumi)
* Allow `levelColor` template function to parse numbers (#321) db69276 (Jimmie Högklint)

# v1.31.0

## Changes
* Fix --verbosity flag to show missing logs ([#317](https://github.com/stern/stern/pull/317)) c2b4410 (Takashi Kusumi)
* Update dependencies for Kubernetes 1.31 ([#315](https://github.com/stern/stern/pull/315)) a4fdcc9 (Takashi Kusumi)

# 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`
- `toUTC`
- `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)
0707010000000D000081A4000000000000000000000001678EF50A00000393000000000000000000000000000000000000001D00000000stern-1.32.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.
0707010000000E000081A4000000000000000000000001678EF50A000000AB000000000000000000000000000000000000001800000000stern-1.32.0/DockerfileFROM gcr.io/distroless/static-debian12
LABEL org.opencontainers.image.source https://github.com/stern/stern
COPY stern /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/stern"]
0707010000000F000081A4000000000000000000000001678EF50A00002C5D000000000000000000000000000000000000001500000000stern-1.32.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.
07070100000010000081A4000000000000000000000001678EF50A0000084E000000000000000000000000000000000000001600000000stern-1.32.0/MakefileSHELL:=/usr/bin/env bash

.PHONY: build
build:
	go build -o dist/stern .

TOOLS_BIN_DIR := $(CURDIR)/hack/tools/bin
GORELEASER_VERSION ?= v2.5.1
GORELEASER := $(TOOLS_BIN_DIR)/goreleaser
GOLANGCI_LINT_VERSION ?= v1.63.4
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/v2@$(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
07070100000011000081A4000000000000000000000001678EF50A00005677000000000000000000000000000000000000001700000000stern-1.32.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'.
 `--condition`               |                               | The condition to filter on: [condition-name[=condition-value]. The default condition-value is true. Match is case-insensitive. Currently only supported with --tail=0 or --no-follow.
 `--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                                     |
| `extjson`   | Outputs extended JSON with colorized pod/container names                                              |
| `ppextjson` | Pretty-prints extended JSON with colorized pod/container names                                        |

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                                                    |
| `prettyJSON`          | `any`                       | Parse input and emit it as pretty printed JSON, if parse fails output string as is.                                                         |
| `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                                                                                                     |
| `bunyanLevelColor`    | `string`                    | Print [bunyan](https://github.com/trentm/node-bunyan) numeric 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
```

Pretty print JSON (if it is JSON) and output it:

```
# Will try to parse .Message as JSON and pretty print it, if not json will output as is
stern --template='{{ .Message | prettyJSON }}{{"\n"}}' backend
# Or with parsed json, will drop non-json logs because of `with`
stern --template='{{ with $msg := .Message | tryParseJSON }}{{ prettyJSON $msg }}{{"\n"}}{{end}}' 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
```

Only display logs for pods that are not ready:

```
stern . --condition=ready=false --tail=0
```

## 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.
07070100000012000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001100000000stern-1.32.0/cmd07070100000013000081A4000000000000000000000001678EF50A000064C8000000000000000000000000000000000000001800000000stern-1.32.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
	condition           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},
		condition:           "",
		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 cannot be set at the same time")
	}
	if o.noFollow && o.tail == 0 {
		return errors.New("--no-follow cannot be used with --tail=0")
	}
	if o.condition != "" && o.tail != 0 && !o.noFollow {
		return errors.New("--condition is currently only supported with --tail=0 or --no-follow")
	}

	return nil
}

func (o *options) Run(cmd *cobra.Command) error {
	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")
	}

	condition := stern.Condition{}
	if o.condition != "" {
		condition, err = stern.NewCondition(o.condition)
		if err != nil {
			return nil, err
		}
	}

	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,
		Condition:             condition,
		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.StringVar(&o.condition, "condition", o.condition, "The condition to filter on: [condition-name[=condition-value]. The default condition-value is true. Match is case-insensitive. Currently only supported with --tail=0 or --no-follow.")
	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
		},
		"prettyJSON": func(value any) string {
			var data map[string]any

			switch v := value.(type) {
			case string:
				if err := json.Unmarshal([]byte(v), &data); err != nil {
					return v
				}
			case map[string]any:
				data = v
			default:
				return fmt.Sprintf("%v", value)
			}

			b, err := json.MarshalIndent(data, "", "  ")
			if err != nil {
				return fmt.Sprintf("%v", value)
			}

			return string(b)
		},
		"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(value any) string {
			switch level := value.(type) {
			case 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)
			default:
				return ""
			}
		},
		"bunyanLevelColor": func(value any) string {
			var lv int64
			var err error

			switch level := value.(type) {
			// tryParseJSON yields json.Number
			case json.Number:
				lv, err = level.Int64()
				if err != nil {
					return ""
				}
			// parseJSON yields float64
			case float64:
				lv = int64(level)
			default:
				return ""
			}

			var levelColor *color.Color
			switch {
			case lv < 30:
				levelColor = color.New(color.FgMagenta)
			case lv < 40:
				levelColor = color.New(color.FgBlue)
			case lv < 50:
				levelColor = color.New(color.FgYellow)
			case lv < 60:
				levelColor = color.New(color.FgRed)
			case lv < 100:
				levelColor = color.New(color.FgCyan)
			default:
				return strconv.FormatInt(lv, 10)
			}
			return levelColor.SprintFunc()(lv)
		},
	}
	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 {
			// klog's v flag should be initialized before creating a k8s client
			if err := o.setVerbosity(); err != nil {
				return err
			}

			// 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
}
07070100000014000081A4000000000000000000000001678EF50A00005297000000000000000000000000000000000000001D00000000stern-1.32.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 cannot be set at the same time",
		},
		{
			"Specify both --no-follow and --tail=0",
			func() *options {
				o := NewOptions(streams)
				o.podQuery = "."
				o.noFollow = true
				o.tail = 0

				return o
			}(),
			"--no-follow cannot be used with --tail=0",
		},
		{
			"Specify --condition without --tail=0 and no --no-follow",
			func() *options {
				o := NewOptions(streams)
				o.podQuery = "."
				o.condition = "ready=false"

				return o
			}(),
			"--condition is currently only supported with --tail=0 or --no-follow",
		},
		{
			"Specify --condition without --no-follow and no --tail=0",
			func() *options {
				o := NewOptions(streams)
				o.podQuery = "."
				o.condition = "ready=false"

				return o
			}(),
			"--condition is currently only supported with --tail=0 or --no-follow",
		},
		{
			"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)
			}
		})
	}

}
07070100000015000081A4000000000000000000000001678EF50A0000232C000000000000000000000000000000000000002400000000stern-1.32.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
}
07070100000016000081A4000000000000000000000001678EF50A00000CF3000000000000000000000000000000000000002900000000stern-1.32.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
				}
			}
		})
	}
}
07070100000017000081A4000000000000000000000001678EF50A0000065B000000000000000000000000000000000000002000000000stern-1.32.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
}
07070100000018000081A4000000000000000000000001678EF50A00000134000000000000000000000000000000000000002100000000stern-1.32.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)
	}
}
07070100000019000081A4000000000000000000000001678EF50A00000434000000000000000000000000000000000000001B00000000stern-1.32.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
}
0707010000001A000081A4000000000000000000000001678EF50A000005AA000000000000000000000000000000000000002000000000stern-1.32.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())
			}
		})
	}
}
0707010000001B000081A4000000000000000000000001678EF50A000000EE000000000000000000000000000000000000001A00000000stern-1.32.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 }}0707010000001C000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001A00000000stern-1.32.0/cmd/testdata0707010000001D000081A4000000000000000000000001678EF50A0000000C000000000000000000000000000000000000002D00000000stern-1.32.0/cmd/testdata/config-array0.yamlexclude: []
0707010000001E000081A4000000000000000000000001678EF50A00000012000000000000000000000000000000000000002D00000000stern-1.32.0/cmd/testdata/config-array1.yamlexclude: ["abcd"]
0707010000001F000081A4000000000000000000000001678EF50A0000001A000000000000000000000000000000000000002D00000000stern-1.32.0/cmd/testdata/config-array2.yamlexclude: ["abcd", "efgh"]
07070100000020000081A4000000000000000000000001678EF50A00000000000000000000000000000000000000000000002C00000000stern-1.32.0/cmd/testdata/config-empty.yaml07070100000021000081A4000000000000000000000001678EF50A0000001C000000000000000000000000000000000000002E00000000stern-1.32.0/cmd/testdata/config-invalid.yamlthis is invalid config file
07070100000022000081A4000000000000000000000001678EF50A00000017000000000000000000000000000000000000002D00000000stern-1.32.0/cmd/testdata/config-string.yamlexclude: "hello-world"
07070100000023000081A4000000000000000000000001678EF50A0000000E000000000000000000000000000000000000003900000000stern-1.32.0/cmd/testdata/config-tail-invalid-value.yamltail: invalid
07070100000024000081A4000000000000000000000001678EF50A00000008000000000000000000000000000000000000002C00000000stern-1.32.0/cmd/testdata/config-tail1.yamltail: 1
07070100000025000081A4000000000000000000000001678EF50A00000015000000000000000000000000000000000000003500000000stern-1.32.0/cmd/testdata/config-unknown-option.yamlunknown: 999
tail: 1
07070100000026000081A4000000000000000000000001678EF50A00000CBA000000000000000000000000000000000000001400000000stern-1.32.0/go.modmodule github.com/stern/stern

go 1.23.5

require (
	github.com/AlecAivazis/survey/v2 v2.3.7
	github.com/fatih/color v1.18.0
	github.com/mitchellh/go-homedir v1.1.0
	github.com/pkg/errors v0.9.1
	github.com/spf13/cast v1.7.1
	github.com/spf13/cobra v1.8.1
	github.com/spf13/pflag v1.0.5
	golang.org/x/sync v0.10.0
	golang.org/x/time v0.9.0
	gopkg.in/yaml.v3 v3.0.1
	k8s.io/api v0.32.1
	k8s.io/apimachinery v0.32.1
	k8s.io/cli-runtime v0.32.1
	k8s.io/client-go v0.32.1
	k8s.io/klog/v2 v2.130.1
	k8s.io/utils v0.0.0-20241210054802-24370beab758
)

require (
	github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // 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.1 // indirect
	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
	github.com/go-errors/errors v1.5.1 // indirect
	github.com/go-logr/logr v1.4.2 // indirect
	github.com/go-openapi/jsonpointer v0.21.0 // indirect
	github.com/go-openapi/jsonreference v0.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.3 // indirect
	github.com/google/gnostic-models v0.6.9 // 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/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.9.0 // indirect
	github.com/mattn/go-colorable v0.1.14 // 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.2 // 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/x448/float16 v0.8.4 // indirect
	github.com/xlab/treeprint v1.2.0 // indirect
	golang.org/x/net v0.34.0 // indirect
	golang.org/x/oauth2 v0.25.0 // indirect
	golang.org/x/sys v0.29.0 // indirect
	golang.org/x/term v0.28.0 // indirect
	golang.org/x/text v0.21.0 // indirect
	google.golang.org/protobuf v1.36.3 // indirect
	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
	gopkg.in/inf.v0 v0.9.1 // indirect
	k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect
	sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
	sigs.k8s.io/kustomize/api v0.19.0 // indirect
	sigs.k8s.io/kustomize/kyaml v0.19.0 // indirect
	sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect
	sigs.k8s.io/yaml v1.4.0 // indirect
)
07070100000027000081A4000000000000000000000001678EF50A00005474000000000000000000000000000000000000001400000000stern-1.32.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-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/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.4/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.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU=
github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
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/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-errors/errors v1.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.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/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/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/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.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.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-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/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/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.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=
github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
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.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
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.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
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.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.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/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=
github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70=
golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
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.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.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.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.26.0 h1:v/60pFQmzmT9ExmjDv2gGIfi3OqfKoEP6I5+umXlbnQ=
golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU=
google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.1 h1:f562zw9cy+GvXzXf0CKlVQ7yHJVYzLfL6JAS4kOAaOc=
k8s.io/api v0.32.1/go.mod h1:/Yi/BqkuueW1BgpoePYBRdDYfjPF5sgTr5+YqDZra5k=
k8s.io/apimachinery v0.32.1 h1:683ENpaCBjma4CYqsmZyhEzrGz6cjn1MY/X2jB2hkZs=
k8s.io/apimachinery v0.32.1/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/cli-runtime v0.32.1 h1:19nwZPlYGJPUDbhAxDIS2/oydCikvKMHsxroKNGA2mM=
k8s.io/cli-runtime v0.32.1/go.mod h1:NJPbeadVFnV2E7B7vF+FvU09mpwYlZCu8PqjzfuOnkY=
k8s.io/client-go v0.32.1 h1:otM0AxdhdBIaQh7l1Q0jQpmo7WOFIk5FFa4bg6YMdUU=
k8s.io/client-go v0.32.1/go.mod h1:aTTKZY7MdxUaJ/KiUs8D+GssR9zJZi77ZqtzcGXIiDg=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg=
k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas=
k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0=
k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/kustomize/api v0.19.0 h1:F+2HB2mU1MSiR9Hp1NEgoU2q9ItNOaBJl0I4Dlus5SQ=
sigs.k8s.io/kustomize/api v0.19.0/go.mod h1:/BbwnivGVcBh1r+8m3tH1VNxJmHSk1PzP5fkP6lbL1o=
sigs.k8s.io/kustomize/kyaml v0.19.0 h1:RFge5qsO1uHhwJsu3ipV7RNolC7Uozc0jUBC/61XSlA=
sigs.k8s.io/kustomize/kyaml v0.19.0/go.mod h1:FeKD5jEOH+FbZPpqUghBP8mrLjJ3+zD3/rf9NNu1cwY=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk=
sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
07070100000028000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001200000000stern-1.32.0/hack07070100000029000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000002000000000stern-1.32.0/hack/update-readme0707010000002A000081A4000000000000000000000001678EF50A000010FA000000000000000000000000000000000000003100000000stern-1.32.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
}
0707010000002B000081ED000000000000000000000001678EF50A000001D3000000000000000000000000000000000000002300000000stern-1.32.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
0707010000002C000081A4000000000000000000000001678EF50A000003BC000000000000000000000000000000000000001500000000stern-1.32.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)
	}
}
0707010000002D000041ED000000000000000000000002678EF50A00000000000000000000000000000000000000000000001300000000stern-1.32.0/stern0707010000002E000081A4000000000000000000000001678EF50A000008E2000000000000000000000000000000000000001C00000000stern-1.32.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
}
0707010000002F000081A4000000000000000000000001678EF50A00000B34000000000000000000000000000000000000002100000000stern-1.32.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])
				}
			}
		})
	}
}
07070100000030000081A4000000000000000000000001678EF50A000008F2000000000000000000000000000000000000002000000000stern-1.32.0/stern/condition.gopackage stern

import (
	"fmt"
	"strings"

	v1 "k8s.io/api/core/v1"
)

type Condition struct {
	Name  v1.PodConditionType
	Value v1.ConditionStatus
}

// NewCondition returns a Condition struct for a given conditionString
func NewCondition(conditionString string) (Condition, error) {
	// condition can be: condition-name or condition-name=condition-value
	conditionNameString := strings.ToLower(conditionString)
	conditionValueString := "true"
	if equalsIndex := strings.Index(conditionNameString, "="); equalsIndex != -1 {
		conditionValueString = conditionNameString[equalsIndex+1:]
		conditionNameString = conditionNameString[0:equalsIndex]
	}

	var conditionName v1.PodConditionType

	validConditions := []v1.PodConditionType{
		v1.ContainersReady,
		v1.PodInitialized,
		v1.PodReady,
		v1.PodScheduled,
		v1.DisruptionTarget,
		v1.PodReadyToStartContainers,
	}

	for _, validCondition := range validConditions {
		if strings.ToLower(string(validCondition)) == conditionNameString {
			conditionName = validCondition
			break
		}
	}

	if conditionName == "" {
		validConditionsStrings := make([]string, len(validConditions))
		for i, val := range validConditions {
			validConditionsStrings[i] = string(val)
		}
		return Condition{}, fmt.Errorf("condition should be one of '%s'", strings.Join(validConditionsStrings, "', '"))
	}

	var conditionValue v1.ConditionStatus

	validValues := []v1.ConditionStatus{
		v1.ConditionTrue,
		v1.ConditionFalse,
		v1.ConditionUnknown,
	}

	for _, validValue := range validValues {
		if strings.ToLower(string(validValue)) == conditionValueString {
			conditionValue = validValue
			break
		}
	}

	if conditionValue == "" {
		validValuesStrings := make([]string, len(validValues))
		for i, val := range validValues {
			validValuesStrings[i] = string(val)
		}
		return Condition{}, fmt.Errorf("condition value should be one of '%s'", strings.Join(validValuesStrings, "', '"))
	}

	return Condition{
		Name:  conditionName,
		Value: conditionValue,
	}, nil
}

// Match returns if pod matches the condition
func (conditionConfig Condition) Match(podConditions []v1.PodCondition) bool {
	for _, condition := range podConditions {
		if condition.Type == conditionConfig.Name {
			return condition.Status == conditionConfig.Value
		}
	}

	return false
}
07070100000031000081A4000000000000000000000001678EF50A000007DC000000000000000000000000000000000000002500000000stern-1.32.0/stern/condition_test.gopackage stern

import (
	"testing"

	v1 "k8s.io/api/core/v1"
)

func TestNewCondition(t *testing.T) {
	tests := []struct {
		conditionString string
		expected        Condition
		isError         bool
	}{
		{
			"Ready",
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionTrue,
			},
			false,
		},
		{
			"ready=true",
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionTrue,
			},
			false,
		},
		{
			"Ready=False",
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionFalse,
			},
			false,
		},
		{
			"ready=Unknown",
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionUnknown,
			},
			false,
		},
		{
			"beautiful",
			Condition{},
			true,
		},
		{
			"ready=NotYet",
			Condition{},
			true,
		},
	}

	for i, tt := range tests {
		condition, err := NewCondition(tt.conditionString)

		if tt.expected != condition {
			t.Errorf("%d: expected %v, but actual %v", i, tt.expected, condition)
		}

		if (tt.isError && err == nil) || (!tt.isError && err != nil) {
			t.Errorf("%d: expected error is %v, but actual %v", i, tt.isError, err)
		}
	}
}

func TestConditionMatch(t *testing.T) {
	tests := []struct {
		condition       Condition
		v1PodConditions []v1.PodCondition
		expected        bool
	}{
		{
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionTrue,
			},
			[]v1.PodCondition{
				{
					Type:   v1.PodReady,
					Status: v1.ConditionTrue,
				},
			},
			true,
		},
		{
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionTrue,
			},
			[]v1.PodCondition{
				{
					Type:   v1.PodReady,
					Status: v1.ConditionFalse,
				},
			},
			false,
		},
		{
			Condition{
				Name:  v1.PodReady,
				Value: v1.ConditionTrue,
			},
			[]v1.PodCondition{
				{
					Type:   v1.PodInitialized,
					Status: v1.ConditionFalse,
				},
			},
			false,
		},
	}

	for i, tt := range tests {
		actual := tt.condition.Match(tt.v1PodConditions)

		if tt.expected != actual {
			t.Errorf("%d: expected %v, but actual %v", i, tt.expected, actual)
		}
	}
}
07070100000032000081A4000000000000000000000001678EF50A000006EA000000000000000000000000000000000000001D00000000stern-1.32.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
	Condition             Condition
	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
}
07070100000033000081A4000000000000000000000001678EF50A0000067C000000000000000000000000000000000000002600000000stern-1.32.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)
}
07070100000034000081A4000000000000000000000001678EF50A000007EA000000000000000000000000000000000000002B00000000stern-1.32.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 TestContainerStateMatch(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)
		}
	}
}
07070100000035000081A4000000000000000000000001678EF50A000006B0000000000000000000000000000000000000002000000000stern-1.32.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)
}
07070100000036000081A4000000000000000000000001678EF50A0000035E000000000000000000000000000000000000002500000000stern-1.32.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)
			}
		})
	}
}
07070100000037000081A4000000000000000000000001678EF50A000009AC000000000000000000000000000000000000001B00000000stern-1.32.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, conditionFound bool) {
			if conditionFound {
				targets = append(targets, t)
			}
		})
	}
	return targets, nil
}
07070100000038000081A4000000000000000000000001678EF50A00000986000000000000000000000000000000000000002700000000stern-1.32.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,
	}
)
07070100000039000081A4000000000000000000000001678EF50A00002176000000000000000000000000000000000000001C00000000stern-1.32.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"
	"sync"
	"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,
		condition:              config.Condition,
		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
			}
		}
	}

	cancelMap := sync.Map{}
	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, d, 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)
					}
					ctx, cancel := context.WithCancel(nctx)
					cancelMap.Store(target.GetID(), cancel)
					go func() {
						tailTarget(ctx, target)
						numRequests.Add(-1)
						cancel()
						cancelMap.Delete(target.GetID())
					}()
				case target := <-d:
					if cancel, ok := cancelMap.LoadAndDelete(target.GetID()); ok {
						cancel.(context.CancelFunc)()
					}
				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)
}
0707010000003A000081A4000000000000000000000001678EF50A0000104F000000000000000000000000000000000000002100000000stern-1.32.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)
				}
			}
		})
	}
}
0707010000003B000081A4000000000000000000000001678EF50A00001FE3000000000000000000000000000000000000001B00000000stern-1.32.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:]
}
0707010000003C000081A4000000000000000000000001678EF50A00001D29000000000000000000000000000000000000002000000000stern-1.32.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)
		}
	}
}
0707010000003D000081A4000000000000000000000001678EF50A00000994000000000000000000000000000000000000002100000000stern-1.32.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
}
0707010000003E000081A4000000000000000000000001678EF50A00001A27000000000000000000000000000000000000002600000000stern-1.32.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)
		}
	}
}
0707010000003F000081A4000000000000000000000001678EF50A0000175F000000000000000000000000000000000000001D00000000stern-1.32.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
	condition              Condition
	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, conditionFound bool)) {
	// 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 condition
	conditionFound := true
	if f.c.condition != (Condition{}) {
		conditionFound = f.c.condition.Match(pod.Status.Conditions)
	}

	// 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 !conditionFound {
			visitor(t, false)
			f.forget(string(pod.UID))
			continue
		}

		if f.shouldAdd(t, string(pod.UID), c) {
			visitor(t, true)
		}
	}
}

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"
}
07070100000040000081A4000000000000000000000001678EF50A000044DD000000000000000000000000000000000000002200000000stern-1.32.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, condition bool) {
					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, condition bool) {
				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)
			}
		})
	}
}
07070100000041000081A4000000000000000000000001678EF50A000009ED000000000000000000000000000000000000001C00000000stern-1.32.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) (added, deleted chan *Target, err 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, nil, errors.Wrap(err, "failed to create a watcher")
	}

	added = make(chan *Target)
	deleted = 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, conditionFound bool) {
						if conditionFound {
							added <- t
						} else {
							deleted <- t
						}
					})
				case watch.Deleted:
					filter.forget(string(pod.UID))
				}
			case <-ctx.Done():
				watcher.Stop()
				close(added)
				return
			}
		}
	}()

	return added, deleted, nil
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!494 blocks
openSUSE Build Service is sponsored by